Java Flight Recorder(JFR)是 JVM 内置的低开销性能分析器,能在生产环境以不到 1% 的额外开销,持续采集方法采样、GC、锁竞争、文件与网络 IO 等数百种事件。当线上 Java 应用出现”响应变慢””CPU 飙高””GC 停顿过长”却找不到根因时,用 JFR 给 JVM 做一次全链路性能体检,往往能把问题直接定位到具体方法甚至代码行。本文从三种启动方式讲起,带你把 JFR 真正用在生产。
一、为什么 JFR 比 GC 日志更全面
很多团队排查性能问题只盯着 GC 日志,但 GC 只是 JVM 行为的冰山一角。一次请求变慢,可能是锁竞争,可能是同步 IO 阻塞,也可能是某个热点方法自身 CPU 密集。JFR 以事件流的方式把所有这些维度记录下来,且开销极低——OpenJDK 社区与多家大厂都推荐把它作为常驻的”黑匣子”。它与 JVM 调优实战 里讲的 GC 日志互为补充:GC 日志看内存回收,JFR 看全栈行为。
二、三种启动 JFR 的方式
2.1 命令行 jcmd(最常用,无需重启)
# 查看目标 JVM 进程号
jps -l
# 启动一个 60s 的录制,采样间隔 10ms,使用详细预设
jcmd <pid> JFR.start name=myrecording duration=60s settings=profile
# 把录制结果转储到文件
jcmd <pid> JFR.dump name=myrecording filename=/tmp/rec.jfr
# 停止录制并保存
jcmd <pid> JFR.stop name=myrecording
jcmd 的好处是可以在不重启进程的前提下动态开关录制,特别适合线上临时抓现场。settings=profile 是较详细的预设,包含方法级采样;如果只是想看概况,可用 settings=default(开销更低,约 0.5%)。
2.2 JVM 启动参数(常驻录制)
java -XX:+FlightRecorder \
-XX:StartFlightRecording=disk=true,filename=/tmp/app.jfr,maxsize=200m,maxage=1h \
-jar app.jar
这种方式适合把 JFR 作为长期”黑匣子”常驻,maxage 与 maxsize 控制磁盘占用上限,环形缓冲会自动滚动覆盖旧数据,不会因为忘了停录制而把磁盘写满。
2.3 运行时 Java API(代码里按需触发)
import jdk.jfr.*;
import jdk.management.jfr.*;
// 通过 JMX 启动一次录制
FlightRecorderMXBean bean =
ManagementFactory.getPlatformMXBean(FlightRecorderMXBean.class);
long id = bean.newRecording();
Recording recording = FlightRecorder.getRecording(id);
// 只采集超过 10ms 的 Socket 读,降低噪音
recording.enable("jdk.SocketRead").withThreshold(Duration.ofMillis(10));
recording.start();
// ... 业务运行一段时间 ...
recording.dump(Path.of("/tmp/live.jfr"));
recording.stop();
在压测脚本或诊断 endpoint 里用 API 触发,能做到”出问题时自动录、录完自动 dump”,是做自愈诊断的好抓手。
三、一次真实的性能体检:抓吞吐与延迟
拿到 .jfr 文件后,最直接的办法是用 jfr 命令行工具查看摘要,再用 JDK Mission Control(JMC)做可视化火焰图。下面这条命令能聚焦最影响吞吐的两类事件:
# 只打印 GC 与锁竞争两类事件到文本
jfr print --events jdk.GarbageCollection,jdk.MonitorEnter /tmp/rec.jfr > report.txt
# 导出热点方法栈(用于生成火焰图)
jfr assemble /tmp/rec.jfr /tmp/rec_fixed.jfr
| 事件 | 说明 | 何时关注 |
|---|---|---|
| jdk.ExecutionSample | 基于采样的调用栈 | 找 CPU 热点方法 |
| jdk.MethodProfilingSample | 方法级耗时分布 | 细化热点算法 |
| jdk.GCHeapSummary | 堆内存变化曲线 | 内存增长 / 泄漏 |
| jdk.MonitorEnter | 管程(synchronized)竞争 | 锁热点 |
| jdk.SocketRead / jdk.FileRead | 同步阻塞 IO | IO 等待过长 |
结合 Java 21 虚拟线程 的使用场景,JFR 还能区分平台线程与虚拟线程的阻塞,帮助判断服务是不是被同步 IO 拖垮——这是传统线程池视角很难一眼看穿的。
四、用 JFR 定位 GC 压力与锁竞争
GC 压力大时,jdk.GarbageCollection 事件会显示每次停顿时长与原因;锁竞争则藏在 jdk.MonitorEnter 的平均等待里。一个典型信号是:吞吐没掉、延迟却抖——多半是锁或 IO,而不是 GC。把它们和 GC 日志调优 对照着看,结论更稳。
| 症状 | 高相关事件 | 处理方向 |
|---|---|---|
| 平均延迟抖、吞吐稳 | jdk.MonitorEnter 等待高 | 减小锁粒度 / 改无锁结构 |
| CPU 满、吞吐上不去 | jdk.ExecutionSample 集中某方法 | 优化热点算法 |
| 内存缓慢增长不回落 | jdk.GCHeapSummary 单调上升 | 查对象引用 / 缓存泄漏 |
| 偶发卡顿 | jdk.SocketRead 长耗时 | 加超时 / 异步化 |
五、在 Docker 与 Kubernetes 里采集 JFR
容器环境下 JVM 看到的”可用内存”常和容器 limit 不一致,容易触发 OOMKilled。通过 JAVA_TOOL_OPTIONS 注入录制参数,再把 .jfr 落到挂载卷,就能在容器外取走分析:
docker run -d --name app \
-e JAVA_TOOL_OPTIONS="-XX:+FlightRecorder \
-XX:StartFlightRecording=disk=true,filename=/rec/app.jfr,maxsize=100m,maxage=12h" \
-v /data/jfr:/rec \
myapp:latest
在 Kubernetes 里可以放进 sidecar 或 initContainer 做定时 dump,配合对象存储归档,就能沉淀出长期的性能基线,版本迭代时一眼看出是否回归。
六、把 JFR 接进持续性能体检
JFR 不该只在出事时才用。把它接进 CI/CD 流水线,每次压测都自动录一份,能捕捉版本间的性能回归。下面是一个在 GitHub Actions 里随负载测试录制的示例:
- name: Run with JFR
run: |
java -XX:+FlightRecorder \
-XX:StartFlightRecording=filename=build/rec.jfr \
-jar app.jar &
APP_PID=$!
./run-load-test.sh
kill -3 $APP_PID
sleep 2
更进一步,可用 Prometheus 抓取 JMX 暴露的 JFR 指标,或用 JMC 的自动化规则做告警。对想做原生镜像、追求毫秒级启动的服务,参考 Spring Boot GraalVM 原生镜像 时也要注意:GraalVM 原生镜像下 JFR 同样可用,只是部分依赖运行时代入的动态事件会受限。
七、JFR vs Async Profiler vs APM 怎么选
| 工具 | 开销 | 强项 | 适合场景 |
|---|---|---|---|
| JFR | 极低(<1%) | 全栈事件、生产常驻 | 长期黑匣子、综合体检 |
| Async Profiler | 低 | 火焰图、CPU/alloc/wall | 单点深度剖析 |
| APM(SkyWalking 等) | 中 | 分布式链路、业务指标 | 全链路追踪 |
三者并不冲突:JFR 做常驻体检,Async Profiler 做单点深挖,APM 看业务链路。小服务用 JFR + Async Profiler 就够了。
八、常见误区与排障清单
| 误区 | 真相 |
|---|---|
| 开 JFR 会拖垮生产 | 默认 profile 预设开销 <1%,可常驻 |
| 只在出事才录 | 应常驻环形缓冲,事后也能 dump |
| 只看 GC 不看锁/IO | 多数延迟抖来自锁与 IO |
| jfr 文件太大 | 用 maxsize/maxage 限容,或调高采样阈值 |
最后提醒:JFR 是诊断利器,但前提是 Spring Boot 3 升级 这类基础环境稳定、应用能正常暴露 JMX。把 JFR 当成每辆车的行车记录仪,你的 JVM 性能问题将不再”无迹可寻”。
九、把 JFR 变成团队的肌肉记忆
性能问题最贵的不是修复,而是”找不到”。把 JFR 设成每个服务的常驻黑匣子、在 CI 压测里强制录制、把 .jfr 归档到对象存储,三件事做全,团队就拥有了可回溯的性能历史。下次再有人问”为什么这个接口比上周慢了 30ms”,你不必靠猜——拉一份 JFR 对比,答案一目了然。
十、一个真实案例:用 JFR 揪出隐藏的锁竞争
某次上线后,一个看似简单的库存扣减接口在高峰期延迟从 8ms 涨到 60ms,但 CPU 与 GC 都正常。在压测中开启 JFR 后,jdk.MonitorEnter 事件的平均等待时间高达 40ms——根因是一个被低估的全局 ConcurrentHashMap 在扩容瞬间触发了 synchronized 分段锁竞争。把锁粒度下沉到分段键、并对热键做本地缓存后,延迟回落到 9ms。
这个案例的启示是:很多”玄学”延迟,本质是并发原语在高压下的排队。JFR 把这种不可见的等待变成了可量化的事件,让你不用猜、不用加满日志也能定位。它和火焰图配合,几乎能覆盖 80% 的 Java 性能排查场景。
# 用 jfr 工具快速统计锁等待 TopN
jfr summary /tmp/rec.jfr | grep -A20 "jdk.MonitorEnter"
jfr print --events jdk.MonitorEnter --stack-depth 16 /tmp/rec.jfr > locks.txt




