JFR 实战:用 Java Flight Recorder 给 JVM 做性能体检

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同步阻塞 IOIO 等待过长

结合 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
上一篇 PostgREST 实战:把 PostgreSQL 一键变成安全 REST API
下一篇 ArgoCD 实战:用 GitOps 实现 Kubernetes 持续部署
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]