中篇|丝般顺滑!全新垃圾回收器 ZGC 原理与调优
ZGC原理
从宏观的角度看,ZGC 是一种并发( concurrent )的压缩式( compacting ) GC 算法:
并发:在 Java 线程运行的同时,GC 线程在后台默默执行;
压缩式:定期将堆中活跃对象整理到一起,解决内存碎片化问题。
ZGC 的读屏障是在指针加载的操作的时候,插入一段针对该指针的处理逻辑:
如果指针指向已经被转移的对象,那么读屏障将修正该指针;
在标记阶段,如果该指针未被标记,那么读屏障将标记该指针;
在转移阶段,如果该指针指向需要转移的区域,那么该指针指向的对象将被转移,然后修正该指针。
读屏障能够确保在 GC 线程与 Java 线程并发运行的情况下,每次指针载入都能访问到正确的对象。
ZGC日志分析
单次ZGC周期实际执行过程中需要三次短促的暂停,每次暂停之后是若干并发阶段。
[2020-12-23T13:30:57.402+0800] GC(10) Garbage Collection (Allocation Rate)
[2020-12-23T13:30:57.408+0800] GC(10) Pause Mark Start 2.918ms
[2020-12-23T13:30:58.083+0800] GC(10) Concurrent Mark 674.216ms
[2020-12-23T13:30:58.087+0800] GC(10) Pause Mark End 1.336ms
[2020-12-23T13:30:58.105+0800] GC(10) Concurrent Process Non-Strong References 18.293ms
[2020-12-23T13:30:58.111+0800] GC(10) Concurrent Reset Relocation Set 5.533ms
[2020-12-23T13:30:58.111+0800] GC(10) Concurrent Destroy Detached Pages 0.001ms
[2020-12-23T13:30:58.121+0800] GC(10) Concurrent Select Relocation Set 10.148ms
[2020-12-23T13:30:58.130+0800] GC(10) Concurrent Prepare Relocation Set 9.083ms
[2020-12-23T13:30:58.136+0800] GC(10) Pause Relocate Start 2.452ms
[2020-12-23T13:30:58.203+0800] GC(10) Concurrent Relocate 66.595ms
... (此处忽略一些数据统计信息)
[2020-12-23T13:30:58.203+0800] GC(10) Garbage Collection (Allocation Rate) 62020M(76%)->41270M(50%)
Pause Mark Start(标记开始暂停);
Pause Mark End(标记结束暂停);
Pause Relocate Start(转移开始暂停)。
可以看到上面的 GC 日志中,ZGC 三个暂停阶段的时间明显低于 10ms 。这三个暂停阶段主要承担 GC Roots 的标记和转移,以及标记线程同步的工作。
这三个暂停阶段后面以 Concurrent 开头的阶段即为并发阶段,其中最核心的两个阶段是
Concurrent Mark(并发标记);
Concurrent Relocate(并发转移)。
其余的并发阶段主要是并发转移之前的一些预备工作。
ZGC 各个阶段的图示
ZGC调优
基本调优
堆空间大小
GC 通常需要开发者指定堆空间大小,具体数值应该大于堆内活跃对象的总大小。冗余空间比例越高,GC 性能通常越好。例如估计对象总大小达到 32GB ,可设置- Xmx40g ,代表开启 40GB 的堆。
ZGC 与传统 GC 的不同之处在于,ZGC 在回收对象的同时,Java 线程也在分配新对象。因此 ZGC 比传统 GC 需要更高比例的冗余空间。
每一轮 ZGC 的过程中分配的对象总大小可以用“分配速度·单轮 ZGC 时间”来估算,因此堆空间的大小应大于“活跃对象的总大小+单次 ZGC 期间分配的对象总大小”。
并发GC线程数量
ZGC 缺省的并发 GC 线程数量是 1/8 的 CPU 核数,例如 16 核的机器,如果没有指定 ConcGCThreads,那么 ZGC 就会开启 2 个并发 GC 线程。
在 GC 日志中,如果频繁出现“ Allocation Stall ”,代表回收跟不上分配的情况,那么可能需要提高 ConcGCThreads 了。当然 ConcGCThreads 不能无限制地增加,因为过多并发的 GC 线程会占据 CPU 资源,甚至影响 Java 线程的正常执行。
进阶调优
https://github.com/alibaba/dragonwell11/wiki/Alibaba-Dragonwell-11-Release-Notes#110117
进阶调优最核心的部分是 GC 触发时机的控制。由于 ZGC 在回收时依然分配对象,于是 ZGC 不能等到堆空间满了以后才触发 GC ,而需要提前一段时间触发 GC ,使得 ZGC 执行的过程中堆空间不会变满,导致 Allocation Stall 或者 OOM。但是如果 ZGC 触发地过于频繁,则 CPU 资源消耗变多,从而降低吞吐率。
Dragonwell11 支持以下 GC 触发时机相关选项:
ZAllocationSpikeTolerance:ZGC 通过估算“分配速度·单轮 ZGC 时间”来估算单次 ZGC 期间分配的对象总大小,只要这个总大小小于当前剩余的堆空间,就需要触发 GC 。但是由于 Java 业务的分配速度往往不是稳定的,因此需要为分配速度乘上“毛刺系数” ZAllocationSpikeTolerance ,从而保守地提前触发 GC 。如果 Java 业务分配速度不稳定,偶尔有 Allocation Stall 的发生,那么就应当考虑适当增加 ZAllocationSpikeTolerance。
ZCollectionInterval:定时触发 GC,避免 GC 间隔过长。
ZProactive:字面意思是“主动触发 GC ”,用于处理分配速率较低的情况。
ZHighUsagePercent:堆的水位超过此百分比,则触发 ZGC 。
SoftMaxHeapSize 选项可以设置 ZGC 堆空间的“软上限”,介于 Xmx 和 Xms 之间。以上的 ZAllocationSpikeTolerance/ZProactive/ZHighUsagePercent 均以 SoftMaxHeapSize 的值作为 ZGC 堆空间的“软上限”,当分配速度过快时可以扩展到至多 Xmx 的堆空间,当分配速度较慢时可以将堆空间收缩到 Xms 。SoftMaxHeapSize 通常需要打开 - XX:+ZUncommit 。
除此之外还有一些有用的进阶调优功能:
ZFragmentationLimit:控制 ZGC 的对象的碎片化程度,ZFragmentationLimit 越低,ZGC 回收越彻底;
ZMarkStackSpaceLimit:调节 ZGC 标记栈空间大小;
ZUnloadClassesFrequency:控制 ZGC 类卸载的频率;
ZRelocationReservePercent:控制 ZGC 的预留分配空间,降低 OOM 风险;
ZStatisticsInterval:控制 ZGC 日志中统计信息的输出频率,原先 10 秒一次输出可能会影响 GC 详细信息的解读。
(上面的 ZHighUsagePercent/ZUnloadClassesFrequency/ZRelocationReservePercent 是 Dragonwell11 的特有选项。若切换到其他版本的 OpenJDK 时,请避免使用这些选项。)
在后面的篇章中,读者将看到我们的 Alibaba Dragonwell11 通过对 ZGC 进行生产就绪改造,从而解决生产实践中的一些问题。