【百面成神】JVM基础12问,你能坚持到第几问

简介: 【百面成神】JVM基础12问,你能坚持到第几问

1.JVM调优的经历

脚本慢:程序流程图,加log计时

发现特别慢的时间结点,看代码逻辑,从逻辑上不可能有特别耗时的操作。

考虑是垃圾回收操作。

使用JVisaulVM进行排查,发现是在做垃圾回收。

堆做dump,定位到代码。

有几个地方。

第一个地方是,生成报告的结点是循环依赖的。A、B互相持有,由于JVM的回收机制是根可达算法,会堆积到老年代回收。设置clear()予以解决。

第二个是将缓存变量由类的实例改称了类。

第三个是将concurrentHashMap改为synchornized和WeakHashMap

2.JVM的主要组成部分/内存结构

程序计数器、

堆(解决的是存储数据问题,只有一个,类的实例对象以及数组,线程共享,垃圾回收器会自动回收不再使用的对象)

栈(解决的是程序运行时的问题,是线程所私有的,描述的是java方法所执行的内存信息,每个方法都会创建一个栈帧用于存储局部变量、程序的运行状态、方法返回值等信息,每个方法从调用到结束,就对应着一个栈帧从入栈到出栈的过程。栈中数据直接存放在寄存器中,存取速度比堆要快)、

方法区(类锁共享,常量池、接口定义等)

本地方法区、类加载器(加载类到内存)

3.强引用、弱引用、虚引用

强引用就是不会被回收,当虚拟机内存空间不足时,宁愿抛出OutOfMemoryError也不清除它。一般我们使用的都是强引用。

软引用用来存放一些有用但不是必须的对象,只有当内存不足的时候才会回收。

弱(WeakReference)引用用来存放一些不重要的对象,只要进行回收就会被清理回收(如缓存,可以由这个降到jvm调优的实例)

虚引用并不影响对象的生命周期,随时可能被回收,只是一个标记,主要用来跟踪对象的垃圾回收活动。

4.Serial,Parallel,CMS,G1四大GC收集器有什么区别?

Serial收集器是单线程收集器,在他进行垃圾回收时,其它工作线程都必须等待,直到它收集结束。其cpu利用率最高,但用户的等待时间最长。比较适合小型的应用

Parrallel收集器,是多线程收集器,其停顿时间短,回收效率高,吞吐量很大,比较适合于科学计算,大型应用等。

CMS收集器,采用多线程进行垃圾回收,其算法是标记-清除算法。它以响应时间优先,可以减少垃圾回收的停顿时间,适合于电信领域、服务器等。

G1收集器,吸收CMS收集器的特点,支持高吞吐量、支持很大的堆。

5.聊聊标记清除算法、复制算法和标记整理算法、分代回收算法?

标记清除算法:

当JVM堆内存空间满以后,会做两件事情:标记和清除。

第一是收集器从根节点开始标记所有被根节点引用的对象。

第二是收集器对堆内存从头到尾进行线性扫描,发现没有被标记的对象进行清除。

值得注意的是,清除并不是物理意义上的清除,只是将其记录在空闲地址列表里,当需为了新对象申请存储空间时,判断这个存储区域的空间是不是够用,如果够用则覆盖这个存储区域。

标记清除算法具有下列缺点:可能产生许多内存碎片。进行GC时,需要stw,用户体验感较差。其效率不是很高。

复制算法。复制算法相对于标记清除效率更高一点,但不适用于活跃对象太多的场合,比如老年代。该算法把内存区域分成两半,每次只使用其中一块,但进行垃圾回收时,将活跃的对象复制到另一块内存。将之前使用的内存全部清除。最后将两块内存的角色互换。

复制算法有一个显著的缺点,就是需要浪费掉一半内存。可以配合一个担保空间(老年代)来完善。将大对象存放到担保空间中,这样就可以节省很多空间。

标记整理算法。在标记清除算法的基础上,增加了整理。在回收内存空间后,会将所有存活的对象往左方空闲空间移动。这样解决了内存碎片问题,但是也需要更多的时间成本。

分代回收算法。实际上,JVM虚拟机采用分代回收算法,结合了上述几种算法的优势。将堆内存空间分为新生代、老年代。对于新生代的对象,因为会有大量的对象死去,少量的对象存活,使用复制算法。对于老年代对象,其生命周期较长,没有额外的担保空间,因此采用标记整理或者标记清理算法对其进行回收。

6.JRE、JDK、JVM 及 JIT 之间有什么不同?

JVM代表java虚拟机,它的责任是运行java应用。Java之所以能够实现跨平台,就是因为不同的平台有对应平台的JVM虚拟机。在java源代码被javac编译为class文件以后,由对应平台的jvm虚拟机将class文件转换为机器码后执行。

JRE是java运行时环境,是Java程序运行所必须的,它包含了JVM和java的核心类库。

JDK代表java开发工具包,它包含jdk,java的程序编译器等。

JIT是即时编译器,JVM在将字节码转换为机器码时,就需要利用JIT或者转译器。JIT相比转译器,编译后的字节码会成为优化后的原生指令码,更高效。但是也因此需要一定的成本。因此,对于极少执行到的代码,使用转译器更划算。对于重复或多次执行的代码,使用JIT更加高效。

7.谈谈JVM的常量池

JVM的常量池包括运行时常量池、基本类型包装类常量池、Class文件常量池、全局字符串常量池。

一个java程序被javac编译成class文件,而java中的字节码需要数据支持,这些数据可能较大,不宜直接存在class文件中,因此可以放在Class文件常量池中。 class文件常量池主要存放两大常量:字面量和符号引用(类名、方法名等等)。

基本数据类型的包装类大部分实现了常量池技术,这些类时Byte、Short、Integer、Character、Long、Boolean,即除了浮点类型以外。另外,五种整形包装类只有当值小于127时,才会使用对象池技术。

字符串常量池,字符串的分配,耗费高昂的时间、空间成本,而且其使用十分高频,因此极其影响性能。JVM为了提高性能,使用字符串池进行优化提升。具体做法是,开辟一块空间作为字符串常量池,创建字符串时,先判断串池中是否存在该字符串,存在则返回引用实例,不存在则创建并存到串池中。它其实就是虚拟机所维护的一个字符串引用表。在HotSpot VM中,它就是一个String table,其底层实现就是一个hashtable。

运行时常量池,运行时常量池,它就是class文件的常量池来构建的,它的作用是存储java class文件常量池中的符号信息,运行时常量池相对于class常量池一大特征就是具有动态性,当我们使用API,string.intern()就会把在运行时通过代码生成常量放入运行时常量池。

推荐阅读:这一次,彻底弄懂java中的常量池 - 掘金 (juejin.cn)

8.如何判断一个对象是否存活

判断一个对象是否存活,分为两种方法:引用计数法和根可达计数法。

引用计数法的原理就是:对象每被引用一次,就计数加1,引用失效,则计数减1。当引用计数为0时,就说明该对象可以被回收。当时这种算法无法解决循环依赖问题。

根可达计数法是指,从GCROOT开始,从上往下进行搜索,当一个对象没有与GCROOT连接的路径时,说明该对象不可用。

可以作为GCROOT的对象有:本地方法区中类的静态属性、方法区中常量池常量所引用的对象、本地方法栈JNI所引用的对象,虚拟机栈中所引用的对象

但当一个对象满足了上述条件,并不会马上就被回收,还需要执行两次标记。第一次标记是确定其是否有finalize()方法,并且没有被执行过,将其标记为垃圾对象,等待回收。

第二次标记是创建一个F-Queue,并生成一个finalize线程去执行finalize方法。但是虚拟机并不保证上述过程可以顺利进行,因为可能出现线程死锁或者执行缓慢,导致回收线程崩溃。

如果执行完finalize()方法,该对象依旧没有与GCROOT有直接或者间接的连接,才会执行垃圾回收。


9.请详细说下CMS垃圾回收器回收的过程。

CMS(Concurrent Mark Sweep)垃圾回收器使用的是标记清除算法,他的目标是最快的对用户进行响应。在进行垃圾回收时使用户线程和GC线程并发执行。

CMS的垃圾回收过程是:

(1)初始标记:标记和GCRoot相关联的下级,这个过程会stw,但是时间很短,因为跟GCRoot直接相连的下级很少。

(2)并发标记:在初始标记的基础下,进一步顺着GCRoot这个链路一路向下标记,这个过程耗时很长,但是不会有stw,因为是并发执行的。

(3)重新标记:因为在并发标记过程中,并没有阻塞其他工作线程,因此还可能产生新的垃圾,因此再进行一次标记。

(4)并发清除:清除删掉已经死亡的对象,这个过程也是并发进行的。

CMS垃圾回收器存在的问题是:

(1)内存碎片问题。可能导致大对象没有空间存储,提前进行FullGC。可以通过设置参数,使FullGC之前先进行内存碎片的整理。但是这个整理工作是无法并发执行的。

(2)并发回收导致cpu资源紧张。由于占用了工作线程,会使应用程序变慢,降低程序的总吞吐量。

(3)无法清理浮动垃圾。并发标记、并发清理阶段用户线程依旧工作,还会产生新的垃圾。只能等待下一次CMS垃圾回收时进行回收。

(4)并发失败。由于再垃圾回收阶段,用户线程还会继续工作,需要为其空出一定内存,如果留的空间不够其工作,就会并发失败。虚拟机不得不启动备用方案:退化成Serial OLd,这样会有stw出现,停顿很长一段时间。

10、请详细说下G1垃圾回收器进行垃圾回收的过程。

G1垃圾回收器的全程是Garbage First,它的思想是面向局部收集。在整体上,他采用的算法是标记-整理算法,在局部上,他采用的算法是标记-复制算法。他被设计来替代CMS垃圾回收器,在jdk9以后,他成为了JVM的默认垃圾回收器。

它的回收阶段分为:

(1)初始标记(会STW):在MinorGC时,并发进行初始标记。仅仅标记GCRoot能够直接访问到的对象。并且修改TAMS(Top at Mark Start)指针,使下一阶段用户线程进行并发标记时,能够正确的在可用的region分配对象。

(2)并发标记:从GCRoot开始扫描整个堆中的对象图,找出需要回收的对象。耗时较长。

(3)最终标记(会STW):对用户线程进行短暂的停顿,处理并发标记过程中引用发生变化的对象。

(4)清理阶段(会STW):更新整个region的统计数据,进行回收价值与成本的估计进行排序,根据用户的期望时间进行region回收。由于设计到存活对象的移动,必须进行stw。

推荐阅读:24-一步一图带你理清G1垃圾回收流程 - 知乎 (zhihu.com)

11、说一下JVM进行垃圾回收的完整过程

分为新生代和老年代,新生代默认占空间的1/3,老年代默认占内存空间的2/3。新生代分为Eden区、To survival、From survival,其默认占比是8:1:1。当Eden区内存不足时,会出发Minor GC。Minor GC的发生非常频繁,其速度也较快。

对应的,发生在老年代的GC被称为Major GC。一次Major GC通常伴随着至少一次Minor GC。


下面说说Minor GC的过程。

(1)在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;

(2)Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;

移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代。

(3)Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。

(4)Survivor 区内存不足会发生担保分配,超过指定大小的对象可以直接进入老年代。

(5)大对象将直接进入老年代。为了避免为大对象分配内存时由于分配担保机制(就是当在新生代无法分配内存的时候,把新生代的对象转移到老生代,然后把新对象放入腾空的新生代。)带来的复制而降低效率。

老年代回收(FullGC)触发条件:

(1)无法放入Survivor区直接进入老年代。YoungGC时,如果eden区+ from survivor 区存活的对象无法放到 to survivor 区了,这个时候会直接将部分对象放入到老年代。

(2)调用System.gc时,系统建议执行Full GC,但是不必然执行。

(3)老年代空间不足。

(4)方法区空间不足。


12、什么是类加载,它的过程是怎样的?双亲委派模型又是什么?

在java中,每一个类或者接口都会被编译成为一个.class文件。类加载就是把这些.class文件中的二进制数据读到内存中,进行校验、解析和初始化。


类的加载分为几个阶段:加载、连接(验证、准备、解析)、初始化

类加载的第一步是加载,这个过程主要做三件事:

(1)根据类的全限定名找到对应的.class文件。

(2)把.class文件中的二进制数据读取出来,转成方法区的运行时数据结构

(3)在堆中生成一个对应的java.Lang.Class对象,作为方法区数据的访问入口。

验证阶段主要是做一些文件格式、字节码验证、符号引用验证等,保证文件中的内容符合虚拟机的规范,防止数据危害虚拟机自身的安全。

准备阶段对类的静态信息(static 修饰过的变量)分配内存空间,并赋初始值。

解析阶段会把当前加载的类和它引用的类进行正式的连接。

初始化的过程,就是执行类构造器 ()方法的过程

虚拟机设计团队把类加载的过程放到了JVM外部去实现,以便让应用程序决定如何获取所需的类。JVM提供了三种类加载器,启动类加载器(BootStrap ClassLoader,负责加载<JAVA_HOME>\lib 目录,或者被 -Xbootclasspath 参数指定的路径)、扩展类加载器(Extension ClassLoader,负责加载Java平台中扩展功能的一些jar包)、应用类加载器(Application ClassLoader,我们自己开发的应用程序,就是由它进行加载的).当然用户也可以通过继承java.Lang.ClassLoader类实现自定义的类加载器。


双亲委派模式其实一句话就可以说清楚:任何一个类加载器在接到一个类的加载请求时,都会先让其父类进行加载,只有父类无法加载(或者没有父类)的情况下,才尝试自己加载。

双亲委派模式优势:避免重复加载 + 避免核心类篡改

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载。如果不是采用双亲委派机制,不同的类加载器加载一个类,这个类会被加载两次。而使用双亲委派机制,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

相关文章
|
消息中间件 Java 关系型数据库
膜拜!阿里自爆十万字Java面试手抄本,脉脉一周狂转50w/次
新冠疫情已经是第三个年头了,虽然国内防控做得非常好,但是他对职场的影响还在,一个月后即将又迎来一次大考。近两年企业越来越不好做,导致面试时对程序员的要求越来越高,越来越挑剔;
|
消息中间件 负载均衡 算法
【百面成神】多线程基础16问,你能坚持到第几问
【百面成神】多线程基础16问,你能坚持到第几问
|
存储 Java 编译器
【java筑基】吃透泛型(一万字长文,建议收藏)
【java筑基】吃透泛型(一万字长文,建议收藏)
|
存储 算法 编译器
【C语言深度剖析】— 史上最全关键字(爆肝半个月、数万字详解、考试必备)(2)
【C语言深度剖析】— 史上最全关键字(爆肝半个月、数万字详解、考试必备)(2)
134 0
【C语言深度剖析】— 史上最全关键字(爆肝半个月、数万字详解、考试必备)(2)
|
存储 缓存 安全
【C语言深度剖析】— 史上最全关键字(爆肝半个月、数万字详解、考试必备)(1)
【C语言深度剖析】— 史上最全关键字(爆肝半个月、数万字详解、考试必备)(1)
180 0
【C语言深度剖析】— 史上最全关键字(爆肝半个月、数万字详解、考试必备)(1)
|
存储 网络协议 编译器
【C语言深度剖析】— 史上最全关键字(爆肝半个月、数万字详解、考试必备)(3)
【C语言深度剖析】— 史上最全关键字(爆肝半个月、数万字详解、考试必备)(3)
【C语言深度剖析】— 史上最全关键字(爆肝半个月、数万字详解、考试必备)(3)
|
存储 缓存 Java
阿里巴巴面试题- - -JVM篇(二十)
阿里巴巴面试题- - -JVM篇(二十)
|
存储 Java 数据安全/隐私保护
《我要进大厂》- Java基础夺命连环10问,你能坚持到第几问?(面向对象基础篇)
《我要进大厂》- Java基础夺命连环10问,你能坚持到第几问?(面向对象基础篇)
《我要进大厂》- Java基础夺命连环10问,你能坚持到第几问?(面向对象基础篇)
|
存储 缓存 Java
《我要进大厂》- Java基础夺命连环18问,你能坚持到第几问?(基础概念 | 基本语法 | 基本数据类型)(三)
《我要进大厂》- Java基础夺命连环18问,你能坚持到第几问?(基础概念 | 基本语法 | 基本数据类型)
《我要进大厂》- Java基础夺命连环18问,你能坚持到第几问?(基础概念 | 基本语法 | 基本数据类型)(三)
|
IDE Java 编译器
《我要进大厂》- Java基础夺命连环18问,你能坚持到第几问?(基础概念 | 基本语法 | 基本数据类型)(二)
《我要进大厂》- Java基础夺命连环18问,你能坚持到第几问?(基础概念 | 基本语法 | 基本数据类型)
《我要进大厂》- Java基础夺命连环18问,你能坚持到第几问?(基础概念 | 基本语法 | 基本数据类型)(二)