调优原因:生产服务器4核16g,框架SpringBoot,当压测1小时左右时,出现很多次Full GC,GCT总时间也很大。 jvm启动参数:
-Xms10G -Xmx10G -Xmn3g
jdk1.8默认GC
问题分析:
老年代7G,会发生多次FullGC,说明老年代的内存慢慢上涨,但是不是内存泄漏,因为如果是内存泄漏,FullGC完应该会OOM。所以就分析老年代内存逐渐跑满的原因:
一个对象从Eden区创建,到Survivor区域,逃过15次GC,最终才能到老年代,这种老年代的对象,在FullGC发生时,也基本不会被回收。但是压测过程中出现的现象时老年代满了之后,FullGC会回收很大比例的垃圾对象,说明很多对象没逃过15次GC,直接跑到老年代了,所以接下来分析:什么情况对象会 “直接” 跑到老年代?
1、JVM设置了参数(-XX:PretenureSizeThreshold=1000000),大对象直接进入老年代。
2、Minor GC时,Survivor区域太小。
3、老年代内存本身比较小,Minor GC时,有概率触发Full GC
在笔者这里,情况1、3可以排除。只剩下2。
猜测原因:
UseAdaptiveSizePolicy参数导致Survivor动态调整到过小,对象直接到老年代。
jdk8中默认的垃圾收集器,开启了UseAdaptiveSizePolicy, 会动态调整Survivor的大小,当Survivor区域小到不够存放Eden区域一次Minor GC后活下的对象时,对象就会直接进入老年代,压测时间久了,自然会发生很多次full GC,当老年代内存比较大时,FullGC卡顿时间也越久。所以直接关闭UseAdaptiveSizePolicy, 并且通过-XX:SurvivorRatio=N把Survivor区域调整到一定的大小(500M-1G, 越大的话浪费的内存也相对多一些,不过都10G了,也不在乎那一点内存了),Eden区域比例不可过大,否则minor GC耗时也会上去,也不可过小,太频繁的话,GC过程本身也有一定的耗时,积少成多,吞吐也自然会下降。Eden具体大小设置可根据jstat -gc $pid, 分析Eden大小和GC耗时做个对比。
为了验证这个猜测, 笔者本地环境重新压测对比了两轮(持续一小时20分),4核8G的内存,JVM启动参数和GCT耗时分别为:
关闭:UseAdaptiveSizePolicy
local-1、-XX:-UseAdaptiveSizePolicy -XX:SurvivorRatio=3 -Xms4G -Xmx4G -Xmn2G
GCT: 44秒, FullGC:0次, minor GC:2600次
YGC AVG: 0.017秒/次
开启:UseAdaptiveSizePolicy
local-2、 -Xms4G -Xmx4G -Xmn2G
GCT: 28秒,FullGC:4次,minor GC:1403次
YGC AVG: 401/14647 = 0.02秒/次
FGC AVG: 207/152 = 0.2秒/次
两次都压测了1个小时左右,结果却证明UseAdaptiveSizePolicy 效果更佳,原因其实不难看出:
当Survivor区域大小固定后,Eden区域可使用的内存也是固定的,而UseAdaptiveSizePolicy 可以动态缩小Survivor大小,虽然会有概率导致部分对象直接到老年代,但是变向使得Eden的可使用内存变大了,具体来说两点:
1、动态调整Survivor区域,可以让新生代内存中,Eden区域的比例尽可能大,减少minorGC次数
2、把一定比例的对象直接挪到老年代,等于Eden区可以动态的挪用部分老年代的内存作为 “新生代Survivor内存不足时的备用内存”。
明白以上两点时,可以看出UseAdaptiveSizePolicy 的最大问题是:被挪用的老年代内存太大。导致FullGC太久。
我们都知道新生代的GC要比老年代GC快很多,如果被挪用的老年代内存能尽可能小,那么同时新生代比例同时也会变大,那么会带来两个好处:
1、老年代GC时间缩短
2、新生代内存比例同时变大了,minor GC 次数少了。(新生代GC很快,内存加大一点,影响不明显)
结论:
所以最优的策略是:开启UseAdaptiveSizePolicy,同时压测时观察GC log,查看老年代Full GC后内存使用率判断老年代内存尽可能可以小到多少。 笔者这里观察老年代实际使用的内存不会超过300M(老年代实际需要存放的往往是经历15轮GC后仍存活的对象)
所以又来了一轮压测(持续1小时20分):
开启:UseAdaptiveSizePolicy
local-3、 -Xms4G -Xmx4G -Xmn3600M
GCT: 10.8秒
FGC AVG: 0.1秒/次
YGC AVG: 0.016 秒/次
实践证明:开启UseAdaptiveSizePolicy 时,老年代内存够用就行!!!
有了这个结论后,再看看生产环境的压测数据:
YGC | YGCT | FGC | FGCT | GCT |
14647次 | 401秒 | 152次 | 207秒 | 608秒 |
YGC AVG: 401/14647 = 0.02秒/次
FGC AVG: 207/152 = 1.36秒/次
很明显,问题就是老年代内存太多了!!!!!
jdk8下使用默认GC时,调优宗旨:
- 当可以预判老年代内存用量时:
老年代内存够用就行(尽可能小,但是又不会OOM)
- 当无法预判老年代内存用量时:
尽量从GC Log分析预判老年代内存实际用量。
备注:UseAdaptiveSizePolicy, 只会动态调整Eden、Survivor From、Survivor To的大小,而新生代和老年代的比例在JVM进程启动的时候就固定了,即在jvm进程销毁前,老年代的大小都不会再变化了。 (未手动指定比例如:-Xmn3g时,则由系统自动决定分配比例。系统决定的比例往往不是最优的!!!)
另外的调优方案:
- 笔者这边生产环境jvm堆内存总量已经10g了,所以笔者尝试换了jkd9的默认GC:G1垃圾收集器,G1不需要配置太多参数,设置堆内存即可,jdk8下开启如:
-Xms10G -Xmx10G -XX:+UseG1GC
G1不需要设置新生代/老年代比例、SurvivorRatio等,因为这些都会由G1自动的动态调整,对比发现G1在jvm大内存下表现更优,几乎不需要什么特殊的调优。