您的位置:  首页 > 技术 > go语言 > 正文

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

2021-08-28 17:00 https://my.oschina.net/u/5265430/blog/5204814 OpenAnolis龙蜥社区 次阅读 条评论

本文是 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 功能完善

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 … #endifZGC_ONLY( … )
运行时检查采用“条件隔离”的方式 ,保证其他 GC 开启时候不会执行到我们移植的代码,进一步降低 Dragonwell 11 的风险。“条件隔离”即采用 if 语句进行隔离:
   
   
   
    
    
    if (UseZGC) { … }
   
   
   

开源与测试流程

我们在 GitHub 开源了 ZGC 生产就绪改造的过程,记录在了里程碑(https://github.com/alibaba/dragonwell11/milestone/1)中。里程碑囊括了两百余个 ZGC 相关 patch。每个 patch 都得到了阿里巴巴专家的精心 review。

我们维护了负责测试的 Nightly build 流水线,保证每晚都能够在 x64 和 AArch64 平台上正常编译,并且开启/关闭 ZGC 都能通过 OpenJDK 的测试。

展望

我们注意到 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

关于作者 唐浩,2019 年加入阿里云编程语言与编译器团队,目前从事 JVM 内存管理优化方向的工作。 欢迎加入SIG

 DragonWell 已加入龙蜥社区 (OpenAnolis )Java 语言与虚拟机 SIG,同时龙蜥操作系统(Anolis OS )8 版本支持 DragonWell 云原生 Java ,欢迎大家加入社区 SIG,参与社区共建。

SIG 地址

官网:https://openanolis.cn/sig/java/doc/216166872482840581

—— 完 ——
加入龙蜥社群

加入微信群:添加社区助理-龙蜥社区小龙(微信:openanolis_assis),备注【龙蜥】拉你入群;加入钉钉群:扫描下方钉钉群二维码。欢迎开发者/用户加入龙蜥社区(OpenAnolis)交流,共同推进龙蜥社区的发展,一起打造一个活跃的、健康的开源操作系统生态!

DragonWell 钉钉交流群

龙蜥社区钉钉交流群

关于龙蜥社区

龙蜥社区(OpenAnolis)是由企事业单位、高等院校、科研单位、非营利性组织、个人等按照自愿、平等、开源、协作的基础上组成的非盈利性开源社区。龙蜥社区成立于2020年9月,旨在构建一个开源、中立、开放的Linux上游发行版社区及创新平台。

短期目标是开发龙蜥操作系统Anolis OS作为CentOS替代版,重新构建一个兼容国际Linux主流厂商发行版。中长期目标是探索打造一个面向未来的操作系统,建立统一的开源操作系统生态,孵化创新开源项目,繁荣开源生态。

龙蜥OS 8.4已发布,支持x86_64和ARM64架构,完善适配Intel、飞腾、海光、兆芯、鲲鹏芯片。

欢迎下载:https://openanolis.cn/download

加入我们,一起打造面向未来的开源操作系统!

https://openanolis.cn

戳“阅读原文”直达龙蜥社区SIG哦~

本文分享自微信公众号 - OpenAnolis龙蜥(OpenAnolis)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

  • 0
    感动
  • 0
    路过
  • 0
    高兴
  • 0
    难过
  • 0
    搞笑
  • 0
    无聊
  • 0
    愤怒
  • 0
    同情
热度排行
友情链接