跳到主要内容

JEP 376:ZGC:并发线程堆栈处理

概括

将 ZGC 线程堆栈处理从安全点转移到并发阶段。

目标

  • 从 ZGC 安全点删除线程堆栈处理。
  • 使堆栈处理变得惰性、协作、并发和增量。
  • 从 ZGC 安全点删除所有其他每线程根处理。
  • 提供一种机制,其他 HotSpot 子系统可以通过该机制延迟处理堆栈。

非目标

  • 实现非 GC 安全点操作(例如类重定义)的并发每线程处理并不是目标。

成功指标

  • 延迟改进的吞吐量成本应该是微不足道的。
  • 在典型机器上,ZGC 安全点内花费的时间应少于一毫秒。

动机

ZGC 垃圾收集器 (GC) 旨在使 HotSpot 中的 GC 暂停和可扩展性问题成为过去。到目前为止,我们已将所有随堆大小和元空间大小扩展的 GC 操作从安全点操作移至并发阶段。其中包括标记、重定位、引用处理、类卸载和大多数根处理。

GC 安全点中仍然完成的唯一活动是根处理的子集和有时间限制的标记终止操作。根包括 Java 线程堆栈和各种其他线程根。这些根是有问题的,因为它们随着线程数量的增加而扩展。由于大型机器上有很多线程,根处理就成为一个问题。

为了超越我们今天所拥有的,并满足 GC 安全点内部花费的时间不超过一毫秒的期望,即使在大型机器上,我们也必须将这种每线程处理(包括堆栈扫描)移至并发阶段。

完成这项工作后,ZGC 安全点操作中基本上不会做任何有意义的事情。

作为该项目一部分构建的基础设施最终可能会被其他项目(例如 Loom 和 JFR)使用,以统一惰性堆栈处理。

描述

_我们建议使用堆栈水印屏障_来解决堆栈扫描问题。 GC 安全点将通过翻转全局变量在逻辑上使 Java 线程堆栈无效。每个无效堆栈将同时处理,并跟踪剩余要处理的内容。当每个线程从安全点唤醒时,它会通过比较一些纪元计数器注意到其堆栈无效,因此它将安装_堆栈水印_来跟踪其堆栈扫描的状态。堆栈水印可以区分给定帧是否高于水印(假设堆栈向下增长),因此不得由 Java 线程使用,因为它可能包含过时的对象引用。

在弹出帧或遍历堆栈最后一帧以下的所有操作(例如,堆栈遍历器、返回和异常)中,挂钩会将某些堆栈本地地址与水印进行比较。 (此堆栈本地地址可以是帧指针(如果可用),也可以是编译帧的堆栈指针,其中帧指针已被优化,但帧具有相当恒定的大小。)当高于水位线时,将采用慢速路径通过更新其中的对象引用并将水印向上移动来修复一帧。为了像现在一样快速返回,堆栈水印屏障将使用稍作修改的安全点轮询。新的轮询不仅在安全点(或者实际上是线程本地握手)待决时采用缓慢的路径,而且在返回尚未修复的帧时也采用缓慢的路径。可以使用单个条件分支对编译方法进行编码。

堆栈水印的一个不变量是,给定被调用者是堆栈的最后一帧,被调用者和调用者都会被处理。为了确保这一点,当从安全点唤醒时安装堆栈水印状态时,调用者和被调用者都会被处理。被调用者已装备,因此从该被调用者返回将触发调用者的进一步处理,将装备框架移动到调用者,等等。因此,由帧展开或行走触发的处理总是发生在正在展开或行走的帧上方两帧。这简化了必须由调用者拥有但又由被调用者使用的参数的传递;调用者和被调用者框架(以及额外的堆栈参数)都可以自由访问。

Java 线程将处理继续执行所需的最小数量的帧。并发 GC 线程将处理剩余的帧,确保最终处理所有线程堆栈和其他线程根。利用堆栈水印屏障的同步将确保 Java 线程在 GC 处理帧时不会返回到帧中。

备择方案

在处理堆栈遍历器时,我们考虑了另一种解决方案,即在从堆栈加载对象引用的虚拟机上分散负载屏障。我们驳回了这一点,因为它从根本上无法保证正确处理内部指针到对象的根处理。内部指针的基指针必须始终在内部指针之后处理,并且堆栈遍历器将面临违反该不变量的风险。因此,我们选择通过堆栈遍历来处理整个帧(如果尚未处理)的方法。

测试

受这项工作影响的主要代码路径是其他测试已经在很大程度上承受压力的路径,因此使用现有测试基础设施进行压力测试应该足够了。