JEP 518:JFR 协作式采样

原文:JEP 518- JFR Cooperative Sampling
作者:
日期:2025-10-28

负责人马库斯·格伦隆德(Markus Grönlund)
类型特性
范围实现
状态已完成 / 已交付
版本25
组件HotSpot / JFR
讨论地址hotspot - jfr - dev@openjdk.org
工作量M
持续时间M
审核人埃里克·加林(Erik Gahlin)、弗拉基米尔·科兹洛夫(Vladimir Kozlov)
批准人弗拉基米尔·科兹洛夫(Vladimir Kozlov)
创建时间2025/02/19 14:12
更新时间2025/06/10 16:31
问题编号8350338

摘要

通过仅在安全点遍历调用栈,同时尽量减少安全点偏差,来提高 JDK 飞行记录器(JFR)在异步采样 Java 线程栈时的稳定性。

动机

一个正在运行的程序会消耗诸如内存、CPU 周期和经过时间等计算资源。对程序进行 剖析,就是测量程序特定元素对这些资源的消耗情况。剖析结果可能表明,例如,一个方法消耗了 20% 的资源,而另一个方法仅消耗 0.1%。

剖析有助于通过确定要优化的程序元素,使程序更高效,让开发人员更具生产力。如果不进行剖析,我们可能会去优化一个原本消耗资源很少的方法,这对程序的整体性能影响甚微,还会浪费精力。例如,将一个占用程序总执行时间 0.1% 的方法优化到运行速度快十倍,也只会使程序的执行时间减少 0.09%。

JDK 飞行记录器(JFR)是 JDK 的剖析和监控工具。JFR 的核心是一种低开销机制,用于记录由 HotSpot JVM 或程序代码发出的事件。一些事件,如加载类,在每次动作发生时都会被记录。其他事件,比如用于剖析的事件,是通过在程序消耗资源时对其活动进行统计采样来记录的。各种 JFR 事件可以开启或关闭,这样在开发过程中可以进行更详细、开销更高的信息收集,而在生产环境中可以进行不太详细、开销较低的信息收集。

JFR 可以创建一个执行时间剖析报告,展示哪些程序元素消耗了大量的实际经过时间,即墙上时钟时间。它通过以固定间隔(比如 20 毫秒)对程序线程的执行栈进行采样来实现这一点。每个样本都会生成一个包含栈跟踪信息的 JFR 事件。像 jfrJDK Mission Control 这样的工具可以将这样的事件流汇总成文本或图形化的剖析报告。

为了生成程序线程的栈跟踪信息,JFR 的采样线程必须暂停目标线程并解析栈上的调用帧。HotSpot JVM 维护元数据来指导栈帧的解析,但只有当线程在称为 安全点 的明确定义的代码位置暂停时,该元数据才有效。然而,如果我们仅在安全点采样栈,那么我们可能会遇到 安全点偏差问题:我们有失去准确性的风险,因为频繁执行的一段代码可能离安全点很远。安全点偏差问题是 众所周知的,并且已经得到了 深入研究

为了避免安全点偏差问题,JFR 异步采样程序线程的栈,在线程不一定处于安全点的代码位置暂停线程并解析它们的栈。由于解析栈帧的元数据在非安全点不能保证有效,JFR 的采样线程使用启发式方法来生成栈跟踪信息。

不幸的是,这些栈解析启发式方法效率低下,更糟糕的是,当它们的结果不正确时,可能会导致 JVM 崩溃。JFR 试图通过特定于平台的崩溃保护机制来防止此类崩溃,但在存在诸如类卸载等并发活动时,这些机制可能会失效。

描述

我们重新设计了 JFR 的采样机制,以避免依赖有风险的栈解析启发式方法。相反,我们仅在安全点解析线程栈。

为了避免安全点偏差问题,我们采用协作式采样。当到了采样时间,JFR 的采样线程仍然会暂停目标线程。然而,它不是尝试解析栈,而是仅在一个 采样请求 中记录目标的程序计数器和栈指针,并将其追加到内部线程本地队列中。然后,它安排目标线程在其下一个安全点停止,之后恢复线程运行。

目标线程正常运行直到下一个安全点。此时,安全点处理代码会检查队列。如果发现任何采样请求,那么对于每个请求,它会重建栈跟踪信息,对安全点偏差进行调整,并发出一个 JFR 执行时间采样事件。

除了安全之外,这种方法还有其他几个优点:

  • 创建采样请求几乎不需要什么工作,并且可以响应硬件事件或在信号处理程序内部完成。
  • 创建栈跟踪信息并发出事件的代码更简单。例如,当它在目标线程上运行时可以动态分配内存,而在采样线程上运行时则无法做到这一点。
  • 采样线程需要做的工作更少,因为它不需要运行启发式方法,从而提高了可扩展性。

当目标线程运行 Java 代码(无论是解释执行还是编译执行)时,这种方法效果良好,但当目标线程运行本地代码时则不然。在这种情况下,我们继续使用现有的方法。

未来工作

我们的新方法并不能完全避免安全点偏差。在某些情况下,例如在 HotSpot JVM 具有内在实现的方法内部进行采样时,可能无法解析栈。在这些情况下,记录的栈跟踪信息将反映最后一个 Java 栈帧,从而引入一些偏差。我们打算在未来的工作中解决这个问题。

替代方案

HotSpot JVM 确实有一个现有的内部但不受支持的机制 AsyncGetCallTrace,一些第三方工具会使用它。不幸的是,这个机制依赖于与 JFR 现在使用的相同类型的有风险的栈解析启发式方法,但没有任何崩溃保护,因此风险更大。另一个缺点是它基于 POSIX 的 SIGPROF 信号,而 Windows 上没有等效的信号。

测试

这严格来说是一个实现上的变化。现有的单元测试、集成测试和压力测试就足够了。

依赖关系

JEP 509(JFR CPU 时间剖析) 的实现利用了这里引入的机制。