依然顺滑!Dragonwell 11如何改造全新垃圾回收器ZGC? | 龙蜥技术
本文是 Alibaba Dragonwell ZGC 系列的第三篇技术分享,重点介绍我们在 Dragonwell 11 上对 ZGC 的生产就绪改造工作,从而有效应对了本系列第一篇提到的 OpenJDK 11 实验性 ZGC 的风险。文末将小结 Dragonwell ZGC 系列文章,并展望未来发展方向。
相关阅读:
丝般顺滑!全新垃圾回收器 ZGC 初体验 | 龙蜥技术
中篇|丝般顺滑!全新垃圾回收器 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 功能完善
ZGC 重构 C2 读屏障
ZGC 多平台支持
ZGC 类卸载支持
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
这是因为 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,程序崩溃。
ZGC 监控升级
Dragonwell 11 更新了 GC 日志的细节:包括错误活跃对象信息更正,并显示不同规格 ZPage 的统计信息。
保持质量稳定性
阿里巴巴 Dragonwell 11 有选择地移植生产就绪 ZGC 代码,并且对这部分代码进行合理地改造,使得 Dragonwell 11 既拥有 OpenJDK15 的 ZGC 能力,也能够享受到 OpenJDK11 长期支持的质量稳定性。
后续升级困难: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( … )
if (UseZGC) { … }
开源与测试流程
我们在 GitHub 开源了 ZGC 生产就绪改造的过程,记录在了里程碑(https://github.com/alibaba/dragonwell11/milestone/1)中。里程碑囊括了两百余个 ZGC 相关 patch。每个 patch 都得到了阿里巴巴专家的精心 review。
展望
我们注意到 ZGC 的一些最新进展,可以进一步优化 ZGC 的性能,包括:
小结
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
https://wiki.openjdk.java.net/display/zgc/Main
http://cr.openjdk.java.net/~pliden/slides/ZGC-OracleDevLive-2020.pdf
现 DragonWell 已加入龙蜥社区 (OpenAnolis )Java 语言与虚拟机 SIG,同时龙蜥操作系统(Anolis OS )8 版本支持 DragonWell 云原生 Java ,欢迎大家加入社区 SIG,参与社区共建。
官网:https://openanolis.cn/sig/java/doc/216166872482840581
加入微信群:添加社区助理-龙蜥社区小龙(微信:openanolis_assis),备注【龙蜥】拉你入群;加入钉钉群:扫描下方钉钉群二维码。欢迎开发者/用户加入龙蜥社区(OpenAnolis)交流,共同推进龙蜥社区的发展,一起打造一个活跃的、健康的开源操作系统生态!
龙蜥社区钉钉交流群
龙蜥社区(OpenAnolis)是由企事业单位、高等院校、科研单位、非营利性组织、个人等按照自愿、平等、开源、协作的基础上组成的非盈利性开源社区。龙蜥社区成立于2020年9月,旨在构建一个开源、中立、开放的Linux上游发行版社区及创新平台。
短期目标是开发龙蜥操作系统Anolis OS作为CentOS替代版,重新构建一个兼容国际Linux主流厂商发行版。中长期目标是探索打造一个面向未来的操作系统,建立统一的开源操作系统生态,孵化创新开源项目,繁荣开源生态。
龙蜥OS 8.4已发布,支持x86_64和ARM64架构,完善适配Intel、飞腾、海光、兆芯、鲲鹏芯片。
欢迎下载:https://openanolis.cn/download
加入我们,一起打造面向未来的开源操作系统!
https://openanolis.cn
戳“阅读原文”直达龙蜥社区SIG哦~