JEP 518:JFR 协作式采样
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 事件。像 jfr 和 JDK 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 时间剖析) 的实现利用了这里引入的机制。