Java Flight Recorder(简称 JFR)是 OpenJDK 从 11 起免费内置的事件记录框架,能以低于 1% 的采样开销,持续收集 JVM 的 CPU、内存、GC、锁、IO 与线程等运行时数据。它像飞机上的”黑匣子”,把生产环境最难复现的性能问题原样录下来,让你在事后用 JMC 慢慢回放、精准定位,而不是靠猜。
一、JFR 是什么,为什么开销这么低
传统 APM 或采样型 Profiler 往往在每次方法进入/退出都插桩,开销随调用量线性增长;而 JFR 采用事件驱动 + 环形缓冲的设计。JVM 内部在关键路径上埋了上千个事件点(如 GC 开始/结束、线程阻塞、对象分配、文件读写、套接字收发),事件仅以极小的二进制结构写入进程内的一块固定大小内存,满了就覆盖最旧的。因为是 C++ 层面直接写入内存,不经过 Java 堆,也不加锁,所以常驻录制的开销可以压到 1% 以内。
录制产物是一个扩展名为 .jfr 的二进制文件,基于 XML 定义的事件 schema,可被 Java Mission Control(JMC)可视化,也能用官方 jfr 命令行工具或 jdk.jfr 的流式 API 程序化解析。这正是它比 thread dump、heap dump 更适合”持续观测”的原因——前者是某一瞬间的快照,JFR 是一段时间的连续录像。
二、三种启动录制的方式
2.1 启动参数:跟随 JVM 一起开
适合需要”从启动那一刻起就录”的场景,比如复现一个启动慢的问题。下面这段参数会在应用启动后延迟 20 秒、录制 60 秒、以 profile 模板写入文件:
java -XX:+FlightRecorder -XX:StartFlightRecording=delay=20s,duration=60s,filename=recording.jfr,settings=profile -jar myapp.jar
settings=profile 是比默认 default 更细的模板,会开启方法采样、对象分配、锁竞争等更重的事件;default 则只保留开销极低的基础事件,可 7×24 常驻。
2.2 jcmd:运行时动态开关(最常用)
生产环境最推荐的方式:先不录,等问题出现再动态开启,录完即停。先拿到 Java 进程 PID:
jps -l
# 输出类似:12345 com.example.MyApp
# 开启一个 60 秒的录制,存到 /tmp/recording.jfr
jcmd 12345 JFR.start name=myrecording duration=60s filename=/tmp/recording.jfr settings=profile
# 中途也可以手动 dump 当前缓冲(无需等 duration 结束)
jcmd 12345 JFR.dump name=myrecording filename=/tmp/recording_now.jfr
# 停止并落盘
jcmd 12345 JFR.stop name=myrecording
# 查看正在进行的录制
jcmd 12345 JFR.check
2.3 代码内 API:把录制嵌进业务
当你想”在接口超时瞬间自动存一段录像”时,可以用 jdk.jfr 的 API 在代码里精确控制。下面在捕获异常时 dump 最近 120 秒的环形缓冲:
import jdk.jfr.Configuration;
import jdk.jfr.FlightRecorder;
import jdk.jfr.Recording;
public class JfrGuard {
private static final Recording rec = new Recording(
Configuration.getConfiguration("profile"));
static {
rec.setMaxAge(java.time.Duration.ofSeconds(120)); // 环形缓冲只留 120s
rec.start();
}
public static void dumpOnIncident(String tag) {
try {
rec.dump(java.nio.file.Path.of("/tmp/incident-" + tag + ".jfr"));
} catch (Exception e) {
// 录制失败不应影响主流程
}
}
}
三、常用事件类型与配置模板
JFR 的事件有几百种,但日常定位性能问题,下面这几类最关键。profile 模板默认会开启大部分:
| 事件 | 能回答的问题 | 开销 |
|---|---|---|
| CPULoad / Method Profiling | 热点方法在哪里、CPU 时间花在哪 | 中 |
| GarbageCollection / GCPhasePause | 哪次 GC 停顿最长、暂停来源 | 低 |
| Object Allocation in New TLD | 谁在疯狂创建临时对象 | 中 |
| Java Monitor Wait / Blocked | 锁竞争、线程阻塞在哪把锁 | 中 |
| File Read / File Write / Socket Read | 慢 IO、磁盘或网络瓶颈 | 低 |
| Thread Dump / Thread Sleep | 线程状态分布、死锁线索 | 低 |
四、jcmd 实战:采集一次生产慢请求
假设监控告警说接口 P99 从 200ms 飙到 2s。先在告警触发时立刻开录,时长覆盖问题窗口:
# 开一个 120 秒的录制
jcmd 12345 JFR.start name=slowapi duration=120s filename=/tmp/slowapi.jfr settings=profile
# 等窗口过去后,确认文件已生成
ls -lh /tmp/slowapi.jfr
# 把文件拉到本地,用 JMC 打开分析
scp server:/tmp/slowapi.jfr ./slowapi.jfr
把 .jfr 拖进 JMC(Oracle 或 Eclipse 版均可,需装 Flight Recorder 插件)后,重点看三个视图:Method Profiling 的火焰图看 CPU 热点,GC 页看停顿分布,Lock Instances 看被阻塞最久的锁。
五、程序化解析 .jfr:把录像变成告警
不想每次人工开 JMC?JDK 11+ 提供了 jfr 命令行和流式 API,可以把录像直接喂给自动化分析。下面用 jfr 工具做摘要统计:
# 打印录像中的事件类型与数量
jfr summary slowapi.jfr
# 筛选 GC 停顿超过 200ms 的事件
jfr print --events "jdk.GCPhasePause" --max-size 5M slowapi.jfr
# 用 jdk.jfr 流式 API 在 Java 里消费事件
jfr stream slowapi.jfr {
if (event instanceof jdk.GCPhasePause pause && pause.duration > 200_000_000) {
System.out.println("长停顿: " + pause.duration / 1_000_000 + " ms, phase=" + pause.name);
}
}
把这段逻辑放进定时任务,就能对历史录像做”长 GC 停顿 TOP N””锁等待超阈值”等自动巡检,比单纯看 Grafana 折线更下钻。
六、实战案例:三类典型瓶颈怎么看
结合前面提到的内链,JFR 的定位结论常常要和 JVM 调优、虚拟线程等知识交叉印证:
| 现象 | 优先看 JFR 事件 | 常见根因 |
|---|---|---|
| CPU 飙高 | Method Profiling 火焰图 | 序列化/正则/循环热点(参考 JVM 调优) |
| 偶发卡顿 | GCPhasePause / Safepoint | Full GC、偏向锁撤销 |
| 吞吐上不去 | Java Monitor Blocked | 大锁竞争、可换成 AQS 或无锁结构 |
| 线程数暴涨 | Thread Start / Thread Park | 线程池配置不当(可结合 虚拟线程 优化) |
| 磁盘/网络慢 | File/Socket Read Write | 同步阻塞 IO、未用缓冲流 |
七、持续录制 + 异常自动 dump
对线上核心服务,建议常驻一个低开销的 default 录制(环形缓冲,不落盘),一旦监控触发异常,再 JFR.dump 把最近几分钟的录像存下来。这样既不影响性能,又能保留”出事前后”的现场:
# 常驻一个 180s 环形缓冲(不写文件,纯内存)
jcmd 12345 JFR.start name=continuous maxage=180s settings=default
# 告警时立即 dump 最近 180s
jcmd 12345 JFR.dump name=continuous filename=/tmp/incident.jfr
八、JFR 与 AsyncProfiler / APM 怎么选
| 工具 | 优势 | 局限 | 适用 |
|---|---|---|---|
| JFR | 内置、开销低、事件维度全、可常驻 | 默认采样精度略低于 AsyncProfiler | 生产持续观测、事后回放 |
| AsyncProfiler | 火焰图精度高、支持 perf 联动 | 需额外部署、不适合 7×24 | 临时深度火焰图 |
| APM(SkyWalking 等) | 全链路、业务视角 | 看不到 JVM 内部事件 | 服务拓扑与链路追踪 |
九、踩坑与最佳实践
- profile 模板别 7×24 开:它开启对象分配采样,长开会累积内存与少量 CPU,常驻请用 default。
- 环形缓冲要设上限:用
maxage或maxsize防止内存被录像吃光。 - JDK 版本差异:JFR 在 8u262+ 与 11+ 才免费开放,老版本需要商业授权;容器里记得用
-XX:+UnlockCommercialFeatures的等价现代写法。 - .jfr 要脱敏:录像可能含字符串常量与部分参数值,外发前确认不含密钥。
- 配合分布式链路:多实例时,把 traceId 通过
FlightRecorder.addPeriodicEvent写进自定义事件,回放时能按请求串联(涉及 分布式锁与追踪 场景时尤其有用)。
十、30 秒上手清单
记不住全部命令也没关系,照着这套走:① 拿到 PID(jps -l);② 开录(jcmd PID JFR.start name=r duration=120s filename=/tmp/r.jfr settings=profile);③ 复现问题;④ 拉文件(scp);⑤ 用 JMC 看火焰图 + GC + 锁三页;⑥ 定位热点后对照代码修复。把 JFR 当成 JVM 的”黑匣子”,绝大多数”偶发、难复现”的性能问题都会现出原形。本地调试时也可以把应用直接跑在 Docker 容器里,录制路径挂到卷上即可。




