OpenJDK 17 中的 Shenandoah:亚毫秒级 GC 停顿【译】

3,780 阅读9分钟

一、前言

Shenandoah OpenJDK 垃圾收集(GC) 项目的主要动机是减少垃圾收集暂停时间。在 JDK 12 中,发布了原始的 Shenandoah 垃圾收集器,它实现了并发堆疏散,解决了在不停止应用程序的情况下清理(大)堆的主要问题。这个版本最终被移植到 JDK 11。在 JDK 14 中,我们实现了并发类卸载,在 JDK 16 中,我们添加了并发引用处理,这两者都进一步减少了垃圾收集操作的暂停时间。暂停下剩余的垃圾收集操作是线程堆栈处理,我们已经在 JDK 17 中 解决了这个问题。

本文介绍了 Shenandoah GC 中新的并发线程堆栈处理。在 JDK 17 中并发处理线程堆栈为我们提供了可靠的亚毫秒级停顿。

二、Java中的线程处理

什么是线程处理,为什么我们需要为它停止应用程序?Java 程序在线程中执行,每个线程拥有一个_栈_:栈帧的列表,每个帧保存着局部变量、监视器以及与当前执行的方法相关的其他信息。最重要的是,在 Java 垃圾收集的上下文中,它保存对堆对象的引用(例如,引用类型化对象的局部变量)。

当垃圾收集周期开始时,我们首先扫描所有线程的堆栈,以使用我们在堆栈上找到的引用来播种标记队列。我们在_GC 暂停_(安全点)时这样做,因为我们需要在标记开始时堆栈的一致状态,而不是线程的执行并发地与堆栈混淆。完成后,我们继续执行并遍历可达对象的图,从我们在初始线程扫描期间找到的引用开始。

同样,当将可到达的对象疏散到空区域时,我们需要更新线程堆栈上的所有引用以指向新的对象位置。我们需要暂停一下,因为垃圾收集加载屏障通常在从堆加载引用时起作用(例如加载到局部变量或寄存器中),这意味着局部变量或寄存器在需要 GC 的状态下不能有对象引用干涉。到那时,通过垃圾收集屏障为时已晚。为每个局部变量或寄存器访问快速调用垃圾收集屏障会遇到性能问题。

扫描和处理线程堆栈需要时间。小型工作负载(具有小堆栈的少量线程)可能只需要几毫秒来扫描,但是大型工作负载——应用程序服务器,我正在看着你!——很容易花费几十毫秒来处理。所有这些处理都是在应用程序停止时完成的,因此它会影响应用程序的整体端到端延迟。

三、OpenJDK 17 中的并发线程处理

我们如何改善这种情况并同时处理线程堆栈?我们通过使用一种称为堆栈水印的机制(最初由 ZGC 开发人员实现)来实现这一点。中心观察是所有线程堆栈的操作都发生在最顶层的框架中:当前执行的方法。下面的所有帧基本上都是静态的并且不会改变——它们可以被垃圾收集线程安全地并发扫描。我们需要做的就是在堆栈帧被销毁时(例如,通过返回调用者,或通过抛出异常)协调 GC 线程与正在执行的线程,从而退出 GC 处理。这种协调是通过_堆栈水印_实现的,一个告诉我们堆栈的哪些部分可以安全扫描的指针,以及一个允许垃圾收集器处理返回的屏障。

并发线程处理中的堆栈水印。

四、在垃圾收集期间使用堆栈水印

让我们考虑一个例子。比如说,在标记开始时,在初始暂停期间,我们将堆栈水印设置为每个线程的最顶层帧并武装线程。这意味着我们认为所有帧对于并发扫描都是安全的,但没有(还)可以执行。从安全点返回后,我们将控制权交还给 Java 程序,因此需要顶层框架才能安全执行。在这里,堆栈水印屏障开始发挥作用,让垃圾收集器处理顶部帧(出于实际原因,还有它的调用者)。线程将扫描顶部帧并相应地降低水印,并在安全点之前离开的点恢复自己的执行。同时,GC 线程也开始扫描堆栈,从底部向上到水印,即在安全区中。

  1. 将水印降低一帧。
  2. 防止 GC 线程扫描超出该水印。
  3. 通过扫描任何引用来处理现在位于水印上方的帧。

最终结果是我们将有效地扫描所有相同的帧和引用,就像我们在初始标记暂停时所做的那样,但我们是在程序执行时同时进行的。

五、Shenandoah GC 基准测试

那么这些变化在实践中会产生什么影响呢?我已经运行了许多衡量垃圾收集暂停的基准测试。下表显示了 JDK 11、JDK 16 和 JDK 17 中所有基准测试的平均暂停时间。 JDK 16 和 JDK 17 之间的差异显示了并发堆栈处理所实现的改进。与 JDK 11 的区别是为了完整性而显示的,包括与以前版本相比的各种其他改进。

初始标记最终成绩
JDK 11421 微秒1294 微秒
JDK 16321 微秒704 微秒
JDK 1763 微秒328 微秒

六、发行版

Shenandoah 的可用性因供应商和 JDK 版本而异。默认情况下,OpenJDK 12+ 构建通常包括 Shenandoah。OpenJDK 11 需要在构建时选择加入。

已知供应商状态为:

  • 红帽
  • 亚马逊
    • 从 OpenJDK 11.0.9 开始,在 Amazon Corretto 中提供 Shenandoah
  • Oracle
    • 不在 任何版本中发布 Shenandoah,包括 OpenJDK 构建和专有构建。
  • Azul
    • 从 OpenJDK 11.0.9 开始,在 Azul Zulu 中发布 Shenandoah。
  • OpenJDK
    • 从 OpenJDK 11.0.9 开始,以默认二进制文件形式提供 Shenandoah
  • Linux 发行版
    • Debian 从 OpenJDK 11.0.9 开始发布 Shenandoah
    • IcedTea 的 Gentoo ebuild 具有 Shenandoah USE 标志
    • 基于 RHEL/Fedora 的发行版或其他使用其软件包的发行版也可能启用了 Shenandoah。值得注意的是, 众所周知,CentOS、 Oracle LinuxAmazon Linux 都发布了它。

七、启用 Shenandoah

使用 -XX:+UseShenandoahGC JVM 选项通过 Shenandoah GC 运行您的 Java 应用程序。

java <PATH_TO_YOUR_APPLICATION> -XX:+UseShenandoahGC

7.1 模式

模式定义了雪兰多运行的主要方式。这定义了 Shenandoah 使用的障碍(如果有),并定义了主要的性能特征。可以使用 -XX:ShenandoahGCMode= 选择模式。可用模式有:

  1. normal/satb(默认)。此模式使用 Snapshot-At-The-Beginning (SATB) 标记运行并发 GC。这种标记模式类似于 G1 所做的:通过“前一个”对象拦截写入和标记。
  2. iu(实验性)。此模式使用增量更新 (IU) 标记运行并发 GC。这种标记模式是SATB 模式的镜像:通过“新”对象拦截写入和标记。这可能会使标记不那么保守,尤其是在访问弱引用方面。
  3. passive(诊断)。此模式运行 stop-the-world GC。此模式用于功能测试,但有时它对于用 GC 屏障平分性能异常或计算应用程序中的实际实时数据大小很有用。

7.2 基本配置

基本配置和命令行选项:

  • -Xlog:gc (自 JDK 9 起)或 -verbose:gc (至 JDK 8)将打印各个 GC 计时。
  • -Xlog:gc+ergo(自 JDK 9 起)或 -XX:+PrintGCDetails(至 JDK 8)或 将打印启发式决策,这可能会揭示异常值(如果有)。
  • -Xlog:gc+stats (自 JDK 9 起)或 -verbose:gc (至 JDK 8)将在运行结束时打印关于雪兰多内部计时的汇总表。

在启用日志记录的情况下运行几乎总是一个好主意。这个汇总表传达了有关 GC 性能的重要信息,我们几乎不可避免地会在性能错误报告中要求提供。启发式日志对于找出 GC 异常值很有用。

其他推荐的 JVM 选项是:

  • -XX:+AlwaysPreTouch:将堆页面提交到内存有助于减少延迟中断
  • -Xms-Xmx: 使用 -Xms = -Xmx 使堆不可调整大小,减少堆管理的问题。结合AlwaysPreTouch-Xms = -Xmx 将在启动时提交所有内存,从而避免在最终使用内存时出现问题。-Xms还定义了内存未提交的低边界,因此使用 -Xms = -Xmx, 所有内存都将保持提交。也就是说,如果您想配置 Shenandoah 以减少占用空间,则建议设置较低的 -Xms。您需要决定将其设置为多低以平衡提交/取消提交开销与内存占用。在很多情况下,设置 -Xms任意低就可以了。
  • 使用大页面极大地提高了大堆的性能。有两种方式可以选择加入。-XX:+UseLargePages将启用 Hugetlbfs (Linux) 或 Windows(具有适当的权限)支持。 -XX:+UseTransparentHugePages将透明地启用它。对于透明大页面,建议将 /sys/kernel/mm/transparent_hugepage/enabled/sys/kernel/mm/transparent_hugepage/defrag 设置"madvise"。使用 AlwaysPreTouch 运行时,它还将在启动时预热减少碎片整理的开销。
  • -XX:+UseNUMA:虽然 Shenandoah 尚不明确支持 NUMA,但启用它以在多套接字主机上启用 NUMA 交错是一个好主意。与 AlwaysPreTouch 相结合,它提供了比默认的开箱即用配置更好的性能
  • -XX:-UseBiasedLocking:在无竞争(偏向)锁定吞吐量和 JVM 根据需要启用和禁用它们的安全点之间存在权衡。对于面向延迟的工作负载,关闭偏向锁定是有意义的。
  • -XX:+DisableExplicitGC: 从用户代码调用 System.gc() 强制 Shenandoah 执行额外的 GC 循环;禁用它以防止滥用 System.gc() 的代码可能是有益的。它通常不会受到影响,因为 -XX:+ExplicitGCInvokesConcurrent 默认启用,这意味着将调用并发 GC 周期,而不是 STW Full GC。

八、结论

本文解释了 Shenandoah GC 中的并发线程堆栈处理如何解决剩余垃圾收集暂停时间问题并在 JDK 17 中提供可靠的亚毫秒级垃圾收集暂停。要了解更多信息,请访问 Shenandoah GC 项目的 GitHub 存储库OpenJDK Wiki 页面

九、译者说

在原文的基础上添加了大家比较关系的 第六、七节如何使用 相关内容。笔者的 Spring cloud 微服务组件 mica 也已经适配 java17,mybatis-plus 的 pr 已发。近期还有几篇 java17 的文章会出炉,欢迎关注我!!!

原文链接: developers.redhat.com/articles/20…