Oracle 持续加码 JFR,Profiling 值得被重新认识
本文介绍 Oracle 持续增强 JFR 的技术进展与生产实践,解析如何通过低开销持续记录 JVM 运行事件,结合 CPU 热点、GC 暂停、内存泄漏和异常信息定位问题,并利用 JDK 工具实现故障回溯,让 Profiling 成为性能优化与日常运维的重要依据。
线上接口刚刚出现一阵延迟,等排查开始,CPU 已经降下来,线程也恢复了正常。监控留下了一个尖峰,日志里没有足够的上下文,现场却已经过去了。
如果只有异常之后的快照,很难完整解释之前发生了什么。一次查询为什么突然变慢,可能需要同时查看当时的 GC、线程等待和网络活动。排查需要的是一段能够回看的运行历史。
在《The Power of JDK Flight Recorder》这场演讲中,Oracle Java 团队的 Mikael Vidstedt 反复强调 JFR 面向生产环境的设计。他在介绍采集开销时,直接点明了它的使用定位:
“We have designed it to have extremely low overhead. And as a matter of fact, the design is such that you should have it on in production. We are running with this on in production.” “我们在设计 JFR 时,就希望它具有极低的开销。事实上,它的设计目标就是让你在生产环境中启用它。我们自己也在生产环境中使用它。”
——Mikael Vidstedt

这个视角值得重新认识:Profiling 能够持续为性能分析提供方法级证据,而 JDK Flight Recorder(JFR)进一步把线程、GC、I/O 和业务事件放进同一份记录。它的价值,也因此延伸到了故障发生之前。
重新认识 JFR 的生产价值
这种投入既体现在技术分享中,也体现在 JDK 的演进里。Oracle 在2025 年 9 月发布 Java 25时,介绍了三项 JFR 增强:实验性的 CPU-Time Profiling、Cooperative Sampling,以及 Method Timing & Tracing,分别涉及 CPU 时间分析、采样稳定性和方法计时与跟踪。
其中,JDK 25 的 CPU-Time Profiling 在该版本属于实验性功能,面向 Linux;协作式采样则改进了线程栈采样机制。理解这些能力时,需要同时看版本、平台和功能状态。
2026 年 8 月 11 日,Inside Java 发布了这场面向 JFR 的专题分享,重点覆盖持续观测和生产问题分析。同年 9 月 15 日,Oracle 在JDK 27 发布说明中继续介绍 JFR 进程内数据脱敏,使命令行参数、环境变量初始值和系统属性初始值能够在离开进程前被脱敏。
从采样机制到诊断数据的使用边界,JFR 正在围绕实际运行需求持续增强。对应用团队来说,更直接的启发是:可以把运行时记录纳入日常排查流程,为偶发问题提前保留证据。
演讲从一段支持工程师的经历谈起。当时团队已经知道 JVM 内部掌握了许多运行信息:哪些代码正在执行,编译器做了什么,垃圾回收发生在何时。但当客户出现问题,这些信息很难完整地带出来。
航空业的飞行记录仪提供了一个思路:持续记录运行状态,真正需要调查时,再取出历史数据。JFR 的起点正是让 JVM 中已经发生的事情,能够成为之后可以分析的证据。
事件进入缓冲区后,由后台机制归集;按照录制配置,数据保存在内存和磁盘仓库中。导出操作将可用的数据写成独立录制文件,供命令行工具、图形工具或程序离线分析。

这种工作方式适合调查已经恢复的瞬时问题。例如,接口延迟上升后再导出最近一段录制,可以继续检查相近时间内是否出现 GC 暂停、线程竞争或 I/O 等待,再结合业务信息判断原因。
保留窗口必须与运维流程匹配。如果数据已经轮转淘汰,导出也无法恢复。需要保留多久,应结合异常发现时间、处理响应速度和可接受的存储规模决定。
理解事件记录与开销边界
JFR 是一个基于事件的记录框架。事件可以包含类型、时间戳、时长、线程、调用栈,以及事件专属字段;具体包含哪些内容,由事件定义和配置决定。
一次 GC 可以带有暂停信息,一次 I/O 可以带有读写量和持续时间,自定义业务事件则可以记录任务类型或处理阶段。调用栈把事件连接到执行路径,使分析能够继续追踪到方法和源码。

演讲列举了环境、线程执行、文件与网络 I/O、GC、JIT 编译、类加载和异常等事件,也提到了 TLS 握手、证书等信息。它们提供了不同层次的观察入口,实际采集范围仍取决于 JDK 版本与事件设置。
录制文件还包含事件定义及相关元数据,分析端能够据此理解字段、单位和类型。紧凑的二进制格式与信息复用,也使详细数据能够以相对紧凑的形式保存。
火焰图适合展示调用栈和资源权重,事件记录则可以继续展开时间、线程及其他上下文。选择哪一种视图,应由正在调查的问题决定。
持续启用首先需要控制成本。演讲介绍了 JFR 的几项设计:复用 JVM 已掌握的信息;让事件写入尽量走线程本地缓冲区;由后台完成归集;在文件中复用栈、字符串和元数据,减少重复存储。
配置同样重要。对于支持相应设置的事件,可以按类型启用或关闭、设置持续时间阈值,或通过限流控制数据量。调查锁竞争时,更关注等待明显的情况,能够把采集资源集中到有诊断价值的信息上。
介绍默认配置时,讲者提到通常低于 1% 的开销目标,也在问答中为这个数字补充了边界:
“With the default configuration, we're really trying to uphold that less than 1% guarantee. It's a soft guarantee, to be clear.” “使用默认配置时,我们确实在努力把开销控制在 1% 以下。需要说清楚的是,这是一个软性保证。”
——Mikael Vidstedt
这个数字应放在默认配置的设计与测量背景下理解。更深的调用栈、更多事件和更高采样强度都可能增加成本,实际应用仍需结合负载测量。
JDK 内置的 default.jfc 适合以较低开销持续记录,profile.jfc 提供更多诊断信息,适合需要细查时使用。定制配置可以从副本开始,或用 jfr configure 生成,避免直接修改 JDK 自带模板。
另一个容易忽略的细节是多个录制共享底层采集机制。临时启动更详细的录制时,JVM 需要满足这些配置的整体采集需求,因此仍应评估应用的总体开销。
JFR 没有一个能替业务团队判定所有性能影响的内置读数。CPU、延迟、吞吐量和 GC 暂停是否可接受,需要在实际负载下做对照。
四类故障的 JFR 排查路径
演讲中的案例是为展示工具能力而构造的演示。它们的数值用于解释排查路径,实际系统需要结合自身负载、配置与业务目标验证。

CPU 繁忙时,继续找到热点方法
第一个场景中,应用执行缓慢,CPU 负载视图显示机器利用率较高。进一步查看热点方法,一个业务方法在热点采样中的占比约为 30%,为源码检查提供了具体入口。
可迁移的方法是先区分机器繁忙与目标 JVM 繁忙,再查看执行热点及业务调用关系。采样占比可以帮助确定优先级,但不能直接理解为该方法消耗了相同比例的整机 CPU。
CPU 不高时,检查暂停与等待
另一个延迟敏感的演示中,CPU 负载并不突出,GC 视图却显示最大暂停约为 206 ms。继续查看 JVM 配置,发现应用使用了 Serial GC;演示改用 ZGC 后,观察到的暂停降到了微秒量级。
这里值得迁移的是证据顺序:从延迟现象进入 GC 暂停,再核对收集器与运行配置。选择收集器仍需要平衡吞吐量、资源和延迟目标,演示结果不能直接作为其他应用的收益预期。
内存增长时,把对象年龄与引用关系连起来
内存泄漏演示同时使用了对象存活信息、到 GC Roots 的引用路径,以及分配调用栈。讲者结合约 20 分钟的运行时间,选择中间时段分配、持续存活的对象继续调查。
引用链指向登录任务相关对象和一个异常增长的 HashMap,其底层 Node 数组规模约为 200 万个槽位。这是值得继续检查的线索;数组容量也需要与实际条目数量及业务预期区分。
最终发现,代码在处理结束后创建了一个新对象尝试移除原有记录,而相关类型没有实现匹配业务语义的 equals() 和 hashCode(),移除操作未能匹配原对象,记录不断保留。
定位的关键是把对象保留与源码行为连接起来。对象年龄只能帮助筛选候选,不能单独证明泄漏。采集 GC Roots 路径还会增加成本,应在怀疑内存泄漏时针对性启用。
应用无响应时,从异常消息追查配置
最后一个演示中,异常统计显示大量 SQLException,进一步按异常消息查看,出现了 No suitable driver found。排查范围因此缩小到 JDBC 驱动、加载情况及连接 URL 等配置。
这个例子也提醒我们,性能或可用性问题可能来自应用没有正确完成某个环节。异常类型、消息、线程与调用栈能够帮助判断,接下来应该检查代码还是依赖与配置。
用 JDK 工具完成录制与分析
不必等到所有工具链都齐备,才能开始留存证据。JDK 提供启动参数、jcmd 和 jfr 等入口;应用启动时未开启录制,也可以在运行中通过 jcmd 启动。
以下为 Linux 环境的命令示例,12345 需替换为实际 Java 进程 PID,并确认具备本机 Attach 权限及可写的输出目录。jfr view 需要 JDK 21 或更高版本,具体视图以所用 JDK 帮助为准。
# 查找 Java 进程,确认实际 PID
jcmd -l
# 持续录制:保留有限历史
jcmd 12345 JFR.start name=baseline settings=default disk=true maxage=30m maxsize=256m
# 检查当前录制
jcmd 12345 JFR.check
# 在需要分析时导出,原录制继续运行
jcmd 12345 JFR.dump name=baseline filename=/var/tmp/baseline.jfr
30 分钟和 256 MiB 是示例保留限制,数据可能因容量先达到上限而更早被淘汰。导出成功意味着录制文件已生成;它不代表所有诊断所需事件都已经启用。
default 配置无法保证覆盖每一种排障需求。需要更多信息时,可以针对性启用事件,或在评估开销后启动一段短时的 profile 录制,例如:
jcmd 12345 JFR.start name=investigation settings=profile duration=2m filename=/var/tmp/investigation.jfr
拿到录制文件后,先确认记录范围及事件种类,再选择聚合视图。下面几条命令分别用于概要、CPU 负载、热点方法和 GC 暂停分析:
jfr summary /var/tmp/investigation.jfr
jfr view cpu-load /var/tmp/investigation.jfr
jfr view hot-methods /var/tmp/investigation.jfr
jfr view gc-pauses /var/tmp/investigation.jfr
视图没有数据,可能是相关事件未开启、未满足阈值或采集窗口没有覆盖问题,不能直接解释为系统正常。已经错过的事件,也无法通过更换分析视图补采。
jfr print 可以查看具体事件,JDK Mission Control(JMC)则提供图形化分析入口。排查结束后,如果需要停止名为 baseline 的录制,可执行 jcmd 12345 JFR.stop name=baseline;停止持续录制也会结束后续历史积累。
把 JFR 纳入日常运维
演讲还展示了另一种使用方式:通过 API 持续消费事件。RecordingStream 可以在进程内启动录制并处理事件,EventStream.openRepository() 可以读取本机已有录制仓库,RemoteRecordingStream 则支持通过 JMX 连接消费远程事件。
这使得异常识别与录制导出可以被编排成流程。例如,先持续保留基础事件,当外部告警或自定义检测逻辑发现异常,再触发导出,保存问题发生前后的数据。触发条件与导出联动需要相应逻辑,不能假设开启录制后所有异常都会自动归档。
应用还可以定义业务事件,将处理阶段、任务类型或业务耗时与 JVM 事件放在同一份录制中。当一项业务操作变慢,就能够结合时间与线程上下文继续调查 GC、竞争或 I/O 活动。时间接近提供了关联线索,具体因果仍需要进一步验证。
在多实例环境中,可以围绕服务、实例、发布版本和时间统一归档录制,并按业务需要关联指标、日志与调用链。事件消费和分析本身也会消耗资源,扩展到更多实例之前,同样需要测量整体成本。
对于尚未建立 JFR 使用习惯的团队,可以先选择一个关键服务,在少量实例上验证完整流程:
- 以低开销配置建立基线,明确事件范围和历史保留限制。
- 验证异常发生后谁来触发导出,录制保存到哪里,能否被后续分析。
- 结合问题选择更多事件或短时详细录制,并观察业务性能影响。
- 优化前后保持负载条件可比,保留录制、配置与业务指标作为对照。
Profiling 值得重新认识的价值,在于让代码的运行行为成为可检查、可讨论、可复查的证据。JFR 把这种能力嵌入 JDK,并通过持续记录为故障回溯提供基础。讲者用一个形象的比喻概括了这种价值:
“It's a time machine that allows you to go back and see what led up to that and what else happened at the same time.” “它就像一台时光机,让你回到过去,看到问题发生之前的过程,以及同一时间还发生了什么。”
——Mikael Vidstedt
下次接口只慢了一阵、现场已经恢复时,排查可以从一段已有的运行历史开始。先把证据留下来,再沿着事件、线程和调用路径接近原因,性能优化就有了更可靠的起点。
资料与延伸阅读
演讲观点与案例来自 Mikael Vidstedt 的《The Power of JDK Flight Recorder》。录制与命令参数可对照以下官方资料,具体能力以实际 JDK 版本为准。
演讲官方发布页:The Power of JDK Flight Recorder(Mikael Vidstedt,2026-08-11)


