1. 背景
当你遇到线上服务内存偏高,偶然激增,你需要去heap dump分析内存,结合GC日志分析内存的释放情况
2. GC日志分析
2.1 基本结构
2.2 日志分析
[GC (Allocation Failure) [ParNew: 367523K->1293K(410432K), 0.0023988 secs] 522739K->156516K(1322496K), 0.0025301 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]
GC:表明进行了一次垃圾回收,前面没有Full修饰,表明这是一次Minor GC ,注意它不表示只GC新生代,并且现有的不管是新生代还是老年代都会STW。
Allocation Failure:表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。
ParNew:表明本次GC发生在年轻代并且使用的是ParNew垃圾收集器。ParNew是一个Serial收集器的多线程版本,会使用多个CPU和线程完成垃圾收集工作(默认使用的线程数和CPU数相同,可以使用-XX:ParallelGCThreads参数限制)。该收集器采用复制算法回收内存,期间会停止其他工作线程,即Stop The World。
367523K->1293K(410432K):单位是KB, 三个参数分别为:GC前该内存区域(这里是年轻代)使用容量,GC后该内存区域使用容量,该内存区域总容量。
0.0023988 secs:该内存区域GC耗时,单位是秒
522739K->156516K(1322496K):三个参数分别为:堆区垃圾回收前的大小,堆区垃圾回收后的大小,堆区总大小。
0.0025301 secs:该内存区域GC耗时,单位是秒[Times: user=0.04 sys=0.00, real=0.01 secs]:分别表示用户态耗时,内核态耗时和总耗时
分析下可以得出结论:该次GC新生代减少了367523-1293=366239K, Heap区总共减少了522739-156516=366223K, 366239 – 366223 =16K,说明该次共有16K内存从年轻代移到了老年代,可以看出来数量并不多,说明都是生命周期短的对象,只是这种对象有很多。
我们需要的是尽量避免Full GC的发生,让对象尽可能的在年轻代就回收掉,所以这里可以稍微增加一点年轻代的大小,让那17K的数据也保存在年轻代中。
4. GC日志的error分析
ParNew + CMS 组合, 在 CMS 回收器触发时,出现了 promotion failed 和 concurrent mode failure 现象:
promotion failed, 该现象是在进行触发年轻代 ParNew GC 时,存活的对象在 Survivor 区放不下,对象只能进入老年代,而此时老年代也放不下导致的。
concurrent mode failure, 该现象是在执行 CMS 回收器回收垃圾的过程中同时有存活的对象放入老年代,而此时老年代空间不足,或者在做 ParNew GC 的时候,年轻代 Survivor 区放不下,需要放入老年代,而老年代也放不下而导致的。
针对以上2种现象产生的原因进行 JVM 相关参数优化:
可增大年轻代或者 Survivor 区的存储空间, -Xmn1500M, -XX:SurvivorRatio=8
或者提前触发 CMS 垃圾回收和进行 5 次 CMS 垃圾回收后整理清除碎片
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=80
对于生产系统,合理增大年轻代内存大小,本着尽量减少系统 Minor GC,一日最多一次 Full GC的原则