依然顺滑!Dragonwell 11如何改造全新垃圾回收器ZGC?

x_single
发布于 2022-5-7 13:42
浏览
0收藏

本文是 Alibaba Dragonwell ZGC 系列的第三篇技术分享,重点介绍我们在 Dragonwell 11 上对 ZGC 的生产就绪改造工作,从而有效应对了本系列第一篇提到的 OpenJDK 11 实验性 ZGC 的风险。文末将小结 Dragonwell ZGC 系列文章,并展望未来发展方向。

相关阅读:

丝般顺滑!全新垃圾回收器 ZGC 初体验 | 龙蜥技术
中篇|丝般顺滑!全新垃圾回收器 ZGC 原理与调优
依然顺滑!Dragonwell 11如何改造全新垃圾回收器ZGC?-鸿蒙开发者社区

ZGC生产就绪改造
本系列第一篇文章曾提到阿里巴巴将 Dragonwell11 上的 ZGC 改造为生产就绪版本,同时也提到 OpenJDK12-16 并非长期支持版本导致难以在生产中大规模部署。那么问题来了,为何不用 OpenJDK 11 的 ZGC,而需要用Dragonwell 11 的呢?Dragonwell 11 的 ZGC 有什么具体的适用场景?

我们认为,只要采用 Java 11,那么您就应该选择 Dragonwell 11 的生产就绪 ZGC,而不是 OpenJDK 11 的实验性 ZGC。其中最重要的理由,就是实验性 ZGC 有概率发生无征兆的崩溃现象(参考本系列第一篇文章),而该问题在生产就绪 ZGC 中得以修复。Dragonwell 11 的 ZGC 还完善了许多功能,让 ZGC 能够解锁更多的场景。

Dragonwell 11 的生产就绪 ZGC 有诸多优势,包括:

  • ZGC 功能完善且能解决实践中遇到的问题;
  • 持 Java 11 长期支持的质量稳定性;
  • 整的开源和测试流程。
    ZGC 功能完善
    Dragonwell 11 移植了 OpenJDK 15(首个支持生产就绪 ZGC 的 JDK 正式版本)的大部分 ZGC 相关代码,这些代码完善了 ZGC 的功能,支持更多的平台,并且修复了读屏障的重大 bug。
    ZGC 重构 C2 读屏障
    本系列第一篇文章提到,我们发现 ZGC 读屏障与加载操作的中间可能进入 GC 暂停。这是因为 ZGC 读屏障采用 C2 即时编译生成平台相关代码,而 OpenJDK 11 的 ZGC C2 读屏障可能产生上述的情形,由此引发错误。我们对照 OpenJDK 14 对于 ZGC C2 读屏障的改造,取消读屏障 C2 节点,对加载操作相关的 C2 节点进行改造,并使之在机器码生成阶段能够生成正确的读屏障和加载操作。
    我们后续的实践表明,C2 读屏障重构可以消除 ZGC 崩溃现象,进一步提高了  ZGC 在生产实践中的可用性。
    ZGC 多平台支持
    Dragonwell 11 新增支持 AArch64/Linux。许多阿里业务和云上客户希望在 AArch64 平台上也能用到 ZGC 的能力,同时也需要 OpenJDK 11 的长期支持。因此 Dragonwell 11 移植了 ZGC 与 AArch64 相关的生产就绪代码,从而拓宽了 ZGC 适用的机型。
    ZGC 类卸载支持
    类卸载是完整的 GC不 可或缺的一环,负责卸载 Java 中不再活跃的类。许多 Java 代码中会生成大量的类,而不再活跃的类和对象需要及时回收,否则将填满类信息的元空间,影响后续代码的执行。
    OpenJDK 对于 ZGC 的并发类卸载功能是在 OpenJDK 12 中完成的。这个过程伴随着大量的公共数据结构(非 ZGC 代码)的并发化改造。这些大量公共数据结构改造包含上百个代码改动,稍有不慎就会导致难以控制的代码风险,而且后续与上游同步成本会随之增高。
    Dragonwell 11 参考了现有 GC 的类卸载代码,结合 OpenJDK 12中 ZGC 关于类卸载功能的代码,实现了 ZGC 的类卸载功能。尽管 ZGC 的类卸载功能不是并发的,目前我们的实践显示这一类卸载过程能将暂停保持在 10ms 级别。由于类卸载在许多业务中不是频繁发生的,因此我们让 Dragonwell 11 支持 ZUnloadClassesFrequency 选项来调节类卸载的频率。
    ZGC 内存使用优化
    Dragonwell 11 ZGC 相比于 OpenJDK 11 增加了归还物理内存、内存规格的扩展、并行 pre-touching 的支持。这些新增的支持能够帮助 Dragonwell 11  ZGC 解锁更多细化的场景。

归还物理内存:ZGC 的归还物理内存功能适用于“同一个机器部署多个实例”的场景。Dragonwell 11 的 ZGC 可通过设置 ZUncommit 来开启归还物理内存的功能。开发者只要设置堆的上限 Xmx、下限 Xms 以及 SoftMaxHeapSize,Java 业务平时使用的堆大小将保持在 Soft Max Heap Size 左右。当有突发流量到来之时,Java 业务可以临时扩大堆的大小,以应对突发流量;当突发流量过去了以后,还可以将暂时用不到的内存归还给操作系统。

内存规格的扩展:Dragonwell 11 扩展了 ZGC 的适用内存规格,能够支持 16TB 的超大堆和 8MB 的超小堆,使得同一个业务部署不同规格的机器更加方便。

并行pre-touching:GC 的 pre-touching 的能力(打开-XX:+AlwaysPreTouch)可以让业务的 RT 免遭业务刚启动时内存 touch 的影响,而 JDK 11 中的 ZGC pre-touching 是单线程的,导致应用启动时候需要消耗很长的时间(大堆的 pre-touching 过程可达到分钟级别)。Dragonwell 11 并行化改造了 pre-touching 的过程,使得大堆业务的启动速度得以提升。

ZGC 响应时间优化
这里的响应时间是关于非暂停因素影响 RT P99/P999 的情况。

在 JDK 11 的 ZGC 实践过程当中,我们可能会看到 Page Cache Flush。

[2019-09-05T14:14:04.242+0800] GC(10816) Page Cache Flushed: 28M requested, 28M(11424M->11396M) flushed
[2019-09-05T14:14:04.248+0800] Page Cache Flushed: 32M requested, 32M(11928M->11896M) flushed
[2019-09-05T14:14:04.259+0800] Page Cache Flushed: 32M requested, 32M(11912M->11880M) flushed
[2019-09-05T14:14:04.271+0800] Page Cache Flushed: 32M requested, 32M(11878M->11846M) flushed
[2019-09-05T14:14:04.276+0800] Page Cache Flushed: 32M requested, 32M(11846M->11814M) flushed
... (省略35个"Page Cache Flushed")
[2019-09-05T14:14:04.462+0800] Page Cache Flushed: 32M requested, 32M(10596M->10564M) flushed
[2019-09-05T14:14:04.467+0800] Page Cache Flushed: 32M requested, 32M(10564M->10532M) flushed
[2019-09-05T14:14:04.471+0800] Page Cache Flushed: 32M requested, 32M(10522M->10490M) flushed
[2019-09-05T14:14:04.477+0800] GC(10816) Page Cache Flushed: 32M requested, 32M(10490M->10458M) flushed

我们同时会在业务的监控上看到 RT P99 升高到 200ms 以上。如上图,因为发生了连续多次 Page Cache Flushed,持续时间长达 200ms 以上。此时 Page Cache Flush 引发了线程阻塞,几十个对象分配线程均等候在同一个锁上。
这是因为 ZGC 把堆划分成若干个 ZPage (与 G1 的 Region 概念相同),包括小型 (2MB), 中型 (32MB), 大型 (2*N MB)三种规格。对象分配时会把对象按照大小分配到相应规格的 ZPage 当中。Page Cache 是存放空闲 ZPage 的数据结构。

我们在实际运行当中遇到一个问题,即不同规格对象分配速率不稳定。有时候中型对象更多,那么就会导致中型  ZPage 变少,需要把小型/大型 ZPage 转化成中性 ZPage。这个转化动作就是 Page Cache Flush。Page Cache Flush 耗时较长,需要多次进行 mmap 系统调用(开销较大);Page Cache Flush 影响面大,需要锁住 ZPage 分配全局锁。

Dragonwell 11 的解决办法是移植“提升 ZPage 分配并发度”的特性。这个特性可以尽可能避免使用 ZPage 分配全局锁,并且异步执行 mmap。Dragonwell 11 的另一个解决办法是调整中型 ZPage 的对象大小阈值(原来的范围:256KB~4MB),我们新增支持设置 ZMediumObjectUpperBound,例如-XX:ZMediumObjectUpperBound=10MB  (代表调整后中等 ZPage 的范围:256KB~10MB)。实践表明,Dragonwell 11 可以大幅减少了 Page Cache Flush 引发的线程阻塞,从而优化 RT P99/P999。

ZGC 吞吐率问题处理
ZGC 在生产实践中有概率遇到吞吐率不足的情形,包括两种现象:分配暂停 Allocation Stall 和内存不足 OOM(Out of Memory)。

现象1: 分配暂停 Allocation Stall(回收速度跟不上分配速度)

开发人员增加堆大小(Xmx)或并发 GC 线程数量( ConcGCThreads )可以缓解这一现象。然而机器的计算资源是有限的,不可能无限制地增加堆和线程数。这时候就要考虑 ZGC 的触发时机:

(1) ZAllocationSpikeTolerance:这是 ZGC 在 JDK11 中就已经支持的,增加该参数可以处理分配速率毛刺,但是增加该参数不适应日常情形,过度触发 ZGC 导致 CPU 消耗过高;

(2) ZHighUsagePercent:一些业务对接的线上监控在堆的水位过高时候会报警。Experimental ZGC 对 ZGC 水位并没有绝对的限制。Product ready ZGC设置了 95% 作为堆的最高水位。Dragonwell 11 可以通过 ZHighUsagePercent 调节堆最高水位,当堆水位超过ZHighUsagePercent%时触发ZGC。

现象2: 内存不足 OOM

ZGC 预留了固定的空间作为对象转移的区域,但是如果Java线程访问对象速度过快,就可能导致对象转移速度过快,预留空间依然不足,最终导致 OOM,程序崩溃。

Dragonwell 11 可以调节参数 ZRelocationReservePercent,让堆的 ZRelocationReservePercent% 作为预留空间,更大程度避免了 OOM 的情形。
ZGC 监控升级
Dragonwell 11 更新了 GC 日志的细节:包括错误活跃对象信息更正,并显示不同规格 ZPage 的统计信息。

Dragonwell 11 还引入了 ZGC 相关的 JFR 事件:ZAllocationStall,ZPageAllocation,ZRelocationSet,ZRelocationSetGroup,ZUncommit,ZUnmap。这些 JFR 事件可以监控当前 ZGC 的状况,有助于排查 ZGC 出现的异常状况。同时更新了 ZGC 相关的 GarbageCollectorMXBean,从而可以监控 ZGC 的两种指标:ZGC 周期和 ZGC 暂停。
保持质量稳定性
阿里巴巴 Dragonwell 11 有选择地移植生产就绪 ZGC 代码,并且对这部分代码进行合理地改造,使得 Dragonwell 11 既拥有 OpenJDK15 的 ZGC 能力,也能够享受到 OpenJDK11 长期支持的质量稳定性。

我们注意到,如果不加控制地移植所有 ZGC 代码,则有可能修改Dragonwell 11 的公共部分的大量代码。这样带来的后果包括:

  • 后续升级困难:Dragonwell 11 会定期同步上游最新的 OpenJDK11 的代码,如果 OpenJDK11 的更新与我们的 Dragonwell 11 ZGC 改造同时修改了这部分代码,那么这部分代码将难以维护,增加代码出错的风险。
  • 影响 Dragonwell 11 的其他部分代码的正确性:ZGC 依赖的公共代码改动,包括一些类加载和 C2 公共代码的改动。其他的 GC(包括 G1/CMS等)乃至 JDK 的其余部分事实上也调用了这部分代码。如果没有仔细移植公共代码改动,确认这些改动不会影响正确性,那么用户可能遭遇意想不到的风险。
    因此我们需要对代码风险进行控制,把生产就绪改造尽可能控制在 ZGC 代码的范围之内,选择与生产就绪最相关的 ZGC 代码进行合理改造。

为了把改动控制在 ZGC 代码范围之内,我们采用了编译时检查和运行时检查的方式,保证 ZGC 改造代码不会“污染”公共代码。(这部分工作参考了 Shenandoah GC Backport to JDK11 的工作)

编译时检查采用“宏隔离”的方式,在关闭 ZGC 编译时,代码不会被编译,从而确保代码没有问题。这样的做法可以保证 ZGC 开启编译时的代码质量。“宏隔离”即采用宏的方式进行隔离:

#if INCLUDE_ZGC … #endif
ZGC_ONLY( … )

运行时检查采用“条件隔离”的方式,保证其他 GC 开启时候不会执行到我们移植的代码,进一步降低 Dragonwell 11 的风险。“条件隔离”即采用 if 语句进行隔离:

if (UseZGC) { … }

开源与测试流程
我们在 GitHub 开源了 ZGC 生产就绪改造的过程,记录在了里程碑(https://github.com/alibaba/dragonwell11/milestone/1)中。里程碑囊括了两百余个 ZGC 相关 patch。每个 patch 都得到了阿里巴巴专家的精心 review。依然顺滑!Dragonwell 11如何改造全新垃圾回收器ZGC?-鸿蒙开发者社区我们维护了负责测试的 Nightly build 流水线,保证每晚都能够在 x64 和 AArch64 平台上正常编译,并且开启/关闭 ZGC 都能通过 OpenJDK 的测试。依然顺滑!Dragonwell 11如何改造全新垃圾回收器ZGC?-鸿蒙开发者社区展望

我们注意到 ZGC 的一些最新进展,可以进一步优化 ZGC 的性能,包括:

1、类指针压缩(compressed class pointers)。我们的内部实验显示,由于类指针压缩,ZGC 性能得到提升明显(尽管对象指针没有压缩)。由于代码移植对 JDK11 的稳定性有影响,因此暂未开源。
2、原地对象转移:JDK16 ZGC 采用了原地对象转移的技术,避免 OOM 的发生。
3、亚毫秒级别暂停:JDK16 ZGC 支持了并发线程栈处理,从而把 GC Roots的处理也放在并发线程中处理,达到 1ms 以内的暂停时间。
4、吞吐率提升:近期 ZGC 在自己的代码库中公开了分代 ZGC 的代码,有望提升 ZGC 的吞吐率。
我们还评估了 ZGC 的姊妹:Shenandoah GC。我们初步评估发现,Shenandoah GC 在 32GB 以内的堆上效果较好,其中最重要的因素是它对于指针压缩的支持。
小结
Alibaba Dragonwell ZGC 系列从 GC 概念,谈到 ZGC 及其适用场景,以及Dragonwell 11 对 ZGC 的生产就绪改造。这项工作维持了 Dragonwell 11 的稳定性,同时把 ZGC 升级到了生产就绪的 ZGC:修复了 ZGC 重大缺陷,新增支持 AArch64 平台,以及众多新功能的完善。Dragonwell 11 还新增了若干通用特性,适应阿里内部和云上客户的需求。

未来我们还将不定期更新 Alibaba Dragonwell ZGC 系列,分享我们使用 ZGC 的经验,以及我们在 OpenJDK 的相关贡献。

相关链接
Alibaba_Dragonwell_11.0.11.7:

https://github.com/alibaba/dragonwell11/releases/tag/dragonwell-11.0.11.7_jdk-11.0.11-ga

ZGC 项目主页:
https://wiki.openjdk.java.net/display/zgc/Main

ZGC 官方介绍:
http://cr.openjdk.java.net/~pliden/slides/ZGC-OracleDevLive-2020.pdf

已于2022-5-7 13:42:03修改
收藏
回复
举报
回复
    相关推荐