JFR 与 JMC 性能分析实战

系统讲解 JFR 与 JMC 性能分析:JFR 低开销原理与事件体系、录制与命令行操作、JMC 图形化分析路径、生产环境持续录制、与 GC 和堆分析的结合,以及常见性能问题的排查套路。

线上性能问题最难的不是「修」,而是「看」。采样器会带来可观开销,堆栈抓取会漏掉高频短方法,而 JFR(Java Flight Recorder)以极低的开销持续记录 JVM 运行时事件,配合 JMC(JDK Mission Control)图形化分析,已经成为生产环境性能分析的黄金搭档。本文从原理讲到生产实践,覆盖事件体系、录制、分析与常见排查套路。

一、JFR 是什么:低开销的可观测性

JFR 是内置于 HotSpot JVM 的事件记录器,从 JDK 11 起随 OpenJDK 免费分发。它通过**环形缓冲区(ring buffer)**持续记录事件,默认开销在 1% 以内,生产环境可以常开。

特性说明
开销默认 <1%,持续录制安全
数据事件流(分配、锁、GC、方法采样、IO 等)
记录方式内存环形缓冲,随时转储为 jfr 文件
分析工具JMC 图形化,或命令行 jfr 工具
导出格式.jfr,可被多种工具解析
为什么 JFR 比传统采样器好:
  传统 jstack 采样:抓瞬间快照,容易漏掉高频短方法、有锁争用看不到
  JFR:连续记录事件 + 高精度时间戳,还原时间线上的完整图景

一句话总结: JFR 是「常开式」的低开销记录器,用环形缓冲区持续采集事件,1% 以内的开销换取生产环境全时段可回溯,这是它优于传统采样的根本。


二、JFR 原理与事件体系

2.1 事件类型

JFR 事件按生命周期分为三类:

事件类型含义示例
即时事件(Instant)某一时刻发生一次CPU 负载、GC 周期结束
持续事件(Duration)有开始结束时间锁等待、方法执行、IO
采样事件(Timed/Sample)周期性采样方法执行采样、分配采样
JDK 自带几十种内置事件,覆盖:
  JVM 内部:GC、类加载、JIT 编译、线程
  应用层: 锁争用、方法调用、文件/网络 IO、内存分配
  系统层:  CPU 使用率、上下文切换、磁盘 IO

2.2 环形缓冲区与转储

录制过程:
  1. 事件产生 → 写入内存环形缓冲(默认按 1MB 起配)
  2. 缓冲满 → 覆盖最旧数据(除非设置了磁盘溢出)
  3. 需要分析时 → dump 转储为 .jfr 文件

为什么安全:缓冲区写操作极轻量,事件不阻塞业务线程

一句话总结: JFR 事件分为即时、持续、采样三类,覆盖 JVM 与应用的方方面面;环形缓冲区 + 按需转储的设计让它既能持续记录又不占磁盘。


三、录制:命令行操作

3.1 启动时录制

# 启动 JVM 时开启 JFR,录制 60 秒,输出到 app.jfr
java -XX:StartFlightRecording=filename=app.jfr,duration=60s,settings=profile MyApp

# 用预设配置:default(低开销)或 profile(采样更密)
java -XX:StartFlightRecording=filename=app.jfr,settings=profile MyApp

profile 配置适合短期深度分析,default 配置适合生产常开。

3.2 运行时录制

# jcmd 动态开启录制,无需重启
jcmd <pid> JFR.start name=prod_recording settings=default disk=true maxage=1h

# 查看状态
jcmd <pid> JFR.check

# 转储到文件
jcmd <pid> JFR.dump filename=/tmp/prod.jfr

# 停止并保存
jcmd <pid> JFR.stop name=prod_recording filename=/tmp/prod_stop.jfr
# 用 jfr 命令行工具直接查看事件(无需 JMC)
jfr print --events jdk.GCHeapSummary /tmp/prod.jfr
jfr summary /tmp/prod.jfr

一句话总结: 录制有三种姿势——启动参数常开、jcmd 运行时动态启停、jfr 命令行离线分析;jcmd 动态录制让生产环境「出事再录」成为可能。


四、JMC 分析实战

4.1 打开与整体概览

JMC(JDK Mission Control)是 JFR 文件的图形化分析工具,左侧按问题域组织,右侧是图表与数据。

JMC 主要视图:
  线程:线程状态分布、阻塞与等待时间
  内存:GC 时间线、分配速率、对象统计
  代码:方法采样热点(近似火焰图)
  IO: 文件/网络读写吞吐与耗时
  锁: 争用最激烈的锁对象
  JVM:类加载、JIT 编译活动

4.2 分析路径示例

场景:接口 RT 变长,怀疑锁竞争
  1. 打开「线程」→ 看线程状态:WAITING/BLOCKED 占比
  2. 打开「锁」→ 找争用最高的锁实例
  3. 双击锁 → 看哪个线程持锁最久、哪些线程在等待
  4. 切到「代码」→ 看持锁线程的方法热点,定位临界区
# 用 jfr print 快速定位锁事件
jfr print --events jdk.JavaMonitorEnter --stack-depth 8 /tmp/prod.jfr \
  | head -100

4.3 代码热点与近似火焰图

JMC 的「代码」视图基于方法采样事件(jdk.ExecutionSample),展示各方法自占 CPU 的比例,可以导出近似火焰图:

解读要点:
  1. 顶部越宽 → 该调用路径消耗越多 CPU
  2. 关注"自身采样"高的叶子节点(真正干活的方法)
  3. 宽而平的火炬 → 大量小方法均摊,查分配
  4. 一条竖线很高 → 深栈递归,查算法复杂度
# 命令行导出热点方法 Top N
jfr print --events jdk.ExecutionSample --stack-depth 20 /tmp/prod.jfr \
  | jfr summary  # 或结合脚本按栈聚合

4.4 与线程转储互补

jstack 线程转储:某一瞬间的线程状态快照,适合"现在卡在哪"
JFR:一段时间的完整事件流,适合"这段时间发生了什么"

最佳实践:
  先用 JFR 看整体趋势与时间线
  再在关键时间点用 jstack 抓现场堆栈
  两者对账,锁定精确代码位置

一句话总结: JMC 按「线程、内存、代码、IO、锁、JVM」组织视图,分析套路是「看状态分布 → 定位异常类别 → 追到具体对象/方法」;代码视图的近似火焰图是 CPU 定位的入口,与 jstack 快照互补。


五、生产场景实践

5.1 持续录制(Always-On)

生产建议默认开启 JFR,用低开销的 default 配置 + 磁盘溢出策略,出事时随时转储最近一小时的数据。

# 生产常开:写磁盘,保留最近 1 小时
java -XX:StartFlightRecording=disk=true,maxage=1h,settings=default MyApp

# 出事后转储完整记录
jcmd <pid> JFR.dump filename=incident_$(date +%s).jfr

5.2 问题驱动录制策略

场景配置建议
常开监控settings=default,disk=true,maxage=24h
突发排查settings=profile,duration=5m,重启或 jcmd 启动
压测分析settings=profile,覆盖压测全程
低频偶发常开 default + 触发 dump 的告警脚本
# 压测时配合 GC 日志一起分析
java -Xlog:gc*=info:file=gc.log -XX:StartFlightRecording=filename=perf.jfr,settings=profile MyApp

5.3 与 APM 体系集成

常见集成姿势:
  1. 定时 jcmd JFR.dump 归档到对象存储,供事后分析
  2. 配合指标系统:GC 停顿、CPU 采样在 JFR 里提取为时序指标
  3. 出事时脚本自动转储:告警触发 → 转储最近记录 → 附带到工单
  4. 压测报告自动归档 .jfr,与代码版本关联
# 一个简单的"告警自动转储"脚本片段
if curl -s localhost:9090/health | grep -q ERROR; then
  jcmd <pid> JFR.dump filename=incident_$(date +%s).jfr
fi

一句话总结: 生产实践的核心是「default 常开 + 出事后转储」;压测和突发放大采样密度(profile),两者结合既保证可回溯又不背开销;再配合告警自动转储形成闭环。



六、JFR 与 GC、堆分析结合

6.1 从 JFR 看 GC

JFR 中与 GC 相关的关键事件:
  jdk.GCPhasePause       单次停顿
  jdk.GCHeapSummary      堆占用变化
  jdk.AllocationRequiringGC  分配导致 GC 的压力
  jdk.GarbageCollection   完整 GC 记录

判断方法:
  停顿频繁但堆不高  → 分配速率过高,对象生命周期太短
  堆持续高位并 Full GC  → 存在大对象/泄漏
  停顿集中在晋升   → 老年代容量配置问题

6.2 从 JFR 看内存分配

# 分配采样:找出"谁在大量分配"
jfr print --events jdk.ObjectAllocationSample --stack-depth 8 /tmp/prod.jfr \
  | head -50
结合 MAT 的思路:
  JFR 告诉你"哪些调用栈在疯狂分配"
  MAT 告诉你"这些对象最终堆在哪里、被谁引用"
  两者一前一后,形成完整的分配-存活-泄漏证据链

一句话总结: JFR 擅长回答「谁在分配、何时停顿」,堆转储回答「对象存哪、被谁引用」——JFR 定位方向 + MAT 定位泄漏根因是最有效的组合拳。


七、常见问题排查套路

7.1 CPU 高

1. 开 profile 录制 → 「代码」视图看方法采样热点
2. 区分应用代码 vs JVM 内部(GC/JIT)
3. 热点在应用方法 → 优化算法/缓存
4. 热点在 GC → 结合 GC 事件调整堆与分配

7.2 线程阻塞与 RT 抖动

1. 「线程」视图看 WAITING/BLOCKED 占比
2. 「锁」视图找争用点
3. 「IO」视图看网络/磁盘耗时
4. 结合虚拟线程:大量阻塞是常态,重点看载体线程是否被 Pin

7.3 偶发长停顿(STW)

1. 查 jdk.GCPhasePause 事件的长停顿
2. 查 jdk.ThreadPark、jdk.JavaMonitorEnter 是否有长等待
3. 查安全点(Safepoint):jdk.ExecutionSample 期间 JVM 是否在等待安全点
4. 对账系统日志时间戳,锁定停顿窗口

7.4 内存不断增长但没到 OOM

1. JFR 分配采样:看谁在持续分配大对象
2. 堆占用趋势:G1 周期后堆是否逐级抬升
3. 类加载事件:是否频繁加载新类(动态代理/反射泄漏)
4. 配合堆转储:确认对象被全局缓存长期引用
# 查分配最凶的调用栈
jfr print --events jdk.ObjectAllocationSample --stack-depth 16 /tmp/prod.jfr \
  | sort | uniq -c | sort -rn | head -20

一句话总结: JFR 的排查套路是「先看异常类别(CPU/线程/GC/IO),再逐层下钻到具体事件与方法栈」——用事件还原时间线,比抓快照靠谱得多。



八、实战陷阱清单

陷阱现象对策
忘记开 JFR出事时无数据生产常开 default 配置
磁盘无限增长空间耗尽设置 maxage/maxsize
采样密度过高开销超预期用 default 常开,profile 只用于短期
只看采样热点漏掉高频短方法结合持续事件(锁/IO/分配)
转储前就 stop数据丢失先 dump 再 stop
JFR 文件过大分析卡顿按时间窗/事件过滤导出
忽略时间戳校准与日志对不上统一 NTP,记录 jvm 启动时间

九、总结

维度JFR 价值
开销<1%,生产可常开
覆盖JVM 内部 + 应用 + 系统层事件
操作jcmd 动态启停,jfr 离线分析
分析JMC 图形化,问题域视图齐全
结合与 GC 日志、堆转储形成证据链

一句话记住:JFR 是 JVM 自带的全景记录仪,低开销地记录一切,出事时按需回放。先把生产环境 default 常开,把 jcmd 转储练熟,再用 JMC 或 jfr 命令行把事件翻译成结论——这是每个 Java 工程师都应该提前部署的观测能力。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. Java 序列化方案对比与性能
  2. Java 并发设计模式实战
  3. OOM 排查与堆转储分析