☰
JVM 线上排查实战(六):JFR 怎么用?JDK 8 要不要加 UnlockCommercialFeatures、jfr 命令在哪、录的文件为什么读不了
2026/10/8 2:05:19 网站建设 项目流程

这个系列

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

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

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

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

代号版本来源路径
Oracle 81.8.0_381,BUILD_TYPE="commercial"Oracle 官方包/opt/jdk8
OpenJDK 81.8.0_412CentOS 7yum install java-1.8.0-openjdk-devel/usr/lib/jvm/java-1.8.0-openjdk
JDK 1717.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)

写监控脚本去 grepfilename=的话,同样是「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 8u3810.9❌❌
OpenJDK 8u4122.0✅✅
JDK 17.0.82.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
default1727,532318,068 字节702
profile1729,560330,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 8java -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 上没了,以及所有工具都用不了时最后那条后路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询