JEP 509:JFR CPU 时间剖析(实验性)

原文:JEP 509- JFR CPU-Time Profiling (Experimental)
作者:
日期:2025-10-26

作者雅罗斯拉夫·巴乔里克(Jaroslav Bachorík)、约翰内斯·贝希伯格(Johannes Bechberger)、罗恩·普雷斯勒(Ron Pressler)
所有者约翰内斯·贝希伯格(Johannes Bechberger)
类型特性
范围JDK
状态已关闭 / 已交付
发布版本25
组件hotspot/jfr
讨论组hotspot - jfr - dev@openjdk.org
工作量中等
时长
审核人马库斯·格伦隆德(Markus Grönlund)
批准人弗拉基米尔·科兹洛夫(Vladimir Kozlov)
创建时间2024 年 8 月 4 日 10:34
更新时间2025 年 7 月 31 日 10:19
问题编号8337789

摘要

增强 JDK 飞行记录器(JFR),以便在 Linux 上捕获更准确的 CPU 时间剖析信息。这是一项实验性特性。

动机

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

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

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

通常进行剖析的两个重要资源是堆内存和 CPU。

CPU 剖析展示了不同方法消耗的 CPU 周期的相对量。这不一定与其消耗的总执行时间的相对量相关。例如,一个对数组进行排序的方法,它的所有时间都花在 CPU 上,其执行时间与它消耗的 CPU 周期数相对应。相比之下,一个从网络套接字读取数据的方法,可能大部分时间都在闲置等待字节通过网络到达。在它消耗的时间中,只有一小部分花在 CPU 上。然而,即使对于执行大量 IO 操作的服务器应用程序,CPU 剖析也很重要,因为在高负载下,此类应用程序的吞吐量可能受 CPU 使用率的限制。

JFR 对堆分配剖析提供了良好的支持,但对 CPU 剖析的支持有所欠缺。JFR 通过一种称为“执行采样器”的机制对 CPU 剖析进行近似。当这个机制开启时,JFR 会在固定的时间间隔(例如每 20 毫秒)获取 Java 线程的栈跟踪(一个“样本”),并在 JFR 事件中发出。包括 JFR 内置 “视图” 在内的合适工具,可以将这些事件汇总为文本或图形化的剖析结果。由于阻塞线程不消耗 CPU,所以只对正在运行而非等待某些事件的线程进行采样。

这种机制在所有操作系统平台上都能工作,但存在一些缺陷:

  • 它只对当前正在执行 Java 代码的线程进行采样,而不对从 Java 代码调用的本地代码对应的线程进行采样。
  • 当它试图获取样本时,可能由于技术原因而失败,并且它不会报告此类错过样本的数量。
  • 它在每个时间间隔仅选择线程的一个子集进行采样。

因此,得到的剖析结果可能不准确,无法反映实际的 CPU 使用情况。在相对较短的时间段(例如一分钟)内收集样本时,不准确的情况可能更严重。

CPU 时间剖析

Linux 内核从 2.6.12 版本开始通过一个定时器增加了准确测量 CPU 周期消耗的能力,该定时器按固定的 CPU 时间间隔而非固定的实际经过时间间隔发出信号。Linux 上的大多数剖析器都使用这种机制来生成 CPU 时间剖析结果。

一些流行的第三方 Java 工具,包括 async - profiler,使用 Linux 的 CPU 定时器来生成 Java 程序的 CPU 时间剖析结果。然而,为了实现这一点,此类工具通过不受支持的内部接口与 Java 运行时进行交互。这本质上是不安全的,可能导致进程崩溃。

我们应该增强 JFR,使其使用 Linux 内核的 CPU 定时器,安全地生成更准确的 Java 程序 CPU 时间剖析结果。特别是,这种内核机制将使 JFR 能够正确跟踪 Java 程序在运行本地代码时消耗的 CPU 周期。

更优的 CPU 剖析结果将帮助众多在 Linux 上部署 Java 应用程序的开发人员,使这些应用程序更高效。

描述

我们仅在 Linux 系统上为 JFR 添加 CPU 时间剖析功能。目前该功能处于实验阶段,以便我们在将其正式确定之前,根据实际经验进行完善。未来,我们可能会在其他平台上为 JFR 添加 CPU 时间剖析功能。

JFR 将使用 Linux 的 CPU 定时器机制,按固定的 CPU 时间间隔对每个运行 Java 代码的线程栈进行采样。每个这样的样本会记录在一种新的事件类型 jdk.CPUTimeSample 中。该事件默认未启用。

此事件与现有的用于执行时间采样的 jdk.ExecutionSample 事件类似。启用 CPU 时间事件不会以任何方式影响执行时间事件,因此可以同时收集这两种事件。

我们可以在启动时开始的记录中像这样启用新事件:

$ java -XX:StartFlightRecording=jdk.CPUTimeSample#enabled=true,filename=profile.jfr ...
  

示例

假设有一个名为 HttpRequests 的程序,它有两个线程,每个线程都执行 HTTP 请求。一个线程运行 tenFastRequests 方法,该方法依次向一个响应时间为 10 毫秒的 HTTP 端点发出十个请求;另一个线程运行 oneSlowRequest 方法,该方法向一个响应时间为 100 毫秒的端点发出单个请求。这两个方法的平均延迟应该大致相同,因此执行它们所花费的总时间也应该大致相同。

然而,我们预计 tenFastRequestsoneSlowRequest 占用更多的 CPU 时间,因为十轮创建请求和处理响应比仅一轮需要更多的 CPU 周期。如果我们的程序在高负载下成为 CPU 瓶颈,那么应该优化 tenFastRequest,并且剖析结果应该反映这一点。

当我们使用 JDK 任务控制工具(JMC),基于新的 CPU 时间样本记录生成程序的图形化 火焰图 剖析时,确实可以清楚地看到应用程序几乎所有的 CPU 周期都花在了 tenFastRequests 中:

CPU 时间火焰图

我们可以看到这两个方法之间的明显差异(与 tenFastRequests 相比,oneSlowRequest 消耗的 CPU 非常少,以至于在上述剖析中没有显示出来),即使大部分处理是在本地代码中完成的。请注意,本地函数没有直接显示,但它们的 CPU 消耗被正确归因于调用它们的 Java 方法,例如 implFlush

可以通过以下方式获取热点 CPU 方法(即在自身主体而非对其他方法的调用中消耗大量 CPU 周期的方法)的文本剖析:

$ jfr view cpu - time - hot - methods profile.jfr
  

然而,在这个特定示例中,输出不像火焰图那样有用。

事件详细信息

针对特定栈跟踪的 jdk.CPUTimeSample 事件具有以下字段:

  • startTime —— 遍历栈之前的 CPU 时间,
  • eventThread —— 被采样线程的标识,
  • stacktrace —— 栈跟踪信息,如果采样器未能遍历栈则为 null
  • samplingPeriod —— 获取样本时的采样周期,以及
  • biased —— 该样本是否可能是 安全点偏差的

可以通过事件的 throttle 属性以两种方式设置此事件的采样率:

  • 作为时间段:throttle=10ms 表示每个平台线程消耗 10 毫秒 CPU 时间后生成一个样本事件。
  • 作为总体速率:throttle=500/s 表示每秒生成 500 个样本事件,均匀分布在所有平台线程上。

默认的节流设置是 500/s,而在 JDK 中包含的剖析配置 profile.jfc 中,它是 10ms。更高的速率会以增加程序采样开销为代价,生成更准确精确的剖析。

另一个新事件 jdk.CPUTimeSamplesLost,当由于实现限制(例如内部数据结构已满)而丢失样本时会发出。其 lostSamples 字段包含上一轮采样中丢弃的样本数量。此事件确保事件流作为一个整体能够正确反映程序的总体 CPU 活动。它默认启用,但仅在 jdk.CPUTimeSample 启用时才发出。

由于此功能处于实验阶段,这些 JFR 事件在其 JFR 事件类型 上使用 @Experimental 注解进行标记。与 HotSpot JVM 的其他实验性特性不同,这些事件无需通过诸如 -XX:+UnlockExperimentalVMOptions 之类的命令行选项进行特殊启用。

生产环境中的剖析

持续剖析(即在生产环境中运行的应用程序在其整个生命周期内进行剖析)越来越受欢迎。选择具有足够低开销的 throttle 设置,使得在生产环境中持续收集 CPU 时间剖析信息变得可行。

如果在生产环境中无法进行持续的 CPU 时间剖析,另一种选择是偶尔收集 CPU 剖析。首先,创建一个合适的 JFR 配置文件:

$ jfr configure --input profile.jfc --output /tmp/cpu_profile.jfc \
  jdk.CPUTimeSample#enabled=true jdk.CPUTimeSample#throttle=20ms
  

然后使用 jcmd 工具,使用该配置为正在运行的程序启动记录:

$ jcmd <pid> JFR.start settings=/tmp/cpu_profile.jfc duration=4m
  

这里记录在四分钟后停止,但我们可以选择其他时长,或者过一会儿通过 jcmd <pid> JFR.stop 手动停止记录。

与每个 JFR 事件一样,jdk.CPUTimeSample 事件可以 在记录时进行流式传输,甚至 通过网络传输给远程使用者 以进行在线分析。

替代方案

我们可以通过为此目的添加安全且受支持的本地 HotSpot API,使外部工具能够使用 Linux CPU 时间采样器。然而,这样做会暴露运行时的内部细节,这将使 JDK 更难演进。与在 JDK 中直接实现该功能相比,这种方法效率也更低,因此不太适合在生产环境中进行剖析。

依赖关系

此功能的实现利用了 JEP 518 引入的协作采样机制。