1. 前言
随着2023年秋季 JDK 21
的发布,我们现在有了一个新的 LTS 版本来进行基准测试并生成一些 GC 性能图表。JDK 21和 JDK 17 之后的其他版本提供了一系列值得注意的功能,例如虚拟线程
、switch 的模式匹配
和分代 ZGC
。
ZGC
是 Java 高度可扩展、低延迟的垃圾收集器,在 JDK 21 中通过JEP 439进行了更新,成为分代垃圾收集器
。那么,如何使用分代 ZGC?切换到分代 ZGC 后会获得什么样的性能?让我们来看看!
2. 什么是 ZGC?
ZGC
最初作为实验性功能
随 JDK 11 发布,在 JDK 15 中,它升级为生产功能。ZGC 的设计具有高度可扩展性,支持高达 16TB
的堆大小,同时保持亚毫秒级
的暂停时间。
ZGC 能够通过几乎完全并发的方式实现这些目标。这意味着 ZGC 在应用程序运行时执行其工作,分配新对象、扫描无法访问的对象、压缩堆等。
但是,这种设计选择的代价是,应用程序的吞吐量降低了
,因为应用程序可以使用的 CPU 资源却被 ZGC 利用了。
3. 什么是分代 ZGC?
分代 ZGC
旨在通过扩展 ZGC 来为年轻对象和老对象维护单独的代,从而提高应用程序性能。年轻对象往往在年轻时就死亡;维护单独的代将允许 ZGC 更频繁地收集年轻对象。使用分代 ZGC 运行的应用程序应获得以下好处:分配停滞的风险更低
、所需的堆内存开销更低
、垃圾收集 CPU 开销更低
。与非分代 ZGC 相比,这些好处应该可以在不显著降低吞吐量的情况下实现。
分代垃圾收集器
在逻辑上将堆分为两代:年轻代和老一代。如上图所示,分配对象时,它最初被放入年轻代,并经常被扫描。如果对象存活时间足够长,它将被提升到老一代。
分代垃圾收集器执行此行为是为了利用弱代假设
,该假设假定大多数对象在创建后不久就变得无法访问。
因此,ZGC 通过频繁扫描年轻一代,可以更有效地利用 CPU 资源。
在开发 Generational ZGC
的过程中,ZGC 工程团队
定期进行内部性能测试,以确保 Generational 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 个并发客户端,也能保持一致的暂停时间。
4. 使用 ZGC
由于在 ZGC 中实现分代行为是一项重大变更,因此 ZGC 团队设置了一个从单代 ZGC 到分代 ZGC 的过渡期。在 JDK 21 中,单代仍然是使用 ZGC 时的默认实现,但最终,分代 ZGC 将在未来版本中成为默认实现,而单代计划将被弃用,然后被删除。但是,这些步骤的时间表尚未确定。
使用 JDK 21
来使用 Generational ZGC
需要以下两个 JVM 参数:
$java -XX:+UseZGC -XX:+ZGenerational
5. 调整 ZGC
ZGC 的设计目标是自我调整。在大多数情况下,用户只需提供最大堆大小的配置。-Xmx但是,在某些情况下可能需要额外的配置;以下是一些值得考虑的关键配置。
5.1 -XX:SoftMaxHeapSize=SoftMaxHeapSize
此参数提供了 ZGC 尝试保持在以下的堆大小指导值。但是,ZGC 将超出此限制以避免分配问题。ZGC 将尝试尽快回到该值以下并将内存返回给操作系统。
如果您主要关心的是延迟,那么有几种配置值得考虑:
将最小堆大小设置-Xms
为与 mas heap 相同的值-Xmx
。这将阻止 ZGC 将未认领的内存返回给操作系统,从而导致延迟。
5.2 -XX:-ZUncommit
可以使用该值来禁用将内存返回给操作系统。
5.3 -XX:ZUncommitDelay=
这管理 ZGC 在将内存返回给操作系统之前等待的时间。默认值为 300 秒
。
5.4 -XX:+AlwaysPreTouch
这会将堆的准备工作移至启动期间。这会使启动速度稍慢,但好处是可以减少平均延迟。
6. ZGC 剖析
相反,您正在评估分代 ZGC 以确定是否要切换到它或衡量调整更改的影响,您必须对 ZGC 进行分析才能准确评估它。收集有关垃圾收集器的诊断信息有两种主要方法:GC 日志记录
和 JDK Flight Recorder
。
6.1 GC 日志记录
自 JDK 9 以来,使用 JVM 日志记录变得更加容易,同时提供更高质量的数据。这是 JDK 9 中包含的两个 JEP 158和271的结果。这使得 JVM 日志记录成为评估 GC 时的绝佳选择。
JVM 日志记录使用参数配置-Xlog,如以下示例所示:
$ java -Xlog:gc:gen-zgc.log
此命令将捕获仅带有标记的日志语句gc并将它们传送到文件gen-zgc.log
。
对于更广泛的 GC 日志记录,您可以使用以下命令:
$ java -Xlog:gc*:gen-zgc.log
此命令将捕获所有包含该gc标签的日志语句。此命令还将打印出 GC 统计表,如本例所示。
有关 JVM 日志的更多信息,请务必查看官方文档。
6.2 JDK 飞行记录器
JDK Flight Recorder
,JFR
,是 Java 的可观察性和监控框架,直接集成到 JDK 中。要深入了解 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 行为的总体摘要:
注意:某些摘要页面信息可能看起来有些偏离;
Generational ZGC 和 JMC
开发人员正在积极讨论如何在 Generational ZGC
中最好地代表年轻垃圾收集和老垃圾收集。
7. 结论
分代 ZGC
将使 ZGC 成为更多 Java 应用程序的绝佳选择。ZGC
提供可扩展性和超低延迟,并且随着分代功能的增加,分配停滞问题已基本得到解决。升级到 JDK 21 时,请借此机会评估分代 ZGC,看看它是否适合您的 Java 应用程序。