jvm常见的面试问题汇总(持续更新中…)
章节1.jvm内存结构部分
面试题1:jvm内存分区以及各个区的作用和功能
回答:jvm内存分区分为五个部分:堆区,java虚拟机栈区,本地方法栈,方法区以及程序计数器;
内存结构图如下所示:
其中:
程序计数器: 用来存储指向下一条指令的地址,也就是即将执行的指令代码的,并由执行引擎来读取下一条指令
虚拟机栈: 主管java程序的运行,它保存的是方法的局部变量,部分结果,并参与方法的调用和返回。可以这样说,栈解决的是程序的运行问题,即程序如何运行,或者说如何处理数据。
本地方法栈: 用来管理本地方法的调用,当某一个线程调用一个本地方法时(别的语言实现的),会进入本地方法栈中处理本地方法的数据,进行本地方法的调用和返回。每个线程创建时,就会创建一个java虚拟机栈,其内部由一个个的栈帧组成,对应着一次次方法的调用。
堆空间: 堆解决的是数据存储的问题,即数据放在哪里,怎么存放。java堆区在jvm启动时即被创建,是java内存管理的核心区域,几乎所有的对象实例以及数组都存放在堆空间中。
方法区: 方法区用来存储已经被虚拟机加载的类型信息,常量,静态变量,及时编译器(JIT)编译后的代码缓存,域信息,方法信息等(针对JDK1.8及以后版本来说)
面试题2:为什么要有新生代(年轻代)和老年代?
回答:经研究表明,不同对象的生命周期是不相同的,但是70%~99%的对象属于临时对象,将堆区分为新生代和老年代的目的在于优化GC(垃圾回收)的性能,如果不进行分代操作,那么进行GC是会对堆区的所有区域进扫描,而很多对象其实都是“朝生夕死”的,当每次GC都对所有堆区的对象进行扫描回收,将提高GC的时间和降低其效率。但是我们如果分为新生代和老年代,其中新生代存放生命周期较短的对象,即“朝生夕死”或者刚新创建的一些对象,老年代存放生命周期比较长的对象,这样一来进行GC的时候,会先把生命周期较短的临时变量进行回收,不用扫描整个堆区,而是有针对性的进行扫描回收,这样可以节省很多时间和提高回收的效率,大大地提高了jvm的性能。
面试题3:什么时候对象会进入老年代中?
回答:
第一种情况:当对象进入Eden区后经过MinorGC后仍然存活,并且被Survivor区容纳,在接下来的MinorGC中每次都能存活,age依次累加到老年代的阈值(默认为15)后,将其晋升至老年代。
第二种情况:当对象准备从Eden区进入Survivor区中时,发现Survivor无法存放该对象,则进行晋升老年代区
第三种情况:大对象直接分配进入老年代。
第四种情况:动态对象年龄判断:如果Survivor区中的相同年龄的对象的所有对象大于了Survivor空间的一半,年龄大于或者等该对象年龄的对象可以直接进入老年代,无需等到阈值后进入。
面试题4:jdk8中对jvm的内存结构进行了哪些调整?
JDK8由于Oracle收购了JRockit以后,将HotSpot和JRockit进行了整合,移除了永久代(Permant Generation),引进了元空间(MetaSpace),以下是jdk7和jdk8的jvm内存结构图:
而对于jvm,主要的变动就在于方法区内存的改变,此处列出JDK6及之前版本,JDK7和JDK8三种不同版本的改变:
图示如下所示:
JDK6及之前的版本的方法区:
JDK7版本的方法区:
JDK8的方法区:
面试题5:jvm的永久代、元空间会发生垃圾回收吗?
回答:永久代、元空间是会发生垃圾回收的!首先我们说一下在方法区中,java虚拟机规范中对方法区的约束是比较宽松的,提到可以不要求虚拟机在方法区进行垃圾回收。但是这部分区域的回收是很有必要的,其中方法区的垃圾回收主要包括两个方面:常量池中的废弃的常量和再使用的类型。
常量池中主要存放的是:字面量和符号的引用。其中字面量比较接近java语言层次的常量的概念,例如:文本字符串,被声明final的常量值等等,而符号的引用主要包括以下三类常量:
1.类和接口的全限定名
2.字段的名称和描述符
3.方法的名称和描述符
HotSpot虚拟机对于常量池中回收策略很明确:只要常量池中的常量没有被任何地方引用,就可以被回收!!
但是判断一个类型是否属于“不再被使用的类”的条件十分的苛刻!!
首先需要满足一下三个条件
1.该类的所有的实例都已经被回收,也就是java堆中不存在该类以及其任何派生子类的实例;
2.加载该类到的类加载器已经被回收
3.该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法;
但是,java虚拟机被允许的对满足上面满足上述三中情况的无用类进行回收,仅仅是允许而已。
面试题6:jdk8什么会将永久代改变成元空间呢?
回答:由于永久代存在着很多的弊端:
1.为永久代设置空间大小是很困难的;某些场景下,如果动态加载的类过多,容易产生Perm区的OOM。而元空间和永久代最大的区别在于:元空间并不在虚拟机中,而是使用的本地的内存,因此,默认情况下,元空间的大小仅仅受到本地内存的限制。
2.对永久代进行性能的调优是很困难的;如果采用永久代,则内存不足并不可少会触发Full GC,但这样会造成应用程序线程的停顿时间过长,对于性能是有很大的弊端的,而且对于Full GC,我们并不能进行多大程度的性能调优,而采用元空间,直接使用的就是本地的内存,则很大程度上的避免Full GC的发生。
面试题7: StringTable为什么要被调整?
章节2.jvm垃圾收集器部分
面试题1:什么内存需要进行GC?什么时候进行GC?如何进行GC?
回答:
什么内存需要进行GC?
在一个程序运行过程中,如果一个对象,没有任何指针指向它,则说明该对象成为一种垃圾,该对象所占的内存即为需要进行GC的内存。
什么时候进行GC?什么时候会触发垃圾回收?
一般来说,垃圾回收是针对堆区进行的,而堆区主要是针对新生代和老年代的垃圾的回收,当新生代的内存被填满时,会触发新生代(年轻代)垃圾回收器进行垃圾回收,而当新生代中持续存在的对象到达阈值年龄以后或者大对象新生代无法直接存下的时候,会直接存放到老年代中,而老年代的内存空间被填满后,会触发老年代垃圾回收器进行垃圾的回收。如果老年代回收器进行垃圾回收以后,内存依旧不足,则此时会触发FullGC,如果进行FullGC以后还是内存不足,此时就会报OutOfMemoryError(OOM)-----内存溢出异常。
如何进行GC?
面试题2:jvm的GC的相关算法有哪些?目前jdk版本采用什么算法?
回答:
jvm的GC的算法分为两个阶段:垃圾标记阶段和垃圾清除阶段
垃圾标记阶段的算法:主要有引用计数算法和可达性分析算法(或者称为根搜索算法,追踪性垃圾收集算法);
垃圾清除阶段的算法:主要有标记-清除算法,复制算法,标记-压缩(整理)算法;
其中目前jdk8中主要标记阶段使用的是可达性分析算法,而清除阶段,因为jdk8默认的垃圾回收器是ParalleGC,而其内部采用的是复制算法(进行新生代的垃圾回收),与其组合搭配使用的Serial Old GC和Parallel Old GC 采用标记-压缩算法。
面试题3:GC是什么?为什么会有GC?
回答:
GC:Garbage Collection(垃圾回收)
在我们的程序运行过程中,会产生许多的垃圾对象(即在一个程序运行过程中,如果一个对象,没有任何指针指向它,则说明该对象成为一种垃圾),如果不及时进对内存中的垃圾进行清理,那么垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的内存空间就无法被其他的对象所使用,甚至会导致内存的泄露。
面试题4:GC的两种判定方法是什么?
回答:
对象是否死亡的2种判定方法:引用计数和可达性分析(又称引用链),即所谓的两种垃圾的标记算法
1.引用计数
每个对象被创建的时候,jvm会为每一个对象的对象头保存一个引用计数引用,来记录该对象被引用的情况:当该对象被任意对象引用时,计数器+1;引用失效,计数器 -1;GC时会回收计数器为0的对象。注意:该方式无法解决对象互相循环引用的情况,java垃圾回收器没有采用判定方式
2.引用链(可达性分析)
程序把所有的引用看作图(类似树结构的图),选定一个对象作为GC Root根节点,从该节点开始寻找对应的引用节点并标记,找到这个节点之后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点认为是不可达的无用节点,会被回收。
该方式可以解决循环引用的问题;
GC Roots:指的是一组必须活跃的引用
通常情况下,包括以下几种:
1.虚拟机栈中被引用的对象;比如各个线程中使用到的参数,局部变量等等
2.本地方法栈中的JNI引用的对象
3.方法区中类静态属性引用的对象
4.方法区中字符串常量池中的对象
5.所有被同步锁synchronized持有的对象
6.java虚拟机的内部引用
总之,是处于堆外的活跃的对象引用
但是,注意,有的时候,当用户选择的 垃圾回收以及垃圾回收的内存区域不同的时候,我们还可以选择其他的对象作为“临时性GC Roots”加入,其中,我们说对新生代进行垃圾回收时,此时的老年代活跃的对象可以作为GC Roots集合的一份子。
面试题5:分别讲述一下CMS收集器和G1收集器
回答:
CMS收集器(Concurrent-Mark-Sweep) :是jdk1.5的时候提出的,它是HotSpot虚拟机中第一款真正意义上的并发垃圾收集器,它第一次实现了让垃圾收集线程和用户线程同时运行。CMS是一款老年代的收集器,与其新生代搭配使用的垃圾收集器是ParNewGC和Serial GC的一个。当CMS运行期间发生预留的内存无法满足程序的需要,此时会出现“Concurrent Mode Failure”失败,此时,虚拟机会启动后备方案,即启用serial Old GC重新进行老年代的垃圾回收。
版本变动:其中在JDK8版本中将CMS-Serial GC的组合声明为弃用状态,在JDK9中移除。在JDK14中,删除了CMS垃圾回收器。采用的是标记-清除算法。
CMS的垃圾收集过程可以分为四个主要的阶段:
步骤 | 具体内容 |
---|---|
初始标记阶段 | 在此阶段,程序的所有的工作线程会因为“Stop-The-World”机制而出现短暂的暂停(此时停顿第一次),这个阶段的主要任务仅仅只是标记出GC Roots能关联到的对象。一旦标记完成,就会恢复之前被暂停的所有的应用线程。由于直接关联的对象比较地小(因为此时在老年代中进行垃圾回收,能被GC Roots标记到的对象很少),所以这里的速度会很快!! |
并发标记阶段 | 从GC Roots的直接关联对象开始遍历整个对象图,这个过程耗时比较长,但是不需要停顿用户线程,可以与垃圾收集收集线程一起并发运行 |
重新标记阶段 | 由于在并发标记阶段中,程序的工作线程一一直在运行过程中 ,可能会由于和垃圾的收集线程交叉运行导致之前的一些标记会发生一些变动,故进行标记的修正,这个过程比初始标记略长(STW,此时停顿第二次),但比并发标记的时间要短很多 |
并发清除阶段 | 此阶段清理删除掉标记阶段已被判断为已经死亡的对象,并释放内存空间,由于不需要移动内存对象的位置,则此阶段也是可以和用户线程同时并发执行的 |
CMS GC的特点:
优点:
1.并发收集垃圾
2.低延迟性
缺点:
1.因为采用的是标记-清除算法,所以会产生内存的碎片;
2.CMS收集器对cpu资源非常地敏感;在并发阶段,它虽然不会导致用户的停顿,但是会占用一部分的线程而导致应用程序变慢,总的吞吐量会降低
3.CMS无法清除浮动垃圾;可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生,在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发阶段如何产生了一些新的垃圾对象,此时CMS将无法对这些垃圾进行标记,这些垃圾对象也被称为浮动垃圾,最终导致这些新产生的垃圾对象没有被及时的回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
G1收集器(Garbage First): JDK7中引入的垃圾回收器,是目前收集器技术发展的最前沿成果之一;
取名为G1收集器的原因:G1收集器将堆内存分割成很多不相关的区域(region),其物理上是不连续的,使用不同的region来表示Eden区,S0区,S1区老年代等等;G1收集器会跟踪region里面垃圾堆积的价值大小(根据回收所获得的空间大小以及回收所需的时间的经验值等),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。故由于这种方法侧重于回收垃圾的最大量的区间,所以我们取名为G1 GC。
优点:
1.并发和并行性:并行性表现在G1在回收期间,可以由多个GC线程同时工作,有效的利用多核计算能力,此时用户线程STW;并发性表现在G1拥有和应用程序交替执行的能力,部分工作能够和应用程序同时执行。
2.分代收集:它会区分老年代和新生代,年轻代依旧还是Eden区和Survivor区,但是它并不要整个Eden区,年轻代或者老年代都是连续的,也不坚持固定大小和固定的数量。将堆空间分为了若干个区域,这些区域中包含了逻辑上的年轻代和老年代。但是有一点,它和其他的垃圾收集器都不相同,即它同时可以兼顾年轻代和老年代的垃圾回收。
3.空间的整合:G1将内存划分为一个个的region,内存的回收都是以region为单位进行的,region之间使用的是复制算法,但是整体上使用的是标记-压缩算法,两种算法都是可以避免内存碎片的产生。这种特性有利于程序的长时间的运行,分为大对象时不会因为无法找到连续内存空间而提前触发下一次Full GC。尤其是当java的堆内存非常大的时候,G1的优势会非常地明显。
4.可预测的停顿时间模型:即软实时,G1除了追求低停顿以外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段中,消耗在垃圾回收的时间不超过N毫秒。
缺点:
1.垃圾收集时产生的内存占用相较于CMS而言,是要高于它的;
2.程序在运行时额外执行负载(OverLoad)也要高于CMS产生的OverLoad
G1收集的三个环节:
1.年轻代GC(YoungGC/MinorGC): jvm启动的时候,G1会先准备好Eden区,程序在运行过程中,会不断地产生对象到Eden区中,当Eden区的空间耗尽的时候 ,G1会启动一次年轻代垃圾回收过程(YoungGC/MinorGC):具体回收过程如下所示:
第一步 | G1会停止应用程序的执行(Stop The World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区的所有的内存分段 |
---|---|
第二步 | 扫描根:根指的就是GC Roots,根和Rset记录的外部引用作为扫描存活对象的入口 |
第三步 | 更新Rset:处理dirty card queue中的card,更新Rset,此阶段结束时,Rset可以准确的反应老年代对所在的内存分段中对象的引用 |
第四步 | 处理Rset:识别被老年代指向的Eden区的对象,这些被指向的Eden中的对象被认为是存活的对象 |
第五步 | 复制对象:遍历对象树,Eden区中内存段中存活的对象会被复制到Survivor区的空白的内存分段(底层即是复制算法),Survivor区中存活的对象如果年龄未达阈值,则年龄加1,达到阈值则被复制到Old区的空的内存分段,如果Survivor区中内存空间不足。则可以将Eden区中的部分数据直接晋升老年代空间 |
第六步 | 处理引用:处理soft,Weak,Phantom,final等引用,最终使得Eden区变空。GC停止工作,因为目标内存中在复制过程中是连续存储的,没有碎片产生,所以复制过程中,可以达到内存整理的效果 |
注意:上面提到Rset:Remembered Set解释一下其来源和含义
Remembered Set: 因为我们说堆空间被分割成了若干个region,但是每一个region并不是相互独立的,很多时候,一个region里的对象有可能被其他的region里的对象所引用,则我们在扫描过程中寻找存活的对象和已经死亡的对象时,是否需要扫描整个java堆呢??为了避免全局扫描整个堆区耗费大量的时间,此时引进Rset:即给每个Region分配对应的一个Rset,每次Reference类型数据写操作时,都会产生一个Write Barrier暂时的中断操作,然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region,如果不同,则将相关的引用信息记录到引用指向的对象的Region对应的Rset中,当进行垃圾收集的时候,在GC根节点的枚举范围加入Rset,就可以保证不进行局部扫描,也不会存在遗漏。
2.并发标记过程:
步骤 | 具体内容 |
---|---|
第一步 | 初始化标记扫描:标记从根节点可以直接可达的对象(STW) |
第二步 | 根区域扫描:G1GC扫描Survivor区可以直接可达的老年代的区域对象,并标记被引用的对象,这一过程必须在YoungGC之前完成 |
第三步 | 并发标记:在整个堆中进行并发标记(和应用程序并发执行),若发现区域对象中的所有对象都是垃圾,则这个区域会被立即回收,同时,标记过程中,会计算每个区域中的对象活性(即区域中存活对象的比例) |
第四步 | 再次标记:由于应用程序持续进行,需要进一步修正上一次的标记结果(STW),此阶段中G1采用比CMS更快的初始快照算法 |
第五步 | 独占清理:计算各个区域中存活的对象的存活情况和GC回收比例,并进行排序,识别可以混合回收的区域(STW) |
第六步 | 并发清理阶段:识别并清理完全空闲的区域 |
3.混合回收
当越来越多的对象晋升成老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法除了回收真个Young区,还会回收一部分的Old区。其中,混合回收的回收集包括1/8的老年代内存片段,Eden区片段,Survivor区内存片段。混合回收的算法和年轻代回收的算法完全一致,只是回收集多了老年代的内存片段。
4.Full GC
G1的初衷就是避免Full GC的出现,但是如果上述方式不能正常工作,则G1会停止应用程序的执行(Stop The World),使用单线程的内存回收算法进行垃圾回收,性能会比较差,应用停顿时间比较长;
面试题6:常用的垃圾回收器有哪些?各有什么优缺点
面试题7:分代回收讲解一下
回答:目前几乎所有的GC都是采用分代回收的,即Generational Collecting算法执行垃圾回收的。
jvm中使用的HotSpot基于分代的概念,GC所使用的的内存回收算法必须要结合年轻代和老年代各自的特点:
分区 | 分区的特点 |
---|---|
年轻代/新生代 | 区域相对于老年代较小,对象的生命周期较短,存活率低,回收频繁。这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活的对象大小有关,因此很适用于年轻代的垃圾回收。而复制算法内存利用率不高的问题,可以通过HotSpot中的两个Survivor区来进行缓解 |
老年代 | 老年代的区域很大,对象生命周期很长,存活率高,回收不及年轻代频繁。这种情况下存在大量存活率高的对象,复制算法就明显变得不合适。一般情况下我们采用标记-清除或者标记-整理的算法的混合实现 |
如此一来,我们针对堆空间的分代(新生代和老年代)分别进行垃圾的回收,可以大面积提高垃圾回收的效率,减少不必要的停顿时间(Stop-The-World)。
面试题8:常见的垃圾回收的算法有哪些?如何选择这些垃圾回收算法?各有什么优缺点?
常见的垃圾回收算法有标记-清除算法,复制算法,标记-压缩(整理)算法,下面列表比较一下这三种算法的优缺点
垃圾回收算法 | 工作流程 | 优点 | 缺点 |
---|---|---|---|
标记-清除算法(Mark-Sweep) | 当堆中的有效空间被耗尽时,会停顿整个程序(STW),进行标记(即从引用节点开始遍历,标记所有被引用的对象),然后对堆内存进行一次线性的遍历,对于对象头中没有标记可达的对象,进行回收。 | 简单直接 | 效率不高;进行GC的时候回暂停整个应用程序,用户的体验感不好;这种清理方式清理出来的内存空间不是连续的,产生内存碎片,则需要维护一个空闲列表。 |
复制算法(Copying) | 为了解决上面标记-清除算法产生的内存碎片应运而生,将活着的内存空间分为两块,每次只使用其中一块空间,在垃圾回收时,在垃圾回收时将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块中所有对象,交换两个内存的角色,最后完成垃圾回收。 | 没有标记和清除过程,实现简单,运行高效;复制过去以后可以保证空间的连续性,不会出现碎片问题 | 此算法需要使用内存的两倍的空间,对内存空间的需求量比较大;对于G1这种分区的GC,复制而不是移动,意味着GC需要维护region之间的对象的引用关系,不管是内存的占用还是时间开销方面。 |
标记-压缩算法(Mark-Compact) | 第一阶段和标记-清除算法一样,从根节点开始标记所有被引用的对象;然后第二阶段将所有存活的对象压缩到内存的一=端,按照顺序进行排放,之后再清理边界外的所有的空间。 | 消除了标记-清除算法中内存区域分散的缺点,我们需要给新对象分配内存时,jvm只需要持有一个内存的起始地址即可;消除了复制算法当中,内存减半的高额代价。 | 效率上来说低于复制算法;移动对象时,如果对象被其他对象引用,需要调整引用的地址;移动过程中,需要暂停用户的应用程序(STW) |
选择哪种垃圾算法,还是要针对不同的应用场景来说,当我们对新生代、年轻代进行垃圾回收时,可以优先考虑复制算法,因为新生代中大部分的对象都是生命周期很短的,朝生夕死的对象,使用复制算法可以提高回收的效率,同时由于存活的需要复制的对象很少,则大大地发挥了此算法的优势。而对于老年代这种生命周期较长的区域,我们则优先考虑标记-清除和标记-压缩(整理)的结合。