介绍分代 ZGC

ZGC 是 Java 的高度可扩展、低延迟垃圾收集器,在 JDK 21 中通过 JEP 439 更新为分代垃圾收集器。那么,如何使用分代 ZGC?切换到分代 ZGC 会带来什么样的性能提升?让我们来看看!

什么是 ZGC?

ZGC 最初作为实验性功能在 JDK 11 中发布,并在 JDK 15 中升级为生产功能。ZGC 的设计目标是高度可扩展,支持高达 16TB 的堆大小,同时保持亚毫秒级的暂停时间。

ZGC 能够通过几乎完全并发的方式实现这些目标。这意味着 ZGC 在应用程序运行时执行其工作,包括分配新对象、扫描不可达对象、压缩堆等。

这种设计选择带来的权衡是,应用程序的吞吐量会降低,因为应用程序本来可以使用的 CPU 资源被 ZGC 利用了。

什么是分代 ZGC?

分代垃圾收集器在逻辑上将堆分为两代:年轻代和老年代。当分配一个对象时,它最初被放置在年轻代,年轻代会被频繁扫描。如果一个对象存活足够长的时间,它将被提升到老年代。

分代垃圾收集器执行这种行为是为了利用弱代假设,该假设认为大多数对象在创建后不久就会变得不可达。

因此,ZGC 通过频繁扫描年轻代,可以更有效地利用 CPU 资源。

在开发分代 ZGC 期间,ZGC 工程团队定期运行内部性能测试,以确保分代 ZGC 达到目标。

在吞吐量方面,分代 ZGC 在 JDK 17 中比单代 ZGC 提高了约 10%,在 JDK 21 中比单代 ZGC 提高了略高于 10%,JDK 21 中出现了一点回归。

虽然这张图表看起来与上一张几乎相同,但它讲述了一个不同的故事。这张图表显示,与单代 ZGC 相比,分代 ZGC 的平均延迟略有下降。

然而,当我们查看实际数字时,我们发现差异只有 2 到 3 微秒。

当查看最大暂停时间时,ZGC 开始闪耀。下面的图表显示,P99 暂停时间提高了 10-20%,与 JDK 21 和 JDK 17 单代 ZGC 相比,实际数字分别提高了 20 和 30 微秒。

分代 ZGC 最大的优势在于,它显著降低了单代 ZGC 最大的问题——分配停滞的可能性。分配停滞是指新对象分配的速度快于 ZGC 回收内存的速度。

如果我们将用例切换到 Apache Cassandra 并查看 99.999 百分位数,就可以看到这个问题。下面的图表显示,在最多 75 个并发客户端的情况下,单代 ZGC 和分代 ZGC 的性能相似。然而,在超过 75 个并发客户端的情况下,单代 ZGC 会不堪重负,并遇到分配停滞问题。另一方面,分代 ZGC 不会遇到这个问题,即使在多达 275 个并发客户端的情况下,也能保持一致的暂停时间。

如果您有兴趣了解更多关于分配停滞问题以及分代 ZGC 的信息,请务必查看 Erik Osterlünd 关于分代 ZGC 的 JVMLS 演示文稿

使用 ZGC

由于在 ZGC 中实现分代行为是一个重大的变化,ZGC 团队为从单代 ZGC 过渡到分代 ZGC 设置了一个过渡期。在 JDK 21 中,当使用 ZGC 时,单代仍然是默认实现,但最终,分代 ZGC 将在未来的版本中成为默认实现,单代计划被弃用,然后被移除。但是,这些步骤的时间表尚未确定。

在 JDK 21 中,使用分代 ZGC 需要以下两个 JVM 参数

$java -XX:+UseZGC -XX:+ZGenerational

调整 ZGC

ZGC 的设计目标是自调整。在大多数情况下,用户应该提供的唯一配置是最大堆;-Xmx<size>。但是,在某些情况下可能需要额外的配置;以下是一些值得考虑的关键配置。

-XX:SoftMaxHeapSize=<size>: 此参数提供了一个 ZGC 尝试保持在该值以下的堆大小指南。但是,ZGC 会超过此限制以避免分配问题。ZGC 会尽快尝试回到 SoftMaxHeapSize 以下,并将内存返回给操作系统。

如果您的主要关注点是延迟,那么有一些配置值得考虑

将最小堆大小 -Xms 设置为与最大堆 -Xmx 相同的值。这将阻止 ZGC 将未声明的内存返回给操作系统,这会导致延迟。

-XX:-ZUncommit: 或者,可以使用此值禁用将内存返回给操作系统。

-XX:ZUncommitDelay=<seconds>: 这管理 ZGC 在将内存返回给操作系统之前等待的时间。默认值为 300 秒。

-XX:+AlwaysPreTouch: 这将堆的准备工作移到启动期间。这将使启动速度略慢,但有利于降低平均延迟。

分析 ZGC

无论您是评估分代 ZGC 以查看是否要切换到它,还是衡量调整更改的影响,您都必须分析 ZGC 以准确评估它。收集有关垃圾收集器的诊断信息主要有两种方法:GC 日志记录和 JDK Flight Recorder。

GC 日志记录

从 JDK 9 开始,使用 JVM 日志记录变得更加容易,同时提供了更高质量的数据。这是 JDK 9 中包含的两个 JEP 的结果,158271。这使得 JVM 日志记录成为评估 GC 时的一个很好的选择。

JVM 日志记录使用 -Xlog 参数配置,例如以下示例

$ java -Xlog:gc:gen-zgc.log

此命令将捕获标记为 gc 的日志记录语句,并将它们管道传输到文件 gen-zgc.log

对于更广泛的 GC 日志记录,您可以使用以下命令

$ java -Xlog:gc*:gen-zgc.log 

此命令将捕获所有包含 gc 标签的日志记录语句。此命令还将打印出 GC 统计信息表,例如 此示例 中所示。

有关 JVM 日志记录的更多信息,请务必查看 官方文档

JDK Flight Recorder

JDK Flight Recorder (JFR) 是直接集成到 JDK 中的 Java 可观察性和监控框架。要深入了解 JFR,请查看我在 StackWalker 上的关于它的剧集。启动和配置 JFR 有多种选择;在评估 GC 时,您可能希望在启动时使用 -XX:StartFlightRecording 启用它,例如以下示例

-XX:StartFlightRecording=filename=gen-zgc.jfr,settings=profile

这将把 JFR 数据写入 gen-zgc.jfr,并使用 profile 设置,这些设置的开销低于 2%。或者,可以使用默认设置,其开销低于 1%,以及自定义设置

收集到 JFR 数据后,可以在 JDK Mission Control (JMC) 中对其进行评估。JMC 提供了多个选项卡来评估 GC 行为,包括垃圾收集的概述、GC 配置以及 GC 行为的总体摘要

注意:摘要页面中的一些信息可能看起来有点奇怪;分代 ZGC 和 JMC 开发人员之间正在积极讨论如何最好地表示分代 ZGC 中的年轻代和老年代垃圾收集。

结论

分代 ZGC 将使 ZGC 成为更多 Java 应用程序的绝佳选择。ZGC 提供了可扩展性和超低延迟,并且随着分代功能的添加,分配停滞问题已基本得到解决。升级到 JDK 21 时,请抓住机会评估分代 ZGC,看看它是否适合您的 Java 应用程序。

其他阅读材料

GC 调整指南

ZGC Wiki

ZGC OpenJDK 开发邮件列表

分代 ZGC 及其未来