关于G1 GC以及其他垃圾收集器的介绍可以参考前一篇JVM性能调优实践——G1 垃圾收集器介绍篇。了解了G1垃圾收集器的运行机制之后,就可以针对一些GC相关参数来调整内存分配以及运行策略。下文的调优主要针对G1垃圾收集器进行介绍,以及会分析一下G1 GC的日志格式。
在执行具体的调优任务前,需要结合GC日志以及应用本身的特点。打印详细GClog,需要添加如下启动参数:
-XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps
本文使用的Java version:
java version "1.8.0_60"
Java(TM) SE Runtime Environment (build 1.8.0_60-b27)
Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)
下面截取gc.log 中的一次YoungGC和一次MixedGC。
2018-05-26T19:51:45.808-0800: 127.031: [GC pause (G1 Evacuation Pause) (young), 0.0063650 secs]
[Parallel Time: 5.5 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 127030.7, Avg: 127030.7, Max: 127030.7, Diff: 0.0]
[Ext Root Scanning (ms): Min: 1.1, Avg: 1.3, Max: 1.5, Diff: 0.4, Sum: 5.3]
[Update RS (ms): Min: 1.3, Avg: 1.4, Max: 1.4, Diff: 0.1, Sum: 5.4]
[Processed Buffers: Min: 3, Avg: 12.5, Max: 24, Diff: 21, Sum: 50]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.1, Avg: 0.3, Max: 0.7, Diff: 0.5, Sum: 1.3]
[Object Copy (ms): Min: 2.2, Avg: 2.4, Max: 2.5, Diff: 0.4, Sum: 9.5]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 7]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 5.4, Avg: 5.4, Max: 5.4, Diff: 0.1, Sum: 21.6]
[GC Worker End (ms): Min: 127036.1, Avg: 127036.1, Max: 127036.1, Diff: 0.0]
[Code Root Fixup: 0.1 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.6 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.3 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 39.0M(39.0M)->0.0B(2048.0K) Survivors: 3072.0K->4096.0K Heap: 111.4M(128.0M)->72.9M(128.0M)]
[Times: user=0.02 sys=0.00, real=0.01 secs]
2018-05-26T19:57:20.534-0800: 461.748: [GC pause (G1 Evacuation Pause) (mixed), 0.0685311 secs]
[Parallel Time: 67.2 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 461748.1, Avg: 461748.1, Max: 461748.1, Diff: 0.0]
[Ext Root Scanning (ms): Min: 0.8, Avg: 2.6, Max: 7.5, Diff: 6.6, Sum: 10.5]
[Update RS (ms): Min: 0.0, Avg: 0.3, Max: 0.7, Diff: 0.7, Sum: 1.4]
[Processed Buffers: Min: 0, Avg: 9.2, Max: 35, Diff: 35, Sum: 37]
[Scan RS (ms): Min: 29.7, Avg: 34.3, Max: 36.1, Diff: 6.5, Sum: 137.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.5, Max: 0.8, Diff: 0.8, Sum: 2.0]
[Object Copy (ms): Min: 28.8, Avg: 29.3, Max: 29.8, Diff: 1.0, Sum: 117.1]
[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 67.1, Avg: 67.1, Max: 67.1, Diff: 0.0, Sum: 268.5]
[GC Worker End (ms): Min: 461815.2, Avg: 461815.2, Max: 461815.2, Diff: 0.0]
[Code Root Fixup: 0.3 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 1.0 ms]
[Choose CSet: 0.4 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.2 ms]
[Eden: 5120.0K(5120.0K)->0.0B(57.0M) Survivors: 1024.0K->1024.0K Heap: 64.3M(128.0M)->55.8M(128.0M)]
[Times: user=0.07 sys=0.11, real=0.07 secs]
两个收集过程的日志格式相似。先以young gc为例,分析一下日志信息。
并行任务部分主要包含Parallel Time这一行以及其下面的详细任务信息。
[Parallel Time: 5.9 ms, GC Workers: 4] 这一行标记着并行阶段的汇总信息。总共花费时间以及GC的工作线程数。
后续的这两行,start-end是时间戳信息。Diff是偏移平均时间的值。Diff越小越好,说明每个工作线程的速度都很均匀,如果Diff值偏大,就要看下面具体哪一项活动产生的波动。Avg代表平均时间值。如果Avg跟Min,Max偏差不大是比较正常的,否则也要详细分析具体的偏差值大的任务。
下一段显示的是详细的并行阶段的GC活动。
[Ext Root Scanning (ms): Min: 1.1, Avg: 1.3, Max: 1.5, Diff: 0.4, Sum: 5.3]
[Update RS (ms): Min: 1.3, Avg: 1.4, Max: 1.4, Diff: 0.1, Sum: 5.4]
[Processed Buffers: Min: 3, Avg: 12.5, Max: 24, Diff: 21, Sum: 50]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.1, Avg: 0.3, Max: 0.7, Diff: 0.5, Sum: 1.3]
[Object Copy (ms): Min: 2.2, Avg: 2.4, Max: 2.5, Diff: 0.4, Sum: 9.5]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 7]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 5.4, Avg: 5.4, Max: 5.4, Diff: 0.1, Sum: 21.6]
外部根区扫描。外部根是堆外区。JNI引用,JVM系统目录,Classloaders等。后面跟着具体的时间信息。
log中RS指的是RSet。RSet详细信息可以看G1 GC介绍。
[Update RS (ms): Min: 1.3, Avg: 1.4, Max: 1.4, Diff: 0.1, Sum: 5.4]
[Processed Buffers: Min: 3, Avg: 12.5, Max: 24, Diff: 21, Sum: 50]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.1, Avg: 0.3, Max: 0.7, Diff: 0.5, Sum: 1.3]
如果观察到RS的处理时间较长,可以使用**-XX:+G1SummarizeRSetStats**参数,在GC结束后打印RSet的详细信息。一般在debug环境排查用。还有一个辅助参数G1SummarizeRSetStatsPeriod=0用来控制第几次GC后统计一次RSet信息。
Object Copy:该任务主要是对CSet中存活对象进行转移(复制)。对象拷贝的时间一般占用暂停时间的主要部分。如果拷贝时间和”预测暂停时间“有相差很大,也可以调整年轻代尺寸大小。
[Object Copy (ms): Min: 2.2, Avg: 2.4, Max: 2.5, Diff: 0.4, Sum: 9.5]
这里的终止主要是终止工作线程。Work线程在工作终止前会检查其他工作线程的任务,如果其他work线程有没完成的任务,会抢活。如果终止时间较长,额能是某个work线程在某项任务执行时间过长。
花在GC之外的工作线程的时间,比如因为JVM的某个活动,导致GC线程被停掉。这部分消耗的时间不是真正花在GC上,只是作为log的一部分记录。
并行阶段的GC汇总,包含了GC以及GC Worker Other的总时间。
一下是串行的GC活动。包括代码根的更新和扫描。Clear的时候还要清理RSet相应去除的Card Table信息。G1 GC在扫描Card信息时会有一个标记记录,防止重复扫描同一个Card。
[Code Root Fixup: 0.1 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
剩余的部分就是其他GC活动了。主要包含:选择CSet、引用处理和排队、卡片重新脏化、回收空闲巨型分区以及在收集之后释放CSet。
[Other: 0.6 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.3 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
如下对比了一次youngGC和一次mixedGC的垃圾收集结果:
young: [Eden: 39.0M(39.0M)->0.0B(2048.0K) Survivors: 3072.0K->4096.0K Heap: 111.4M(128.0M)->72.9M(128.0M)]
mixed: [Eden: 5120.0K(5120.0K)->0.0B(57.0M) Survivors: 1024.0K->1024.0K Heap: 64.3M(128.0M)->55.8M(128.0M)]
G1 GC是垃圾收集优先的垃圾收集器,同时有着”可预期的暂停时间“,垃圾收集过程是分代的,但堆空间是基于分区进行分配。所以整体的空间利用率,时间效率都有更大的提升。G1的YoungGC和MixedGC以及并发标记阶段都有很多机制可以控制触发时机,一般情况是不建议过度更改官方建议参数。但默认参数不一定适用于所有应用,调优前需要有明确的目标,或者问题处理思路。
以下先整理下G1垃圾收集器可以调整的重要参数:
因为G1 GC是启发式算法,会动态调整年轻代的空间大小。目标也就是为了达到接近预期的暂停时间。年轻代调优中比较重要的就是对暂停时间的处理。一般都是根据MaxGCPauseMillis以及年轻代占比G1NewSizePercent、G1MaxNewSizePercent,结合应用的特点和GC数据进行接近期望pause time的调整。为了能观察到详细的暂停时间信息,可以添加调试的启动参数**-XX:+PrintAdaptiveSizePolicy**。
下面摘取一段youngGC gc log的输出:
26.139: [GC pause (G1 Evacuation Pause) (young) 26.139: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 3484, predicted base time: 5.51 ms, remaining time: 194.49 ms, target pause time: 200.00 ms]
26.139: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 54 regions, survivors: 9 regions, predicted young region time: 5.98 ms]
26.139: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 54 regions, survivors: 9 regions, old: 0 regions, predicted pause time: 11.49 ms, target pause time: 200.00 ms]
, 0.0163685 secs]
target也即目标是200ms,实际的pause time是16ms。远远小于目标暂停时间。并且再CSet中的分区数是“eden: 54 regions, survivors: 9 regions”,可以适当增加CSet中的年轻代分区,也可以适当缩短暂停时间,让实际值和期望值不断接近。
InitiatingHeapOccupancyPercent就是触发并发标记的一个决定阀值。当Java堆空间占用到45%便开启并发周期。并发标记的初始标记阶段伴随着一次YoungGC的暂停。会看到如下log记录:
2018-05-26T19:50:57.256-0800: 78.480: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0076560 secs]
IHOP如果阀值设置过高,可能会遇到转移失败的风险,比如对象进行转移时空间不足。如果阀值设置过低,就会使标记周期运行过于频繁,并且有可能混合收集期回收不到空间。
IHOP值如果设置合理,但是在并发周期时间过长时,可以尝试增加并发线程数,调高ConcGCThreads。
G1 GC对于虚引用、弱引用、软引用的处理会比一般对象多一些收集任务。如果在引用处理占用了很长时间,需要更进一步排查。在并发标记的Remark阶段会记录引用的处理,日志信息如下:
如下:
[GC remark 2018-05-26T19:50:57.386-0800: 78.610: [Finalize Marking, 0.0002675 secs] 2018-05-26T19:50:57.386-0800: 78.611: [GC ref-proc, 0.0001091 secs] 2018-05-26T19:50:57.386-0800: 78.611: [Unloading, 0.0204521 secs], 0.0212793 secs]
可以通过**-XX:+PrintReferenceGC**打印更详细的引用计数信息:
[SoftReference, 0 refs, 0.0000482 secs]2018-06-03T20:52:03.887-0800: 18.033: [WeakReference, 116 refs, 0.0000321 secs]2018-06-03T20:52:03.887-0800: 18.033: [FinalReference, 1073 refs, 0.0009571 secs]2018-06-03T20:52:03.888-0800: 18.034: [PhantomReference, 0 refs, 1 refs, 0.0000211 secs]2018-06-03T20:52:03.888-0800: 18.034: [JNI Weak Reference, 0.0000192 secs], 0.0084976 secs]
一般在Ref Proc时间超过GC暂停时间的10%时就要关注。Ref Proc的信息打印在每次垃圾收集的Other信息模块:
[Other: 0.6 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.4 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
如果SoftReference过多,会有频繁的老年代收集。-XX:SoftRefLRUPolicyMSPerMB参数,可以指定每兆堆空闲空间的软引用的存活时间,默认值是1000,也就是1秒。可以调低这个参数来触发更早的回收软引用。如果调高的话会有更多的存活数据,可能在GC后堆占用空间比会增加。
对于软引用,还是建议尽量少用,会增加存活数据量,增加GC的处理时间。
本文简单介绍了一下G1 GC的调优参数以及G1 GC的日志内容。在具体调优过程,可以增加一些调优的参数,如**-XX:+G1SummarizeRSetStats**、-XX:+PrintReferenceGC、-XX:+PrintAdaptiveSizePolicy等。每次调参后还要密切关注GC log,最好能模拟生产环境进行全链路的测试。没有一个参数的调整是可以普适任何应用的,如果没有GC问题,就不需要为了优化而优化。但是对于GC的知识储备和更新,是每个应用开发工程师必备的知识模块。
G1 algorithms-implementations