1 确定当前的垃圾选择器
执行: java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=1052925824 -XX:MaxHeapSize=16846813184 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
-XX:+UseParallelGC
发现:-XX:+UseParallelGC,这代表什么含义呢?参考下方:
- -XX:+UseSerialGC:Serial收集器串行回收+Serial Old收集器串行回收
- -XX:+UseParNewGC:ParNew收集器并行回收+Serial Old收集器串行回收
- -XX:+UseParallelGC:Parallel收集器并行回收+Serial Old收集器串行回收
- -XX:+UseParallelOldGC:Parallel收集器并行回收+Parallel Old收集器并行回收
- -XX:+UseConcMarkSweepGC:Serial收集器串行回收+CMS收集器并发回收(备用Serial Old收集器)
- -XX:+UseConcMarkSweepGC -XX:-UseParNewGC:ParNew收集器并行回收+CMS收集器并发回收(备用Serial Old收集器)
- -XX:+UseG1GC:G1收集器并发、并行执行内存回收
当前垃圾收集器为:
年轻代:Parallel收集器-复制算法、多线程、可控吞吐量
老年代:Serial Old 收集器-标记整理、单线程、GC期间StopTheWorld
PS:java -XX:+PrintFlagsFinal -version | grep : 也可以查询JVM相关参数
2 调优思路&参数&方案
调优目的:低停顿;少FGC;高吞吐;
调优思路:
- 新生代大小适中,偏大会导致老年代过小,偏小会导致MinorGC过多,对象年龄涨的快,且没有足够survivor空间,这样对象还是容易跑到老年代,尽可能的让周期性对象在新生代收掉,比如某些对象只存活几分钟如Session,这些对象没必要留到老年代。
- survivor空间同理不能过大过小,过大会浪费内存空间,同时减小了eden和old空间,过小会导致对象被迫提前进入老年代
- 新生代存活周期MaxTenuringThreshold默认15,如果觉得15太长,可以缩短,让注定老年对象提前进入老年代
可调整参数:
- SurvivorRatio:新生代中Eden 区域与Survivor 区域的容量比值, 默认为8, 代表 Eden :Survivor=8∶1
- PretenureSizeThreshold:直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
- MaxTenuringThreshold:晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC 之后,年龄就加1,当超过这个参数值时就进入老年代,最大15(对象头只分配了4位最大1111)
- UseAdaptiveSizePolicy:动态调整Java 堆中各个区域的大小以及进入老年代的年龄
- HandlePromotionFailure:是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden 和Survivor 区的所有对象都存活的极端情况
- ParallelGCThreads:设置并行GC 时进行内存回收的线程数
- GCTimeRatio:GC 时间占总时间的比率,默认值为99,即允许1% 的GC 时间。仅在使用Parallel Scavenge 收集器时生效
- MaxGCPauseMillis:设置GC 的最大停顿时间。仅在使用Parallel Scavenge 收集器时生效
- CMSInitiatingOccupancyFraction:设置CMS 收集器在老年代空间被使用多少后触发垃圾收集。默认值为68%
- UseCMSCompactAtFullCollection:设置CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理,默认true
- CMSFullGCsBeforeCompaction:设置CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理,默认0
- TargetSurvivorRatio:默认50,即survivor区对象目标使用率为50%。
调优方案:
- 能YGC解决的,不要留到FGC阶段,年轻代内存一般比老年代内存小,且对象存活率低,复制算法较快
- -Xms和-Xmx设置同样大小值,避免内存调整
- -Xmn和SurvivorRatio,调整Eden和Survivor的大小,合适即可。通过工具多分析对象存活率和存活大小。
- GC收集器调整,现在多核CPU时代,尽量使用并行垃圾收集器。jre8 server模式默认Paralled Scavenge+ParOld,可以调整为ParNew+CMS,降低停顿时间,如果效果还不尽人意,可以试试G1.
3 具体案例
3.1 案例一(懵懂的初次尝试)
某应用程序最初给了2G内存,发现CPU占用特别高,响应很慢,发现大量CPU被13条GC task thread#1 (ParallelGC)线程占用,第一时间先把内存加到16G,然后进行GC分析。
jstat -gc 9801 5000 1000:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
188416.0 188928.0 0.0 182850.1 5214208.0 3947333.3 11185152.0 460441.4 51072.0 50026.8 5760.0 5466.6 85 4.250 2 0.105 4.354
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
S0 +181967.2 S1 -182850.1 eden -4996146 old +5928 ygc +0.059 gc +0.06 约20秒一次GC,每5秒新增对象约1.25G
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
188416.0 188416.0 181967.2 0.0 5215232.0 218062.5 11185152.0 466369.4 51072.0 50026.8 5760.0 5466.6 86 4.309 2 0.105 4.414
最后发现:运行大半天有93次FGC,具体多久忘记了
- 调整一:扩大新生代大小为10G,survivor为1G,老年代6G,希望这样让对象尽可能在新生代销毁
结果:90秒一次FGC,每次3秒多,一天近1000次FGC,严重影响程序运行。优化变恶化!
缘由:发现每次老年代清理都会剩余5G+的内存,老年代一共6G,每次仅留1G不到的内存空间留给后续晋升的对象。
发现:发现Eden和Survivor空间大小是动态调整的,虽然设置了1GSurvivor,其实是由几百兆慢慢上下调整的,才想起当前用的是Parallel Scavenge,它会自己调整大小。
- 调整二:讲年轻代调整为8G,Survivor依旧1G
jstat -gc 1040 500 36000:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
455168.0 388608.0 0.0 388166.9 7502336.0 1836683.8 8388608.0 7810455.3 55552.0 54355.9 6144.0 5889.3 4145 488.767 90 321.415 810.182
......
333312.0 331776.0 0.0 298904.3 7719424.0 1994820.6 8388608.0 6904759.1 55552.0 54387.8 6144.0 5886.8 5565 682.398 128 452.292 1134.691
结果:470s一次FGC,12s一次YGC,这样一天大约会发生180次FGC,还是不行,5小时总GC时间1134秒
- 调整三:改用ParNew+CMS
jstat -gc 20090 500 36000:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
1048576.0 1048576.0 0.0 0.0 6291456.0 2203164.6 8388608.0 13928.5 29928.0 29486.9 3656.0 3518.2 0 0.000 4 0.538 0.538
......
1048576.0 1048576.0 0.0 470621.9 6291456.0 5604772.2 8388608.0 7450988.7 51784.0 50569.2 5704.0 5461.4 1538 709.527 28 93.194 802.721
结果: 750s一次FGC,11秒一次YGC,一天大约110次FGC,感觉还行,5小时总GC时间802秒
- 调整四:改用G1
jstat -gc 20406 500 36000:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
0.0 49152.0 0.0 49152.0 8765440.0 4620288.0 7962624.0 0.0 36300.0 35607.3 4300.0 4084.3 2 0.062 0 0.000 0.062
0.0 376832.0 0.0 376832.0 8437760.0 6119424.0 7962624.0 6904291.3 51532.0 50198.1 5708.0 5449.1 953 240.178 2 29.480 269.658
结果:10298s/171min后才出现第一次FGC,惊喜不,可惜看看FGC时间24秒,STW时间:23秒!吓死......过了450秒,第二次FGC,耗时5.5秒,第二次FGC之后过了2个多小时都没出现第三次FGC。这么看G1的行为不可琢磨。5小时总GC时间269秒。
发现:Survivor空间只有一个,因为是分区间拷贝,不需要两个Survivor。而且Eden和Survivor会不停的调整容量大小。Survivor用了多少就是它的容量,两个值相等。
- 调整五:-XX:MaxGCPauseMillis=200,设置期望达到的最大GC停顿时间200ms(JVM会尽力实现,但不保证达到)
结果:20小时,5次FGC,平均每次15秒,频率很低,但是确实GC时间太长,没有达到预期(原理所示)的好。
总结:调整期间要注意几个数据:1、年轻代新增对象速率2、老年代常驻对象大小3、新生代对象消亡速率;然后按需调整各个年代内存大小。现象上看,总GC时间:Parallel Scavenge+ParOld > CMS+ParNew > G1。所以吞吐量正好相反。但是G1的FGC时间不稳定,可能出现单次较长时间的GC。
爱家人,爱生活,爱设计,爱编程,拥抱精彩人生!