这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 JFR(Java Flight Recorder)是 JVM 内置的事件记录系统,JMC(JDK Mission Control)是分析和可视化这些事件的客户端工具。相比采样 profiler 和业务日志,JFR 的开销低,适合在生产环境持续或按需采集。
JDK 11 以后 JFR 开源并成为 JDK 的重要诊断能力;JDK 8 中的可用性和参数存在商业授权与版本差异。
23.1 JFR 的价值
JFR 可以记录:
- CPU 热点方法;
- 内存分配位置;
- GC 与安全点;
- 锁竞争和线程等待;
- 类加载和异常;
- I/O 与socket相关事件;
- JVM 运行信息。
应用运行
|
+-- JFR Event Stream
|
+-- method sampling
+-- allocation sampling
+-- monitor blocked
+-- GC / safepoint
+-- JVM metadata
它解决的是“当时发生了什么”,而不是事后猜测。
23.2 启动方式
启动时开启持续记录:
java -XX:StartFlightRecording=filename=/log/app.jfr,maxsize=512m,maxage=1d \
-jar app.jar
运行中开始:
jcmd <pid> JFR.start name=live filename=/log/live.jfr maxsize=512m maxage=1d
停止:
jcmd <pid> JFR.stop name=live
dump 当前内容:
jcmd <pid> JFR.dump name=live filename=/log/now.jfr
查看状态:
jcmd <pid> JFR.check
23.3 常用配置
| 配置 | 含义 |
|---|---|
filename |
输出文件 |
maxsize |
文件最大尺寸 |
maxage |
事件保留时长 |
settings |
事件模板,如 profile |
dumponexit=true |
JVM 退出时输出 |
name |
记录名称 |
低开销 profile 示例:
java -XX:StartFlightRecording=filename=/log/app.jfr,settings=profile,maxsize=1g,maxage=6h \
-jar app.jar
settings=profile 通常包含更细的采样事件,但仍应根据实际压测确认开销。
23.4 JMC 界面
JDK Mission Control 打开 .jfr 文件后,常用页面包括:
| 页面 | 用途 |
|---|---|
| Overview | CPU、堆、线程、异常概览 |
| Memory | 堆、GC、分配热点 |
| Code | 方法热点和调用栈 |
| Threads | 线程状态和锁 |
| Lock Instances | 锁竞争 |
| Exceptions | 异常统计 |
| I/O | 文件和网络事件 |
| Environment | JVM 和系统信息 |
分析顺序建议:
1. 看记录时间窗口是否覆盖故障
2. 看 CPU 和堆是否异常
3. 找热点方法或热点分配
4. 查看线程状态和锁
5. 把事件时间与业务日志对齐
23.5 CPU 高分析
JMC 的 Code 页可以看到方法采样占比。
典型结论:
40% CPU -> com.demo.OrderParser.parse
at OrderService.handle
at HttpWorker.run
继续检查:
- 调用次数是否异常;
- 单次耗时是否变长;
- 是否存在正则回溯、大 JSON 解析、重复序列化;
- 是否 JIT 未热身;
- 是否锁竞争导致自旋;
- 是否容器 CPU limit 过低。
JFR 的方法采样是采样数据,适合定位热点方向;精确到每个调用的耗时应结合业务埋点或 Arthas trace。
23.6 内存分配热点
JFR 可以记录对象分配事件的采样栈。
典型现象:
byte[] 分配 800MB/s
-> JsonWriter.toByteArray
-> OrderController.export
排查方向:
- 是否一次性导出大文件;
- 是否循环中创建大集合;
- 是否每请求创建大缓冲;
- 是否反序列化对象过多;
- 是否日志拼接过度;
- 是否缓存未复用。
分配速率高会推高 Young GC 频率,在 ZGC 等收集器下还可能引发 Allocation Stall。
23.7 锁与线程分析
JFR 常见线程状态:
| 状态 | 含义 |
|---|---|
| RUNNABLE | 可运行或运行中 |
| BLOCKED | 等待进入 monitor |
| WAITING | 无限期等待 |
| TIMED_WAITING | 限时等待 |
| SLEEPING | 睡眠 |
如果大量线程 BLOCKED:
1. 查看 Lock Instances
2. 找持有者线程
3. 查看持有时间
4. 定位方法调用栈
5. 缩小临界区或改并发结构
注意 synchronized 和 java.util.concurrent.locks 在事件呈现上可能不同,JDK 版本也会影响锁事件粒度。
23.8 常用事件
| 事件 | 用途 |
|---|---|
| CPU Load | 进程和机器 CPU |
| Java Monitor Blocked | synchronized 竞争 |
| Java Monitor Wait | wait/notify 等待 |
| Thread Park | LockSupport.park |
| Allocation Sample | 分配热点 |
| Execution Sample | CPU 热点 |
| Garbage Collection | GC 概览 |
| Safepoint Begin/End | 安全点 |
| Exception Statistics | 异常 |
| Class Loading | 类加载 |
可用 jfr metadata 查看当前 JFR 支持的事件类型:
jfr metadata
23.9 生产实践
建议策略:
- 常态低开销记录,保留最近数小时;
- 告警触发后立即 dump;
- 把 JFR 文件、GC 日志、业务日志时间对齐;
- 控制文件大小和保留周期;
- 限制下载权限,因为记录可能包含参数和对象信息;
- 变更后对比基线;
- 对高吞吐服务先压测 JFR 开销。
Kubernetes 场景要注意:Pod 重建后本地文件会丢失。可以挂载日志卷,或由运维平台在接收到告警后立刻采集上传。
本章小结
JFR 是 JVM 内置的低开销事件记录系统,配合 JMC 可以分析 CPU 热点、分配热点、锁竞争、GC 和线程状态。生产上适合常态保留最近事件,并在异常时快速 dump。分析时应把事件流与 GC 日志、业务指标和源码对齐,避免把采样热点直接等同根因。
思考题
- JFR 和堆 dump 分别适合什么问题?
maxsize和maxage为什么重要?- 如何用 JFR 定位 Young GC 频繁的分配来源?
- 方法采样占比高是否一定代表根因?
- 如何在 Kubernetes 中设计 JFR 采集方案?