JVM 线上排查实战(六):JFR 怎么用?JDK 8 要不要加 UnlockCommercialFeatures、jfr 命令在哪、录的文件为什么读不了

这个系列

「JVM 线上排查实战」的第六篇。前五篇讲的是出事之后怎么查:

  1. (一)先把 JVM 看清楚:进程、参数、默认值
  2. (二)CPU 飙高:找到那个线程
  3. (三)线程卡住:死锁、BLOCKED、线程池打满
  4. (四)内存:OOM 了先干什么
  5. (五)GC 日志:从 JDK 8 升 17,老启动参数会让进程直接起不来
  6. (六)JFR 飞行记录器:录一段现场下来慢慢看 ------ 本篇

前五篇的工具都是「出事的那一刻你正好在场」才有用:jstack 看的是此刻的线程,jmap 抓的是此刻的堆。JFR 不一样,它是一直录着,出事之后把那段时间捞出来。

本篇实测环境(三个 JDK 装在同一台机器上,输出都是原文):

代号 版本 来源 路径
Oracle 8 1.8.0_381,BUILD_TYPE="commercial" Oracle 官方包 /opt/jdk8
OpenJDK 8 1.8.0_412 CentOS 7 yum install java-1.8.0-openjdk-devel /usr/lib/jvm/java-1.8.0-openjdk
JDK 17 17.0.8,IMPLEMENTOR="Oracle Corporation" Oracle 官方包 /usr/java

系统 CentOS 7.9.2009,内核 3.10.0-1160.71.1.el7.x86_64。

先说结论

  • "JDK 8 用 JFR 要先加 -XX:+UnlockCommercialFeatures"这句话只对 Oracle JDK 成立。 同一台机器上 OpenJDK 8u412 不加任何参数就能录
  • 反过来在 JDK 17 上加这个参数,进程直接起不来 (Unrecognized VM option)------ 和(五)里那批 GC 参数是同一个形状
  • 进程已经起来了、启动参数里什么都没加,也不用重启 :Oracle 8 上先 jcmd <pid> VM.unlock_commercial_features 再 JFR.start;OpenJDK 8 和 17 直接 JFR.start
  • Oracle JDK 8 没有 jfr 命令行工具,OpenJDK 8 和 17 都有
  • Oracle JDK 8 录出来的文件是 0.9 版,jfr 工具直接拒绝读 (OpenJDK 8 的和 17 的都拒绝);OpenJDK 8 录的是 2.0,17 录的是 2.1,都能读
  • settings=profile 比默认多一倍采样 :同一个进程同样录 15 秒,jdk.ExecutionSample 从 702 条变成 1408 条,而文件只从 318,068 字节涨到 330,901 字节
  • kill -9 会让 dumponexit=true 留下一个 0 字节的 .jfr ------ 文件在,内容没有。kill -TERM 则正常写出 377,151 字节

1. 开启:同一条命令,三个 JDK 三种结果 ✅

先用最省事的办法试:java <参数> -version,起不来的当场就知道。

Oracle JDK 8 ------ 不解锁就起不来:

复制代码
$ /opt/jdk8/bin/java -XX:StartFlightRecording=duration=5s,filename=/root/jvmlab/t2.jfr -version
Error: To use 'StartFlightRecording', first unlock using -XX:+UnlockCommercialFeatures.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

加上解锁参数就正常了:

复制代码
$ /opt/jdk8/bin/java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -version
java version "1.8.0_381"
Java(TM) SE Runtime Environment (build 1.8.0_381-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.381-b09, mixed mode)

OpenJDK 8 ------ 什么都不用加:

复制代码
$ /usr/lib/jvm/java-1.8.0-openjdk/bin/java -XX:StartFlightRecording=duration=5s,filename=/root/jvmlab/o1.jfr -version
Started recording 1. The result will be written to:

/root/jvmlab/o1.jfr
openjdk version "1.8.0_412"
OpenJDK Runtime Environment (build 1.8.0_412-b08)
OpenJDK 64-Bit Server VM (build 25.412-b08, mixed mode)

加上 -XX:+UnlockCommercialFeatures 它也不报错(照常起来,版本号照常打印),所以老启动脚本原样拿到 OpenJDK 8 上不会炸。

JDK 17 ------ 加了解锁参数反而起不来:

复制代码
$ /usr/java/bin/java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -version
Unrecognized VM option 'UnlockCommercialFeatures'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

不加就对了:

复制代码
$ /usr/java/bin/java -XX:StartFlightRecording=duration=5s,filename=/root/jvmlab/t4.jfr -version
[0.233s][info][jfr,startup] Started recording 1. The result will be written to:
[0.233s][info][jfr,startup] 
[0.233s][info][jfr,startup] /root/jvmlab/t4.jfr
java version "17.0.8" 2023-07-18 LTS

🔑 这就是那个坑

网上绝大多数 JFR 教程都写着「JDK 8 要先 -XX:+UnlockCommercialFeatures」。照抄的后果分两头:

  • 你的线上是 OpenJDK 8:参数是多余的(但不会炸,所以你也发现不了自己抄错了)
  • 你把这份启动脚本升到 JDK 17:进程直接起不来

怎么一眼分清自己是哪个 8 :看 release 文件。

复制代码
$ grep -E 'IMPLEMENTOR|JAVA_VERSION|BUILD_TYPE' /opt/jdk8/release
JAVA_VERSION="1.8.0_381"
BUILD_TYPE="commercial"

BUILD_TYPE="commercial" = Oracle JDK。CentOS 的 yum 装的 OpenJDK 8 连 release 文件都没有:

复制代码
$ grep -E 'IMPLEMENTOR|BUILD_TYPE' /usr/lib/jvm/java-1.8.0-openjdk/release
grep: /usr/lib/jvm/java-1.8.0-openjdk/release: 没有那个文件或目录

最直接的还是 java -version 第一行:java version 是 Oracle,openjdk version 是 OpenJDK。


2. 进程已经在跑了,没加参数还能不能开 ✅

线上真实的情况通常是:出事了才想起来要 JFR,而进程是三个月前起的。

Oracle JDK 8 ------ 直接 JFR.start 会被拒:

复制代码
$ /opt/jdk8/bin/jcmd 5842 JFR.start name=r1 duration=20s filename=/root/jvmlab/r8n.jfr
5842:
Java Flight Recorder not enabled.

Use VM.unlock_commercial_features to enable.

按它说的做,不用重启进程:

复制代码
$ /opt/jdk8/bin/jcmd 5842 VM.unlock_commercial_features
5842:
Commercial Features now unlocked.

$ /opt/jdk8/bin/jcmd 5842 JFR.start name=r1 duration=20s filename=/root/jvmlab/r8n.jfr
5842:
Started recording 1. The result will be written to:

/root/jvmlab/r8n.jfr

OpenJDK 8 和 JDK 17 ------ 启动参数里什么都没加,直接就能开:

复制代码
$ /usr/lib/jvm/java-1.8.0-openjdk/bin/jcmd 7625 JFR.start name=r1 duration=15s settings=profile filename=/root/jvmlab/oj.jfr
7625:
Started recording 1. The result will be written to:

/root/jvmlab/oj.jfr

$ /usr/java/bin/jcmd 5739 JFR.start name=r1 duration=20s settings=profile filename=/root/jvmlab/r17.jfr
5739:
Started recording 1. The result will be written to:

/root/jvmlab/r17.jfr

⚠️ jcmd 要用和目标进程同一个 JDK 更稳妥。跨版本的情况(比如用 17 的 jcmd 去打 8 的进程)在下一篇(七)里专门测,jcmd/jstack 能过,jmap -heap 不能。


3. 查状态:两个版本输出格式不一样 ✅

复制代码
$ /opt/jdk8/bin/jcmd 5737 JFR.check
5737:
Recording: recording=1 name="r1" duration=20s filename="/root/jvmlab/r8.jfr" compress=false (running)

$ /usr/java/bin/jcmd 5739 JFR.check
5739:
Recording 1: name=r1 duration=20s (running)

Oracle JDK 8 的那行带着 filename= 和 compress=,17 的没有。OpenJDK 8 的格式和 17 一样:

复制代码
$ /usr/lib/jvm/java-1.8.0-openjdk/bin/jcmd 7625 JFR.check
7625:
Recording 1: name=r1 duration=15s (running)

写监控脚本去 grep filename= 的话,同样是「JDK 8」,两个发行版一个有一个没有。


4. 不限时长地录,中途捞一段出来 ✅

线上更常见的用法是不设 duration,让它一直录着,出事了再 dump:

复制代码
$ /usr/java/bin/jcmd 5739 JFR.start name=live settings=default
5739:
Started recording 2. No limit specified, using maxsize=250MB as default.

Use jcmd 5739 JFR.dump name=live filename=FILEPATH to copy recording data to file.

注意它自己说的:没设上限时默认 maxsize=250MB,是个环形缓冲,超了就丢最旧的。录了 12 秒之后 dump:

复制代码
$ /usr/java/bin/jcmd 5739 JFR.dump name=live filename=/root/jvmlab/live1.jfr
5739:
Dumped recording "live", 373.7 kB written to:

/root/jvmlab/live1.jfr

$ ls -la live1.jfr
-rw-r--r--. 1 root root 382694 9月  23 22:44 live1.jfr

dump 完录制还在继续 ,可以反复 dump。停止用 JFR.stop name=live。


5. 读:jfr 命令在不在,以及读不读得动 ✅

Oracle JDK 8 没有这个命令:

复制代码
$ ls /opt/jdk8/bin/jfr
ls: 无法访问/opt/jdk8/bin/jfr: 没有那个文件或目录

$ /opt/jdk8/bin/jfr summary r8.jfr
bash:行1: /opt/jdk8/bin/jfr: 没有那个文件或目录

OpenJDK 8 有:

复制代码
$ ls /usr/lib/jvm/java-1.8.0-openjdk/bin/jfr
/usr/lib/jvm/java-1.8.0-openjdk/bin/jfr

JDK 17 读自己录的,一切正常:

复制代码
$ /usr/java/bin/jfr summary r17.jfr

 Version: 2.1
 Chunks: 1
 Start: 2026-09-23 14:42:42 (UTC)
 Duration: 20 s

 Event Type                                   Count  Size (bytes) 
==================================================================
 jdk.GCPhaseParallel                           4947        131511
 jdk.ExecutionSample                           1765         18998
 jdk.PromoteObjectInNewPLAB                    1231         22077
 jdk.ObjectAllocationSample                     961         14102
 jdk.ThreadSleep                                901         13304
 jdk.TenuringDistribution                       900         10691
 jdk.PromoteObjectOutsidePLAB                   575          9582
 jdk.GCPhasePauseLevel1                         537         21507

🔴 Oracle JDK 8 录的文件,jfr 工具读不了

复制代码
$ /usr/java/bin/jfr summary r8.jfr
jfr summary: could not read recording at /root/jvmlab/r8.jfr. File version 0.9. Only Flight Recorder files of version 1.x and 2.x can be read by this JDK.

换 OpenJDK 8 自己的 jfr 去读,一样不行,报的是同一句话:

复制代码
$ /usr/lib/jvm/java-1.8.0-openjdk/bin/jfr summary r8.jfr
jfr summary: could not read recording at /root/jvmlab/r8.jfr. File version 0.9. Only Flight Recorder files of version 1.x and 2.x can be read by this JDK.

而 OpenJDK 8 录的文件是 2.0 版,JDK 17 读得动:

复制代码
$ /usr/java/bin/jfr summary oj.jfr

 Version: 2.0
 Chunks: 1
 Start: 2026-09-23 14:56:08 (UTC)
 Duration: 15 s

 Event Type                              Count  Size (bytes) 
=============================================================
 jdk.BooleanFlag                           804         27243
 jdk.JavaMonitorWait                       607         17605

三个版本的文件版本号,实测如下:

录制方 .jfr 文件版本 17 的 jfr 能读 OpenJDK 8 的 jfr 能读
Oracle JDK 8u381 0.9 ❌ ❌
OpenJDK 8u412 2.0 ✅ ✅
JDK 17.0.8 2.1 ✅ ✅(OpenJDK 8 的 jfr 读 17 录的 2.1 文件也正常)

🔑 所以在 Oracle JDK 8 的线上开 JFR,要先想清楚谁来读这个文件 ------ 手上这三个 JDK 的命令行工具都读不了它。


6. 从录到的数据里找热点方法 ✅

jfr summary 只给个数量概览,真要定位热点看 jdk.ExecutionSample 的栈:

复制代码
$ /usr/java/bin/jfr print --events jdk.ExecutionSample r17.jfr | grep -E 'JfrDemo\.' | head -6
    JfrDemo.hash(int) line: 16
    JfrDemo.lambda$main$0() line: 7
    JfrDemo.hash(int) line: 16
    JfrDemo.lambda$main$0() line: 7
    JfrDemo.hash(int) line: 16
    JfrDemo.lambda$main$0() line: 7

测试程序里烧 CPU 的就是 hash(),被 worker-1 线程在死循环里调 ------ 采样栈直接指到了行号 。这是 JFR 比 jstack 强的地方:jstack 是你手动敲的那一瞬间的快照,JFR 是这段时间里成百上千次采样。


7. settings=profile 到底多收了多少 ✅

同一个进程(JDK 17),先录 15 秒 default,再录 15 秒 profile:

settings 事件类型数 事件总数 文件大小 jdk.ExecutionSample
default 172 7,532 318,068 字节 702
profile 172 9,560 330,901 字节 1,408

事件类型 一样多(都是 172 种),差别在采样密度:执行采样正好翻了一倍,而文件只大了 4%。

⚠️ 这是一个空转的测试程序的读数,不是你线上那套的读数;换成真实应用两边的绝对值都会变。这里能说的只有一句:从 default 换到 profile,涨的主要是采样条数,不是文件体积。


8. 进程退出时能不能留下文件 ✅

dumponexit=true 的意思是「进程退出时把录制写出来」。分两种退出方式实测,同一个启动参数:

复制代码
-XX:StartFlightRecording=dumponexit=true,filename=/root/jvmlab/<名字>.jfr

kill -TERM(正常退出):

复制代码
$ kill -TERM 6478
$ ls -la exit_term.jfr
-rw-r--r--. 1 root root 377151 9月  23 22:46 exit_term.jfr

kill -9:

复制代码
$ kill -9 6517
$ ls -la exit_kill9.jfr
-rw-r--r--. 1 root root 0 9月  23 22:46 exit_kill9.jfr

🔴 注意这个 0 不是"没有文件"

kill -9 之后文件是存在的,只是 0 字节 。如果你的排查脚本判断的是「.jfr 文件在不在」,它会告诉你「录到了」;直到你把这个文件拖回本地准备分析,才发现里面什么都没有。

JFR 文件是进程退出时才落盘的 (前面 JFR.dump 那种主动导出除外),而 kill -9 不给进程任何执行退出逻辑的机会。所以:排查期间别用 kill -9 停进程,否则你录了一整天的现场在那一刻全没了。


9. 本篇速查

你的情况 怎么做
不知道自己是哪个 JDK 8 java -version 第一行:java version = Oracle,openjdk version = OpenJDK;或 grep BUILD_TYPE $JAVA_HOME/release
Oracle JDK 8,进程已经在跑 jcmd <pid> VM.unlock_commercial_features 再 JFR.start,不用重启
OpenJDK 8 / JDK 17,进程已经在跑 直接 jcmd <pid> JFR.start name=r1 duration=60s settings=profile filename=/tmp/a.jfr
想一直录着,出事再捞 JFR.start name=live settings=default(默认 maxsize=250MB 环形缓冲)→ 出事时 JFR.dump name=live filename=...
启动脚本要两个版本通用 别写 -XX:+UnlockCommercialFeatures(17 上起不来),Oracle 8 改用运行时 VM.unlock_commercial_features
看录到了什么 jfr summary a.jfr
找热点方法 jfr print --events jdk.ExecutionSample a.jfr
Oracle JDK 8 录的文件读不了 报错是 File version 0.9。手上的 jfr 命令行工具(8 和 17 的)都读不了它
停进程 用 kill(TERM),别用 kill -9 ------ 后者会留下 0 字节的 .jfr

下一篇

(七)讲工具连不上的时候怎么办 :jps 看不到进程、jstack 报「不允许的操作」、attach 挂住不返回、jstack -F 在 17 上没了,以及所有工具都用不了时最后那条后路。

相关推荐
小米里的大麦1 小时前
C++ 移动语义
c++·后端
蚰蜒螟1 小时前
一次挂载的完整旅程:Linux 内核 VFS 到 ext4 的调用链剖析
linux·运维·服务器
kele_z1 小时前
按部门控制数据权限,一个注解 + 一个切面 + 一行 SQL 就够了
后端
害人终害己1 小时前
Redis 日志:AUTH 认证信息
运维·服务器
奇牙coding1 小时前
GPT-5.5 API 报 401 但 GPT-5.4 正常怎么办?不是 Key 失效,是 Organization 头的强制校验变了
java·网络·gpt·ai
一个有温度的技术博主1 小时前
凌晨两点的死锁:当线程“互相等死“
开发语言·后端·场景
ThinkerQAQ_1 小时前
并发编程(八):读写锁——从语言规则到 CPU
后端
a努力。1 小时前
Context-State-Memory三重信息架构揭秘
java·服务器·前端
天蓝蓝的本我1 小时前
linux本地部署Qwen3.8 27B
linux·运维·excel