面试题:在一个相对简单的定时任务系统,能否进行JVM参数调优,使其只发生新生代的YoungGC,但几乎不会进行老年代的OldGC?
面试题常规回答
有的面试者会回答:加大新生代的Eden区和Survivor区的内存,让新生代发生GC的时候,Survivor区的容量能放得下存活对象就行。
其实上面的回答思路是对的,只是很多地方考虑不够周到。能回答到这个情况的也是比较少的面试者,很多人基本都不太清楚。而且很多问到了JVM调优的时候,都是回答按照一定比例设置新生代老年代堆大小,加大内存设置等,基本都没有回答到本质的内容。
下面我们先回顾一下新生代的GC过程:
新生代的YoungGC过程
- 新对象的产生是先在新生代的Eden区分配内存,当Eden区的内存容量满了之后,就会开始进行Young GC。
- ParNew垃圾回收器会采用复制清理算法将Eden剩下的存活对象全部复制到其中一个Survivor区,假设为Survivor1区,然后把Eden区清空掉,供新对象分配内存。
- 新对象继续在Eden区分配内存,等Eden区再次满了之后,ParNew会再次将Eden区和Survivor1区中存活的对象全部复制到Survivor2区中,然后再清理。如此循环往复地将存活对象在Survivor1区和Survivor2区来回转移。
- 等到对象的年龄超过了15次(每次垃圾回收都是增加1次年龄),再将存活对象放入老年代。另外一种情况就是大对象超过了指定阈值(默认1M),直接放入老年代。另外还有2种情况对象会进入老年代,具体请阅读上一篇文章面试题剖析:GC中对象什么时候会从新生代进入老年代。
如何只做YoungGC
首先我们需要分析一下每次YoungGC后存活的对象大小,可以借助一些JVM监控的工具,像阿里开源的arthas甚至是jstat命令都可以,知道了存活对象大小,还需要看看YoungGC的频率,GC频率可以反映系统的并发量多高。然后就是看一下是否会产生大对象,这个可以观察一下堆内存的增长情况,如果没经过YoungGC老年代区域内存还是在增长那就是出现了大对象。
下面我们一步步来实现如何只做YoungGC:
- 根据上面得到的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。- 如果产生了大对象,那么上面的计算基础是YoungGC后存活的对象大小加上,大对象在YoungGC时间频率内的大小,这个值就是Survivor区的一半大小。
- 再补充一下gc日志设置:
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
。这样可以通过日志查看每次GC是从多大内存降低为多大内存,也就是剩下多少存活对象。其实也可以从JVM监控界面查看到。 - 通过观察运行的GC情况,继续调整上面的参数,最后确保一点就是:YoungGC后存活的对象大小要小于Survivor区的一半。