

从宏观的角度看,ZGC 是一种 并发( concurrent )的 压缩式( compacting ) GC 算法:
并发:在 Java 线程运行的同时,GC 线程在后台默默执行;
压缩式:定期将堆中活跃对象整理到一起,解决内存碎片化问题。
ZGC 的读屏障是在指针加载的操作的时候,插入一段针对该指针的处理逻辑:
如果指针指向已经被转移的对象,那么读屏障将修正该指针;
在标记阶段,如果该指针未被标记,那么读屏障将标记该指针;
在转移阶段,如果该指针指向需要转移的区域,那么该指针指向的对象将被转移,然后修正该指针。
读屏障能够确保在 GC 线程与 Java 线程并发运行的情况下,每次指针载入都能访问到正确的对象。

单次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(标记结束暂停);
可以看到上面的 GC 日志中,ZGC 三个暂停阶段的时间明显低于 10ms 。这三个暂停阶段主要承担 GC Roots 的标记和转移,以及标记线程同步的工作。
这三个暂停阶段后面以 Concurrent 开头的阶段即为并发阶段,其中最核心的两个阶段是
Concurrent Mark(并发标记);
其余的并发阶段主要是并发转移之前的一些预备工作。

ZGC 各个阶段的图示
GC 通常需要开发者指定堆空间大小,具体数值应该大于堆内活跃对象的总大小。冗余空间比例越高,GC 性能通常越好。例如估计对象总大小达到 32GB ,可设置- Xmx40g ,代表开启 40GB 的堆。
ZGC 与传统 GC 的不同之处在于,ZGC 在回收对象的同时,Java 线程也在分配新对象。因此 ZGC 比传统 GC 需要更高比例的冗余空间。
每一轮 ZGC 的过程中分配的对象总大小可以用“ 分配速度·单轮 ZGC 时间”来估算,因此堆空间的大小应大于“ 活跃对象的总大小+单次 ZGC 期间分配的对象总大小”。
ZGC 缺省的并发 GC 线程数量是 1/8 的 CPU 核数,例如 16 核的机器,如果没有指定 ConcGCThreads,那么 ZGC 就会开启 2 个并发 GC 线程。
在 GC 日志中,如果频繁出现“ Allocation Stall ”,代表回收跟不上分配的情况,那么可能需要提高 ConcGCThreads 了。当然 ConcGCThreads 不能无限制地增加,因为过多并发的 GC 线程会占据 CPU 资源,甚至影响 Java 线程的正常执行。
进阶调优最核心的部分是 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 ”,用于处理分配速率较低的情况。
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 风险;
(上面的 ZHighUsagePercent/ZUnloadClassesFrequency/ZRelocationReservePercent 是 Dragonwell11 的特有选项。若切换到其他版本的 OpenJDK 时,请避免使用这些选项。)
在后面的篇章中,读者将看到我们的 Alibaba Dragonwell11 通过对 ZGC 进行生产就绪改造,从而解决生产实践中的一些问题。

https://openanolis.cn/sig/java/doc/216166872482840581
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70004278/viewspace-2842512/ ,如需转载,请注明出处,否则将追究法律责任。