Java Flight Recorder 实战:用 JFR 飞行记录仪定位性能问题

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 / SafepointFull 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 容器里,录制路径挂到卷上即可。

上一篇 Kafka 消息积压排查实战:消费滞后定位与快速恢复
下一篇 Fail2ban 实战:用 Fail2ban 阻断 SSH 暴力破解
最新评论
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 […]