JEP 376: ZGC: Concurrent Thread-Stack Processing | ZGC:并发线程栈处理
摘要
将 ZGC 的线程堆栈处理从安全点移至并发阶段。
目标
- 从 ZGC 的安全点中移除线程堆栈处理。
- 使堆栈处理变为懒加载、协作式、并发和增量式。
- 从 ZGC 的安全点中移除所有其他每线程根处理。
- 提供一种机制,使其他 HotSpot 子系统能够懒加载处理堆栈。
非目标
- 不实现非 GC 安全点操作的每线程并发处理,如类重定义。
成功指标
- 改进延迟所带来的吞吐量成本应可忽略不计。
- 在典型机器上,ZGC 安全点内的耗时应少于一毫秒。
动机
ZGC 垃圾收集器(GC)旨在使 HotSpot 中的 GC 停顿和可扩展性问题成为过去。到目前为止,我们已将所有随堆大小和元空间大小扩展的 GC 操作从安全点操作移至并发阶段。这些操作包括标记、重新定位、引用处理、类卸载以及大多数根处理。
仍在 GC 安全点中执行的活动仅包括根处理的一个子集和一个时间受限的标记终止操作。这些根包括 Java 线程堆栈和各种其他线程根。这些根存在问题,因为它们随线程数量的增加而扩展。在大型机器上拥有大量线程时,根处理会成为一个问题。
为了超越当前的状况,并满足在大型机器上 GC 安全点内的耗时也不超过一毫秒的期望,我们必须将这种每线程处理(包括堆栈扫描)移至并发阶段。
完成这项工作后,ZGC 安全点操作中基本上不会执行任何重要操作。
作为此项目一部分构建的基础设施最终可能会被其他项目(如 Loom 和 JFR)使用,以统一懒加载堆栈处理。
描述
我们提议使用 堆栈水印屏障 来解决堆栈扫描问题。GC 安全点将通过翻转一个全局变量来在逻辑上使 Java 线程堆栈失效。每个失效的堆栈都将被并发处理,同时跟踪剩余待处理的内容。当每个线程从安全点唤醒时,它会通过比较一些周期计数器来发现其堆栈已失效,因此它将安装一个 堆栈水印 来跟踪其堆栈扫描的状态。堆栈水印使得可以区分给定帧是否位于水印之上(假设堆栈向下增长),因此 Java 线程不得使用它,因为它可能包含过时的对象引用。
在所有弹出帧或遍历到堆栈最后一帧以下(例如,堆栈遍历器、返回和异常)的操作中,钩子将比较一些堆栈本地地址与水印。(此堆栈本地地址可能是帧指针(如果可用),或者是编译帧的堆栈指针,其中帧指针被优化掉但帧具有相对恒定的大小。)当位于水印之上时,将采用慢路径来修复一个帧,方法是更新其中的对象引用并将水印向上移动。为了使返回操作像现在一样快,堆栈水印屏障将使用稍微修改过的安全点轮询。新的轮询不仅在安全点(或确实是线程本地握手)挂起时采用慢路径,而且在返回到尚未修复的帧时也采用慢路径。这可以通过带有单个条件分支的编译方法来编码。
堆栈水印的一个不变性是,给定一个被调用者作为堆栈的最后一帧,被调用者和调用者都会被处理。为了确保这一点,当从安全点唤醒时安装堆栈水印状态时,调用者和被调用者都会被处理。被调用者会被“武装”起来,以便从该被调用者返回的调用会触发对调用者的进一步处理,将“武装”的帧移动到调用者,依此类推。因此,由帧展开或遍历触发的处理总是发生在正在展开或遍历的帧上方两帧处。这简化了必须由调用者拥有但由被调用者使用的参数的传递;调用者和被调用者帧(以及额外的堆栈参数)都可以自由访问。
Java 线程将处理继续执行所需的最小帧数。并发 GC 线程将负责剩余的帧,确保所有线程堆栈和其他线程根最终都被处理。利用堆栈水印屏障的同步将确保在 GC 处理帧时,Java 线程不会返回到该帧。
替代方案
在处理堆栈遍历器时,我们考虑了在整个 VM 中散布加载屏障的替代方案,这些屏障在从堆栈加载对象引用时使用。我们放弃了这一方案,因为它从根本上不能保证内部指针到对象的根处理被正确处理。内部指针的基指针必须在内部指针之后处理,而堆栈遍历器可能会违反这一不变性。因此,我们选择了通过堆栈遍历来处理整个帧(如果尚未处理)的方法。
测试
受这项工作影响的主要代码路径是其他测试已经高度强调的路径,因此使用现有测试基础设施进行压力测试应该就足够了。