jvm面试题及答案,深入剖析底层新生代老年代GC过程

本文从面试题入手,从面试者和面试官两个角度进行深入分析面试题的关键点,并且根据面试官自己的经验给出完整的思路过程和答案。涉及的内容有:ParNew新生代GC、ConcMarkSweep(即CMS)老年代GC、三色标记、JVM调优话题等。

[TOC]

面试题剖析:GC中对象什么时候会从新生代进入老年代

面试题:假设我们使用普通的垃圾回收器,新生代用ParNew、老年代使用ConcMarkSweep(即CMS),发生GC的时候,成为垃圾的对象什么时候会从新生代转移到老年代?

面试题常规回答

有的面试者可能会回答:新对象的产生是先在新生代的Eden区分配内存,当Eden区的内存容量满了之后,进行Young GC的时候,ParNew垃圾回收器会采用复制清理算法将Eden剩下的存活对象全部复制到其中一个Survivor区,假设为Survivor1区,然后把Eden区清空掉,供新对象分配内存。接下来,Eden区再次满了之后,ParNew会再次将Eden区和Survivor1区中存活的对象全部复制到Survivor2区中,然后再清理。如此循环往复地将存活对象在Survivor1区和Survivor2区来回转移,等到对象的年龄超过了15次(每次垃圾回收都是增加1次年龄),再将存活对象放入老年代。

另外一种情况就是大对象超过了指定阈值(默认1M),直接放入老年代。

上面两种情况是经常被提到的回答,当实际上还有两种情况是我在面试过程中几乎没听过的,接下来我们分析一下剩下的两种情况:

Young GC后,存活对象太多也进入老年代

如果ParNew进行Young GC后,发现存活的对象都超过了Survivor区的大小,这时候会直接将全部存活对象转移到老年代。

这里涉及到一个JVM的调优,如果经常出现每次在Young GC后都把大量的存活对象转移到老年代,那么老年代也会很快就进行Old GC,这个过程将会很漫长,还会发生Stop The World,用户看起来就是系统老是一卡一卡的,体验特别差。

这时候就需要考虑一下加大Survivor区的大小了,涉及的参数如下:

-Xms4096M -Xmx4096M -Xmn1024M -XX:SurvivorRatio=8

解释一下参数的意义:

-Xms4096M -Xmx4096M 这两个就是JVM整个堆的最小和最大内存容量4G,一般两个值都设为一样。

-Xmn1024M 表示新生代可用的内存容量,包括Eden+2个Survivor区。

-XX:SurvivorRatio=8 表示新生代中Eden区的比例,这里表示Eden区占80%的内存容量,剩下2个Survivor区各占10%。

动态年龄判断规则:Survivor区中非本次GC的对象都超过了容量的一半,全部进入老年代

还有一种比较少听到的情况,就是当这次Young GC中,发现当前Survivor区中,其他剩余的存活对象的内存大小已经超过了Survivor区容量的一半时,ParNew会直接将这些对象全部转移到了老年代的堆里面。这是因为垃圾回收器认为,如果对象经常需要存活,那么就应该直接进入老年代中,作为常用对象被使用。

举个例子,假设Survivor区中,年龄3及以上的对象占30%,年龄2的对象占20%,那么这时候就会将年龄2+年龄3及以上的对象全部放入老年代,年龄1会继续转移到另一个Survivor区。

再举另外一个例子,年龄4及以上的对象占40%,年龄3的对象占10%,那么年龄3+年龄4及以上的对象全部放入老年代,年龄2及以下对象会继续转移到另一个Survivor区。

其实这最后一种情况才是最经常发生的,新生代对象转移到老年代。


面试题剖析:如何实现只做YoungGC不做OldGC

面试题:在一个相对简单的定时任务系统,能否进行JVM参数调优,使其只发生新生代的YoungGC,但几乎不会进行老年代的OldGC?

面试题常规回答

有的面试者会回答:加大新生代的Eden区和Survivor区的内存,让新生代发生GC的时候,Survivor区的容量能放得下存活对象就行。

其实上面的回答思路是对的,只是很多地方考虑不够周到。能回答到这个情况的也是比较少的面试者,很多人基本都不太清楚。而且很多问到了JVM调优的时候,都是回答按照一定比例设置新生代老年代堆大小,加大内存设置等,基本都没有回答到本质的内容。

下面我们先回顾一下新生代的GC过程:

新生代的YoungGC过程

1. 新对象的产生是先在新生代的Eden区分配内存,当Eden区的内存容量满了之后,就会开始进行Young GC。

2. ParNew垃圾回收器会采用复制清理算法将Eden剩下的存活对象全部复制到其中一个Survivor区,假设为Survivor1区,然后把Eden区清空掉,供新对象分配内存。

3. 新对象继续在Eden区分配内存,等Eden区再次满了之后,ParNew会再次将Eden区和Survivor1区中存活的对象全部复制到Survivor2区中,然后再清理。如此循环往复地将存活对象在Survivor1区和Survivor2区来回转移。

4. 等到对象的年龄超过了15次(每次垃圾回收都是增加1次年龄),再将存活对象放入老年代。另外一种情况就是大对象超过了指定阈值(默认1M),直接放入老年代。另外还有2种情况对象会进入老年代,具体请阅读上一篇文章。

如何只做YoungGC

首先我们需要分析一下每次YoungGC后存活的对象大小,可以借助一些JVM监控的工具,像阿里开源的arthas甚至是jstat命令都可以,知道了存活对象大小,还需要看看YoungGC的频率,GC频率可以反映系统的并发量多高。然后就是看一下是否会产生大对象,这个可以观察一下堆内存的增长情况,如果没经过YoungGC老年代区域内存还是在增长那就是出现了大对象。

下面我们一步步来实现如何只做YoungGC:

1. 根据上面得到的YoungGC后存活的对象大小,假设是100M,那么Survivor区大小应该是2倍(防止动态年龄回收),也就是200M,Eden区的比例可以适当降一些,改为60%。设置参数如下:

-Xms4096M -Xmx4096M -Xmn1024M -XX:SurvivorRatio=6

-Xms4096M -Xmx4096M 这两个就是JVM整个堆的最小和最大内存容量4G,一般两个值都设为一样。

-Xmn1024M 表示新生代可用的内存容量,包括Eden+2个Survivor区。

-XX:SurvivorRatio=6 表示新生代中Eden区的比例,这里表示Eden区占60%的内存容量,剩下2个Survivor区各占20%。也就是Eden区差不多600M,每个Survivor区差不多200M。

2. 如果产生了大对象,那么上面的计算基础是YoungGC后存活的对象大小加上,大对象在YoungGC时间频率内的大小,这个值就是Survivor区的一半大小。

3. 再补充一下gc日志设置:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log。这样可以通过日志查看每次GC是从多大内存降低为多大内存,也就是剩下多少存活对象。其实也可以从JVM监控界面查看到。

4. 通过观察运行的GC情况,继续调整上面的参数,最后确保一点就是:YoungGC后存活的对象大小要小于Survivor区的一半。


面试题剖析:讲一讲类加载机制,什么情况下需要破坏双亲委派机制?

面试题:讲一讲Java类加载机制,双亲委派机制的作用是什么,什么情况下需要破坏?

面试题常规回答

JVM在需要使用到类的时候,就先将“.class”字节码文件加载到内存,然后验证文件头是否正确,再解析代码,将成员变量全部赋值为默认值,最后就是初始化阶段,如果有static代码块则在这时候执行。

双亲委派机制就是防止重复加载类,热加载类的时候需要破坏。

面试题深入剖析

Java类的加载完整过程:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

加载:将“.class”字节码文件加载到内存。

验证:校验加载的class文件是否符合规范。

准备:给静态变量static分配内存空间和初始值。比如int设置为0,object设置为null。

解析:将符号引用替换为直接引用,过程比较复杂,大概就是关联到JVM的具体内存偏移量,便于直接操作内存。

初始化:这个阶段最重要,先举个例子:

public class UsersModel implements CommonDTO {

    private Integer id = 1;

    private String name = "张三";
}

这里面 id = 1和name = "张三" 就是在这一阶段进行赋值的。当然如果有父类,是先执行父类的初始化。

使用和卸载阶段知道就好了,不用深究。

上面的过程都是依靠类加载器来实现的,不同的类加载器加载的同一个class文件的话,equals判断结果也是false的。

那么双亲委派机制是什么呢?

双亲委派机制就是加载类的时候是先层层上报:

应用程序类加载器(上一级能加载吗?) -> 扩展类加载器(上一级能加载吗?)-> 启动类加载器(我能加载吗?)

如果有一级能加载的就直接加载了,并返回类对象。如果不能则层层下传:

启动类加载器(我不能加载,你自己试试)-> 扩展类加载器(我不能加载,你自己试试)-> 应用程序类加载器(我自己试试)。

一般除了Java内置提供的类,开发人员自己创建的类都是 应用程序类加载器 加载的。

这就引发了另一个问题,我能做到发布在生产环境的class文件能够根据需要替换为最新的吗?也即是

怎么做到热部署?

答案是自己实现一个自定义的类加载器,然后在生产环境替换好class文件之后,触发一下重新加载类就能做到热部署。

Tomcat内部其实就是这样做的,自己实现了一套类加载器,可以监听JSP文件的变化重新加载最新的JSP文件。

后面我们也会分享JVM-Sandbox是如何做到更牛逼的字节码插桩,更方便地实现替换类方法。


面试题剖析:讲一讲FullGC的过程,三色标记是什么?

面试题:讲一讲在使用CMS垃圾回收器的情况下,FullGC也即是老年代GC的过程,存活对象的标记算法是什么,什么时候会出现漏标的情况?

面试题常规回答

面试者一般都是回答:老年代GC是使用了标记整理算法,先从GCRoot出发将存活的对象标记,然后并发清理所有的垃圾对象,最后还会进行内存碎片整理。

三色标记的只知道是白色、灰色和黑色,是一种并发标记算法。

面试题深入剖析

...

完整的文章请在底部的链接下载。


面试题剖析:如何减少“Stop the World”的影响?

面试题:如何在FullGC中减少系统停顿时间,也就是如何进行JVM调优了。

面试题常规回答

根据业务系统的并发量和数据量调整一下堆内存大小,把内存调大就行。或者如果是大内存机器(超过16G)可以使用G1垃圾回收器,设置好垃圾回收的预期停顿时间就行。

然后另一块就是对系统代码进行优化,减少大对象、递归、及时设置为无效对象等。

面试题深入剖析

完整的文章请在这里下载:

链接:
https://xbp.pub/i/Vda80c6b90ouH

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容