1. 概述#
1.1. JFR 事件采集概述#
JFR(Java Flight Recorder) 是 Java 平台提供的一个低开销的性能监控和诊断工具。它最初是 Oracle JDK 的商业特性,从 JDK 11 开始作为开源特性集成到 OpenJDK 中,对应的 JEP 和 JBS 分别是:
JFR 通过事件采集机制,记录 JVM 运行时的各种信息,包括:
- GC 事件:GC 类型、持续时间、回收的内存大小等
- 线程事件:线程创建、销毁、状态变化等
- 方法执行事件:方法调用、执行时间、采样信息等
- 对象分配事件:对象分配位置、大小、类型等
- 系统事件:CPU 使用率、内存使用率、网络 I/O 等
- 自定义事件:应用程序可以定义和记录自定义事件
JFR 的核心优势在于其低开销特性。通过精心设计的事件采集机制,JFR 可以在生产环境中持续运行,对应用程序性能的影响通常小于 1%(吹牛逼,实际则需要根据你的实际需要进行配置后,可以在很低的消耗下仍能持续采集定位问题)
传统的性能监控工具通常采用采样或插桩的方式,但这些方式往往带来较大的性能开销。JFR 是一种可以持续开启的低开销 JVM 监控机制,特别适合事后分析性能瓶颈和极限问题。在生产环境中,JFR 的核心价值体现在以下几个方面:
- 问题发生时的应急处理:当 JVM 进程出现问题时,首要任务是尽快恢复业务(重启实例、下线问题实例、扩容等),而不是立即进行问题分析。此时需要的是能够持续记录、事后分析的工具。
- 问题现场的时间窗口:问题出现时,往往已经错过了问题发生的关键时间点。即使通过下线实例来保护现场,使用实时分析工具(如 JVisualVM、Arthas)可能已经无法获取到问题发生时的现场数据。
- JVM 无响应场景:某些严重问题(如 OOM、死锁导致线程阻塞等)可能导致 JVM 无法响应外部诊断请求(JMX、jcmd),此时实时分析工具完全失效。JFR 由于是持续记录机制,即使 JVM 出现问题,也能保留问题发生前的历史数据。具体案例可参考:6. 如何通过 JFR 快速定位 Java 堆 OOM 的实战与底层原理
以下是几类常见工具与 JFR 的对比:
1.1.1. JVisualVM#
JVisualVM 是 JDK 自带的图形化监控工具,通过 JMX 连接获取 JVM 运行时数据。
- 优势:图形化界面,易于使用;支持堆转储、线程转储分析
- 劣势:
- 基于 JMX 连接 + Agent 插桩采样,性能开销大(10~20%)
- 在生产环境 JVM 压力大时,采集可能中断
- 无法持续记录历史数据,不适合事后分析
- 与 JFR 对比:JFR 开销更低(<5%),支持持续记录历史数据,更适合生产环境持续运行和事后分析
1.1.2. Arthas#
Arthas 是阿里巴巴开源的 Java 诊断工具,通过字节码增强实现动态监控。
- 优势:无需重启应用;支持动态修改代码;提供丰富的诊断命令
- 劣势:
- 很多功能基于字节码增强,性能开销较大(10~20%)
- 不适合长期开启,存在安全风险
- 需要 JVM 能够响应外部请求,不适合事后问题分析
- 与 JFR 对比:JFR 开销更低,更适合生产环境持续运行;Arthas 更适合临时诊断和问题排查
1.1.3. OpenTelemetry / Skywalking#
OpenTelemetry 是分布式追踪和 APM(Application Performance Monitoring)标准,Skywalking 是基于 OpenTelemetry 实现的分布式追踪工具,通过 Java Agent 插桩实现。
- 优势:支持分布式追踪;提供完整的调用链分析;支持多语言
- 劣势:
- 基于字节码插桩,性能开销较大(5-15%)
- 需要额外的存储和计算资源,全量上报成本很高(远高于日志成本)
- 专注于应用层调用链分析,不适合 JVM 内部事件监控
- 与 JFR 对比:JFR 专注于 JVM 内部事件,开销更低;APM 工具专注于应用层调用链,两者可以互补使用
1.1.4. 实际使用建议#
在实际生产环境中,建议采用分层监控策略,结合多种工具的优势:
1. OpenTelemetry + APM 工具(如 Skywalking):
- Metrics 采集:使用 Prometheus + Grafana 进行指标监控和告警
- Traces 采集:使用 Jaeger 或 Skywalking 进行调用链追踪
- 由于全量上报成本很高,建议采用采样策略(如 5% 采样率)
- 主要用于告警和初步定位问题(定位到出问题的微服务实例和大致位置)
- 如果 Traces 数据不足以定位根因,再查看对应实例的 JFR 数据 2. JFR 持续采集:
- 每个微服务实例都开启 JFR,利用 JFR 自带的机制在本地临时文件目录保存最近 3 天的 JFR 文件
- 当出现问题时,在 JVM 实例下线前,将对应实例临时文件目录的所有 JFR 文件上传到集中式存储(如 S3、OSS)用于事后分析
- JFR 数据用于深入分析 JDK + JVM 内部事件,定位性能瓶颈和根因问题:
- 比如所有的锁问题,包括分布式锁,都可以通过 JFR 的锁相关事件(Monitor 相关事件,Thread Sleep,Thread Park 等等)
- JFR 还可以帮助分析 GC 行为、线程状态、热点方法执行情况等
1.2. JFR 相关配置概览#
flowchart LR
A[JFR 配置体系] --> B[全局配置]
A --> C[记录级别配置]
B --> B1[-XX:+FlightRecorder
JDK 11 引入
作用: 启用/禁用 JFR 功能]
B --> B2[全局选项
通过 -XX:FlightRecorderOptions= 指定
或 jcmd JFR.configure 修改
或通过 JMX 修改]
B2 --> B21[repository
JDK 11 引入
默认: 临时目录
作用: Repository 路径]
B2 --> B22[globalbuffersize
JDK 11 引入
默认: 512k
作用: 全局缓冲区大小]
B2 --> B23[numglobalbuffers
JDK 11 引入
默认: 20
作用: 全局缓冲区数量]
B2 --> B24[memorysize
JDK 11 引入
默认: 10m
作用: JFR 占用总内存限制]
B2 --> B25[threadbuffersize
JDK 11 引入
默认: 8k 或操作系统 pageSize
作用: 线程缓冲区大小]
B2 --> B26[maxchunksize
JDK 11 引入
默认: 12m
作用: 最大 Chunk 大小]
B2 --> B27[samplethreads
JDK 11 引入
JDK 19 废弃
默认: true
作用: 是否启用线程采样]
B2 --> B28[stackdepth
JDK 11 引入
默认: 64
作用: 堆栈跟踪深度]
B2 --> B29[retransform
JDK 11 引入
默认: true
作用: 是否允许 JVMTI retransform]
B2 --> B30[old-object-queue-size
JDK 11 引入
默认: 256
作用: OldObjectSample 队列大小]
B2 --> B31[dumppath
JDK 17 引入
默认: 当前工作目录
作用: 紧急转储路径]
B2 --> B32[preserve-repository
JDK 21 引入
默认: false
作用: JVM 退出时是否保留 Repository]
B2 --> B33[sampleprotection
JDK 11 引入
仅 DEBUG 模式
默认: false/true
作用: 采样线程时堆栈遍历的保护措施]
C --> C1[单个记录配置
通过 -XX:StartFlightRecording= 指定
或 jcmd JFR.start 指定
或 JDK API 指定
或 JMX 指定]
C --> C2[记录级别但影响全局的配置
每个记录可设置,但实际影响全局]
C1 --> C11[settings
JDK 11 引入
默认: default.jfc
作用: 事件配置文件]
C1 --> C12[name
JDK 11 引入
默认: null
作用: 记录名称]
C1 --> C13[delay
JDK 11 引入
默认: 0s
作用: 延迟启动时间]
C1 --> C14[duration
JDK 11 引入
默认: 0s
作用: 自动停止时间]
C1 --> C15[filename
JDK 11 引入
默认: null
作用: dump 时的文件名]
C1 --> C16[maxage
JDK 11 引入
默认: 0s
作用: 最大保留时间]
C1 --> C17[maxsize
JDK 11 引入
默认: 0
作用: 最大总大小]
C1 --> C18[dumponexit
JDK 11 引入
默认: false
作用: JVM 退出时是否 dump]
C1 --> C19[path-to-gc-roots
JDK 11 引入
默认: false
作用: 是否记录 GC 根]
C1 --> C20[report-on-exit
JDK 25 引入
默认: null
作用: JVM 退出时生成报告视图]
C2 --> C21[disk
JDK 11 引入
默认: true
作用: 是否保存到磁盘
影响: 全局共享 chunk 文件]
C2 --> C22[flush-interval
JDK 17 引入
默认: 1s
作用: 定时 flush 间隔
影响: 使用所有记录的最小值]
C11 --> C23[jfc 文件
default.jfc, profile.jfc]
C23 --> C24[enabled: JDK 11 引入
所有事件类型
控制事件是否被记录]
C23 --> C25[threshold: JDK 11 引入
仅对持续时间事件有效
指定持续时间阈值]
C23 --> C26[period: JDK 11 引入
仅对周期性事件有效
指定周期性事件采集间隔]
C23 --> C27[stackTrace: JDK 11 引入
所有事件类型
控制是否采集堆栈跟踪]
C23 --> C28[throttle: JDK 16 引入
所有事件类型
控制事件最大采集速率]
C23 --> C29[filter: JDK 25 引入
仅对特定事件有效
指定方法过滤器]
C23 --> C30[cutoff: JDK 11 引入
仅对 jdk.OldObjectSample 有效
限制查找 GC root 路径的最大时间]
C23 --> C31[level: JDK 22 引入
仅对 jdk.DeprecatedInvocation 有效
控制记录哪些废弃方法调用]
%% 根节点和主要分类
style A fill:#2c3e50,stroke:#34495e,stroke-width:3px,color:#fff
style B fill:#3498db,stroke:#2980b9,stroke-width:2px,color:#fff
style C fill:#e67e22,stroke:#d35400,stroke-width:2px,color:#fff
%% 全局配置 - 启用标志(特殊)
style B1 fill:#27ae60,stroke:#229954,stroke-width:2px,color:#fff
%% 全局配置 - 容器
style B2 fill:#5dade2,stroke:#3498db,stroke-width:2px,color:#fff
%% 全局配置 - 缓冲区相关(蓝色系)
style B22 fill:#aed6f1,stroke:#5dade2,stroke-width:1.5px
style B23 fill:#aed6f1,stroke:#5dade2,stroke-width:1.5px
style B24 fill:#aed6f1,stroke:#5dade2,stroke-width:1.5px
style B25 fill:#aed6f1,stroke:#5dade2,stroke-width:1.5px
%% 全局配置 - 存储相关(绿色系)
style B21 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px
style B26 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px
%% 全局配置 - 采样相关(紫色系)
style B27 fill:#d2b4de,stroke:#bb8fce,stroke-width:1.5px
style B33 fill:#d2b4de,stroke:#bb8fce,stroke-width:1.5px
%% 全局配置 - 其他基础配置(浅蓝色系)
style B28 fill:#d6eaf8,stroke:#85c1e9,stroke-width:1.5px
style B29 fill:#d6eaf8,stroke:#85c1e9,stroke-width:1.5px
style B30 fill:#d6eaf8,stroke:#85c1e9,stroke-width:1.5px
%% 全局配置 - 新版本特性(粉色系,JDK 17+)
style B31 fill:#f8c9d4,stroke:#ec7063,stroke-width:1.5px
style B32 fill:#f8c9d4,stroke:#ec7063,stroke-width:1.5px
%% 记录级别配置 - 容器
style C1 fill:#f39c12,stroke:#e67e22,stroke-width:2px,color:#fff
style C2 fill:#e74c3c,stroke:#c0392b,stroke-width:2px,color:#fff
%% 记录级别配置 - 单个记录配置(橙色系)
style C11 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px
style C12 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px
style C13 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px
style C14 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px
style C15 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px
style C16 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px
style C17 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px
style C18 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px
style C19 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px
%% 记录级别配置 - 新版本特性(红色系,JDK 22+)
style C20 fill:#f1948a,stroke:#e74c3c,stroke-width:1.5px
%% 记录级别配置 - 影响全局的配置(深橙色系)
style C21 fill:#f5b041,stroke:#e67e22,stroke-width:1.5px
style C22 fill:#f5b041,stroke:#e67e22,stroke-width:1.5px
%% jfc 文件配置(绿色系)
style C23 fill:#58d68d,stroke:#27ae60,stroke-width:2px,color:#fff
style C24 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px
style C25 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px
style C26 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px
style C27 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px
style C28 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px
style C29 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px
style C30 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px
style C31 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px
1.3. JFR 相关配置详情与演进#
- 全局配置:控制 JFR 功能的启用和全局选项
- 是否启用 JFR,不启用则连模块都不会加载:
-XX:+FlightRecorder(JDK 11 引入,默认值:false,这个配置已过期,并且虽然默认值是 false,但是如果没有设置,JVM 还是将其设置为 true,即默认启用 JFR 功能,只有在显式设置为 false 时才会禁用 JFR 功能) - 全局选项:通过
-XX:FlightRecorderOptions=指定,或者通过jcmd <pid> JFR.configure命令修改,或者通过 JMX 修改repository:JDK 11 引入,默认值:null(实际使用java.io.tmpdir系统属性指定的目录,即 Java 临时文件目录),作用:Flight recorder disk repository location(Repository 路径):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:指定 JFR 存储 chunk 文件的磁盘目录。当启用磁盘录制时,JFR 会在此目录创建子目录(格式:
<timestamp>_<pid>)存储 chunk 文件。影响磁盘 I/O 性能和存储空间管理 - 运行时动态修改:可以在运行时通过
jcmd <pid> JFR.configure repositorypath=<path>或 JMX 修改。如果 JFR 已初始化,修改时会切换到新的 repository 路径,旧的 repository 数据会保留在原位置,不会被迁移。如果旧的 repository 目录已经在cleanupDirectories中(即之前通过newChunk()创建过),JFR 会继续追踪它,并在 JVM 关闭时自动删除(如果preserve-repository=false)。如果有正在运行的磁盘记录,当前 chunk 会被标记为 final 并轮转,新的 chunk 会写入新的 repository 路径 - 相关 JBS:
- JDK-8243452:修复了在有超过 200 个记录时,无法在存储库中创建 chunk 的问题(Fix Version: 15)- https://bugs.openjdk.org/browse/JDK-8243452
globalbuffersize:JDK 11 引入,默认值:512k,作用:Global buffer size(全局缓冲区大小):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:单个全局缓冲区的大小(范围:64K-2G)。与
numglobalbuffers共同决定总内存占用(memorysize = globalbuffersize * numglobalbuffers)。缓冲区越大,单次可保留的事件数据越多,但内存占用也越大。通常建议通过memorysize统一调整 - 运行时动态修改:不可以在运行时修改。只能在 JFR 初始化前通过
-XX:FlightRecorderOptions设置。如果 JFR 已初始化,通过jcmd JFR.configure修改此选项会被忽略(参数不会被传递到 Java 层) - 相关 JBS:
- JDK-8213015:修复了
JFR.configure和-XX:FlightRecorderOptions之间的设置不一致问题,统一了globalbuffersize等选项的值格式和默认值(Fix Version: 12)- https://bugs.openjdk.org/browse/JDK-8213015 - JDK-8203457:修复了在全局缓冲区满时,没有向记录器线程发送通知以刷新到磁盘的问题(Fix Version: 11)- https://bugs.openjdk.org/browse/JDK-8203457
- JDK-8242088:将互斥列表替换为并发替代方案,提高了缓冲区管理的并发性能(Fix Version: 15)- https://bugs.openjdk.org/browse/JDK-8242088
- JDK-8213015:修复了
numglobalbuffers:JDK 11 引入,默认值:20,作用:Number of global buffers(全局缓冲区数量):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:全局缓冲区的数量(最小值:2)。缓冲区数量越多,并发写入能力越强,但内存占用也越大。与
globalbuffersize共同决定总内存占用。通常建议通过memorysize统一调整 - 运行时动态修改:不可以在运行时修改。只能在 JFR 初始化前通过
-XX:FlightRecorderOptions设置。如果 JFR 已初始化,通过jcmd JFR.configure修改此选项会被忽略(参数不会被传递到 Java 层)。 - 相关 JBS:
- JDK-8213015:修复了
JFR.configure和-XX:FlightRecorderOptions之间的设置不一致问题,统一了globalbuffercount等选项的值格式和默认值(Fix Version: 12)- https://bugs.openjdk.org/browse/JDK-8213015 - JDK-8203457:修复了在全局缓冲区满时,没有向记录器线程发送通知以刷新到磁盘的问题(Fix Version: 11)- https://bugs.openjdk.org/browse/JDK-8203457
- JDK-8242088:将互斥列表替换为并发替代方案,提高了缓冲区管理的并发性能(Fix Version: 15)- https://bugs.openjdk.org/browse/JDK-8242088
- JDK-8213015:修复了
memorysize:JDK 11 引入,默认值:10m,作用:Size of memory to be used by Flight Recorder(Flight Recorder 使用的内存大小):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:JFR 全局缓冲区的总内存大小(最小值:1M),计算公式:
memorysize = globalbuffersize * numglobalbuffers。注意:memorysize仅包含全局缓冲区的内存,不包含线程本地缓冲区的内存。线程本地缓冲区通过 C 堆独立分配,不受memorysize限制。JFR 实际占用的总内存 =memorysize+ 线程本地缓冲区内存(threadbuffersize * 活跃线程数,实际可能更少,因为缓冲区是动态分配的)。如果只设置此选项,JFR 会自动计算globalbuffersize和numglobalbuffers的最优组合 - 运行时动态修改:不可以在运行时修改。只能在 JFR 初始化前通过
-XX:FlightRecorderOptions设置。如果 JFR 已初始化,通过jcmd JFR.configure修改此选项会被忽略(参数不会被传递到 Java 层) - 相关 JBS:
- JDK-8213015:修复了
JFR.configure和-XX:FlightRecorderOptions之间的设置不一致问题,统一了memorysize等选项的值格式和默认值(Fix Version: 12)- https://bugs.openjdk.org/browse/JDK-8213015 - JDK-8203457:修复了在全局缓冲区满时,没有向记录器线程发送通知以刷新到磁盘的问题(Fix Version: 11)- https://bugs.openjdk.org/browse/JDK-8203457
- JDK-8242088:将互斥列表替换为并发替代方案,提高了缓冲区管理的并发性能(Fix Version: 15)- https://bugs.openjdk.org/browse/JDK-8242088
- JDK-8213015:修复了
threadbuffersize:JDK 11 引入,默认值:8k(或操作系统页大小,取较大值),作用:Thread buffer size(线程缓冲区大小):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:每个线程的本地缓冲区大小(范围:4K-2G,必须 ≤
globalbuffersize)。重要:threadbuffersize不受memorysize影响,线程本地缓冲区通过 C 堆(JfrCHeapObj)独立分配,是堆外内存。线程缓冲区用于暂存事件数据,满后刷新到全局缓冲区。缓冲区越大,线程本地可缓存的事件越多,减少全局缓冲区竞争,但每个线程的内存占用也越大。注意:线程本地缓冲区池的 free_list 最多缓存 8 个缓冲区(thread_local_cache_count = 8,写死在 JDK 源码中不可修改),这是缓存限制而非线程数限制。当活跃线程数超过 8 个时,多出的线程会直接分配新缓冲区;当缓冲区释放时,如果 free_list 已满(8 个),则直接释放内存而非缓存。JFR 实际占用的总内存 =memorysize+ 线程本地缓冲区内存(threadbuffersize * 活跃线程数,实际可能更少,因为缓冲区是动态分配的) - 运行时动态修改:不可以在运行时修改。只能在 JFR 初始化前通过
-XX:FlightRecorderOptions设置。如果 JFR 已初始化,通过jcmd JFR.configure修改此选项会被忽略(参数不会被传递到 Java 层) - 相关 JBS:
- JDK-8213015:修复了
JFR.configure和-XX:FlightRecorderOptions之间的设置不一致问题,统一了thread_buffer_size等选项的值格式和默认值(Fix Version: 12)- https://bugs.openjdk.org/browse/JDK-8213015
- JDK-8213015:修复了
maxchunksize:JDK 11 引入,默认值:12m,作用:Maximum size of a single repository disk chunk(单个 repository 磁盘块的最大大小):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:单个 chunk 文件的最大大小(最小值:1M)。当 chunk 达到此大小时会触发轮转(rotation),创建新的 chunk 文件。值越大,chunk 文件越少,但单文件越大;值越小,chunk 文件越多,但便于分段分析。影响磁盘写入频率和文件管理
- 运行时动态修改:不可以在运行时修改。只能在 JFR 初始化前通过
-XX:FlightRecorderOptions设置。如果 JFR 已初始化,通过jcmd JFR.configure修改此选项会被忽略(参数不会被传递到 Java 层) - 相关 JBS:
- JDK-8213015:修复了
JFR.configure和-XX:FlightRecorderOptions之间的设置不一致问题,统一了maxchunksize等选项的值格式和默认值(Fix Version: 12)- https://bugs.openjdk.org/browse/JDK-8213015 - JDK-8243452:修复了在有超过 200 个记录时,无法在存储库中创建 chunk 的问题(Fix Version: 15)- https://bugs.openjdk.org/browse/JDK-8243452
- JDK-8240783:修复了 TestClose 测试无法完成 chunk 的问题(Fix Version: 15)- https://bugs.openjdk.org/browse/JDK-8240783
- JDK-8236487:修复了 JFR Recorder Thread 崩溃的问题,崩溃发生在 chunk writer 无效时(Fix Version: 14)- https://bugs.openjdk.org/browse/JDK-8236487
- JDK-8213015:修复了
samplethreads:JDK 11 引入,JDK 19 废弃(但仍可用),默认值:true,作用:Thread sampling enable / disable (only sampling when event enabled and sampling enabled)(线程采样启用/禁用):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 废弃:JDK-8259774: Deprecate -XX:FlightRecorderOptions:samplethreads (Fix Version: 19) - https://bugs.openjdk.org/browse/JDK-8259774
- 影响说明:控制是否启用线程采样。当为 false 时,即使启用了需要采样的事件(如 ExecutionSample),也不会进行线程采样。JDK 19 后建议使用
-XX:StartFlightRecording:method-profiling替代 - 相关 JBS:
- JDK-8203635:修复了 JFR 采样器线程不记录堆栈信息的问题,这对于 NMT(Native Memory Tracking)很重要(Fix Version: 11)- https://bugs.openjdk.org/browse/JDK-8203635
- JDK-8215727:恢复了 JFR 线程采样器循环的旧行为,确保采样器能够收集到足够的样本(Fix Version: 13)- https://bugs.openjdk.org/browse/JDK-8215727
- JDK-8240819:为 JfrThreadSampler 线程分配一个名称,以便于调试(Fix Version: 15)- https://bugs.openjdk.org/browse/JDK-8240819
- JDK-8288663:修复了禁用 JfrThreadSampler 时只提交部分禁用状态的问题(Fix Version: 20)- https://bugs.openjdk.org/browse/JDK-8288663
- JDK-8367953:修复了 JFR 采样器线程不出现在线程转储中的问题(Fix Version: 26)- https://bugs.openjdk.org/browse/JDK-8367953
stackdepth:JDK 11 引入,默认值:64,作用:Stack depth for stacktraces (minimum 1, maximum 2048)(堆栈跟踪的堆栈深度):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:堆栈跟踪的最大深度(范围:1-2048)。深度越大,堆栈信息越完整,但占用内存和 CPU 开销也越大(堆栈遍历成本)。深度过小可能丢失关键调用信息。需要平衡信息完整性和性能开销
- 运行时动态修改:不可以在运行时修改。只能在 JFR 初始化前通过
-XX:FlightRecorderOptions设置。如果 JFR 已初始化,通过jcmd JFR.configure修改此选项会被忽略(参数不会被传递到 Java 层)。JDK 17 修复了此契约的强制执行(JDK-8278419) - 相关 JBS:
- JDK-8278419:修复了 jcmd JFR.configure 命令中某些选项的契约未强制执行的问题,
stackdepth在 JFR 初始化后无法更改(Fix Version: 17)- https://bugs.openjdk.org/browse/JDK-8278419 - JDK-8249713:修复了 java.base 事件的堆栈跟踪不完整的问题,堆栈跟踪的 skip level 设置过大(Fix Version: 15)- https://bugs.openjdk.org/browse/JDK-8249713
- JDK-8278419:修复了 jcmd JFR.configure 命令中某些选项的契约未强制执行的问题,
retransform:JDK 11 引入,默认值:true,作用:If event classes should be instrumented using JVMTI (by default true)(是否使用 JVMTI 对事件类进行插桩):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:
- retransform=true(默认):使用 JVMTI RetransformClasses 功能,可在类加载后动态对事件类进行插桩,支持动态启用事件和方法追踪(Method Tracing)功能
- retransform=false:使用"急切插桩"(eager instrumentation),仅在类初始加载时插桩;已加载但未插桩的事件类无法再插桩,方法追踪的新过滤器会被忽略,可能影响动态启用事件的能力
- 运行时动态修改:不可以在运行时修改。只能在 JFR 初始化前通过
-XX:FlightRecorderOptions设置。此选项不在jcmd JFR.configure命令的支持列表中
old-object-queue-size:JDK 11 引入,默认值:256,作用:Maximum number of old objects to track(要跟踪的旧对象的最大数量):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:OldObjectSample 事件跟踪的旧对象最大数量。值越大,可跟踪的潜在内存泄漏对象越多,但内存占用也越大(每个对象需要存储引用链信息)。设置为 0 可禁用 OldObjectSample 事件,节省内存
- 运行时动态修改:不可以在运行时修改。只能在 JFR 初始化前通过
-XX:FlightRecorderOptions设置。此选项不在jcmd JFR.configure命令的支持列表中 - 相关 JBS:
- JDK-8212232:修复了 OldObjectSample 事件的 cutoff 配置元数据错误,元数据显示为 BooleanFlag 类型但应该是 Timespan 类型(Fix Version: 12)- https://bugs.openjdk.org/browse/JDK-8212232
- JDK-8214542:修复了在深度堆上 Old Object Sample 事件在调试构建中运行缓慢的问题(Fix Version: 13)- https://bugs.openjdk.org/browse/JDK-8214542
- JDK-8313394:修复了 OldObjectSample 事件中 Array Elements 字段描述不正确的问题(Fix Version: 21)- https://bugs.openjdk.org/browse/JDK-8313394
- JDK-8330215:优化了 OldObjectSamples 的工作集,减少内存占用(Fix Version: 22)- https://bugs.openjdk.org/browse/JDK-8330215
dumppath:JDK 17 引入,默认值:null(实际使用当前工作目录,即 JVM 启动时的当前目录),作用:Path to emergency dump(紧急转储路径):- 引入:JDK-8271949: dumppath in -XX:FlightRecorderOptions does not affect (Fix Version: 17) - https://bugs.openjdk.org/browse/JDK-8271949
- 影响说明:指定紧急转储文件的保存目录。当 JVM 发生异常关闭(crash、OOM、栈溢出等)时,JFR 会将 repository 中的所有数据合并写入紧急转储文件(文件名格式:
hs_err_pid<pid>.jfr、hs_oom_pid<pid>.jfr或hs_soe_pid<pid>.jfr)。如果指定目录无法写入,会自动回退到当前工作目录。这对于在异常情况下保存 JFR 数据用于问题诊断非常重要 - 紧急转储详解:
- 作用:在 JVM 崩溃或异常关闭时,自动将 repository 中的所有 JFR chunk 文件按时间顺序合并写入一个紧急转储文件,确保在异常情况下也能保存 JFR 数据用于问题诊断
- 触发条件:
- VM 错误时:通过
JfrEmergencyDump::on_vm_error()触发(在JfrRepository::on_vm_error()中调用),当 JVM 遇到严重错误(如崩溃、断言失败等)时触发 - VM 关闭时:通过
JfrEmergencyDump::on_vm_shutdown()触发(在Jfr::on_vm_shutdown()中调用,在java.cpp的before_exit()中调用),当 JVM 正常关闭时也会触发 - 注意:如果 WatcherThread 崩溃,不会生成紧急转储(因为 WatcherThread 是安全网,用于在紧急转储死锁时超时退出)
- VM 错误时:通过
- 转储内容:
- Repository 数据:将 repository 目录中的所有
.jfrchunk 文件按时间顺序(ISO8601 时间戳)合并写入紧急转储文件 - 事件数据:在
on_vm_shutdown()时,还会触发以下事件并写入转储文件:EventDumpReason:记录转储原因(reason字段为 “Out of Memory” 或 “Crash”)EventShutdown:如果emit_event_shutdown=true,记录 VM 关闭事件- Old Object Samples:如果
emit_old_object_samples=true,发出旧对象样本(用于 OOM 分析)
- Repository 数据:将 repository 目录中的所有
- 文件命名规则:
- 一般错误:
hs_err_pid<pid>.jfr(默认情况,包括崩溃、断言失败等) - 内存不足(OOM):
hs_oom_pid<pid>.jfr(当JfrJavaSupport::cause()返回OUT_OF_MEMORY时) - 栈溢出(SOE):
hs_soe_pid<pid>.jfr(当JfrJavaSupport::cause()返回STACK_OVERFLOW时)
- 一般错误:
- 转储路径:
- 如果设置了
dumppath,会在该目录下创建紧急转储文件 - 如果未设置
dumppath或设置失败,会回退到当前工作目录 - 如果指定目录无法写入,会记录警告并尝试在当前目录创建
- 如果设置了
- 实现细节:
- 使用 1MB 的缓冲区块进行文件复制,避免内存不足
- 在转储过程中会释放所有持有的锁(Threads_lock、Module_lock、Heap_lock 等),避免死锁
- 使用重入保护机制(
guard_reentrancy()),确保同一时间只有一个线程执行紧急转储 - 如果紧急转储过程中发生死锁,WatcherThread 会作为安全网在超时后强制退出 JVM
- 运行时动态修改:可以在运行时通过
jcmd <pid> JFR.configure dumppath=<path>或 JMX 修改 - 相关 JBS:
- JDK-8206254:修复了在安全点(safepoint)期间无法完成 JFR 紧急转储的问题(Fix Version: 11)- https://bugs.openjdk.org/browse/JDK-8206254
- JDK-8233706:修复了紧急转储在错误报告之前执行的问题,现在紧急转储在错误报告(如 NMT 报告、CI Replay)之后执行(Fix Version: 15)- https://bugs.openjdk.org/browse/JDK-8233706
- JDK-8235390:修复了
JfrEmergencyDump::on_vm_shutdown崩溃的问题,在 VM 关闭过程中安全地完成转储(Fix Version: 14)- https://bugs.openjdk.org/browse/JDK-8235390 - JDK-8249878:修复了紧急转储中的二次崩溃问题,改进了对非 JavaThread(如 VMThread)的处理(Fix Version: 16)- https://bugs.openjdk.org/browse/JDK-8249878
- JDK-8282947:修复了在某些条件下,关闭时转储(dump on shutdown)导致活锁的问题(Fix Version: 20)- https://bugs.openjdk.org/browse/JDK-8282947
preserve-repository:JDK 21 引入,默认值:false,作用:Preserve disk repository after JVM exit(JVM 退出后保留磁盘 repository):- 引入:JDK-8303229: JFR: Preserve disk repository after exit (Fix Version: 21) - https://bugs.openjdk.org/browse/JDK-8303229
- 影响说明:控制 JVM 退出时是否保留 repository 目录中的 chunk 文件。为 true 时,JVM 正常退出后 repository 文件不会被清理,便于后续分析;为 false 时,JVM 退出时会自动清理 repository,避免磁盘空间占用
- 运行时动态修改:可以在运行时通过
jcmd <pid> JFR.configure preserve-repository=<true|false>或 JMX 修改
sampleprotection:JDK 11 引入(仅 DEBUG 模式),默认值:false(DEBUG 模式)/true(非 DEBUG 模式),作用:Safeguard for stackwalking while sampling threads (false by default)(采样线程时堆栈遍历的保护措施):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:仅在 DEBUG 构建中可用,用于在采样线程时保护堆栈遍历操作。DEBUG 模式下默认为 false,非 DEBUG 模式下默认为 true。主要用于开发和调试场景,生产环境通常不可用
- 是否启用 JFR,不启用则连模块都不会加载:
- 记录级别配置:控制单个记录的事件配置和存储选项,一个 JVM 进程可以有多个记录同时运行,每个记录配置可以独立设置,互不影响
- 单个记录配置:通过
-XX:StartFlightRecording=指定,或者通过jcmd <pid> JFR.start命令修改,或者通过 JDK API 指定,或者通过 JMX 指定settings:JDK 11 引入,默认值:default.jfc,作用:Settings file(s) that identifies which events to record(事件配置文件):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:指定 JFR 事件配置文件(.jfc 文件),用于确定要记录哪些事件及其配置。可以指定多个配置文件,使用 settings 参数重复指定。如果文件不在 JAVA_HOME/lib/jfr 目录中,需要包含完整路径。可以使用 “none” 来启动不包含预定义配置的记录。默认使用 default.jfc,提供低开销的预定义事件集,适合生产环境持续使用。每个记录可以独立设置,互不影响
- 运行时动态修改:可以在记录运行时通过
Recording.setSettings()或 JMX 修改。修改会立即生效,如果记录正在运行(RUNNING 状态),会调用recorder.updateSettings(true)更新事件配置 - 相关 JBS:
- JDK-8216064:修复了
-XX:StartFlightRecording:settings=选项不能正常工作的问题,改进了错误处理并支持 “none” 值(Fix Version: 13)- https://bugs.openjdk.org/browse/JDK-8216064
- JDK-8216064:修复了
- 单个事件的配置:
enabled- JDK 11 引入- 作用:控制事件是否被记录
- 适用事件:所有事件类型
- 运行时动态修改:可以在记录运行时通过
Recording.setSettings()或 JMX 修改,修改会立即生效
threshold- JDK 11 引入- 作用:指定持续时间阈值,低于此阈值的事件不记录
- 适用事件:仅对有持续时间(Duration)的事件有效
- 运行时动态修改:可以在记录运行时通过
Recording.setSettings()或 JMX 修改,修改会立即生效 - 示例:
jdk.ThreadSleep#threshold=20 ms表示只记录睡眠时间超过 20ms 的线程睡眠事件
period- JDK 11 引入- 作用:指定周期性事件的采集间隔
- 默认值:
"everyChunk"(每次 chunk 轮转时采集) - 适用事件:仅对周期性事件(Periodic Events)有效
- 配置值:
- Chunk 相关值:
"everyChunk"(默认值):在每次 chunk 轮转时采集事件,包括开始和结束。事件数量取决于 chunk 轮转次数。如果记录期间没有轮转,至少采集一次。适用于统计类事件(如jdk.ClassLoadingStatistics、jdk.ThreadAllocationStatistics)"beginChunk":只在 chunk 开始轮转时采集。每个新 chunk 开始时采集一次"endChunk":只在 chunk 结束轮转时采集。每个 chunk 结束时采集一次
- 时间间隔值(固定频率):
- 格式:
<数字> <单位> - 支持的单位:
ns(纳秒)、us(微秒)、ms(毫秒)、s(秒)、m(分钟)、h(小时)、d(天) - 示例:
"20 ms"(每 20 毫秒)、"1 s"(每秒)、"5 m"(每 5 分钟)、"1 h"(每小时) - 最小间隔:实际最小间隔为 1 毫秒,即使设置更小的值也会被调整为 1 毫秒
- 如果设置为
"0 ns"或Long.MAX_VALUE,会被视为禁用周期性采集
- 格式:
- Chunk 相关值:
- 值的合并规则(多个记录同时运行时):
- 如果指定了时间间隔值,优先使用最小的时间间隔
- 如果只指定了 chunk 相关值,按以下规则:
- 同时指定
beginChunk和endChunk→ 等同于everyChunk - 只指定
beginChunk→ 返回beginChunk - 只指定
endChunk→ 返回endChunk - 默认返回
everyChunk
- 同时指定
- 示例:
jdk.CPULoad#period=1 s表示每秒采集一次 CPU 负载事件jdk.ClassLoadingStatistics#period=everyChunk表示每次 chunk 轮转时采集类加载统计信息
- 运行时动态修改:可以在记录运行时通过
Recording.setSettings()或 JMX 修改,修改会立即生效 - 注意事项:
period只对周期性事件有效:只有标注了@Period的事件才支持此配置- Chunk 轮转频率:
everyChunk、beginChunk、endChunk的采集频率取决于 chunk 轮转频率,而 chunk 轮转由maxchunksize等全局配置控制 - 性能影响:时间间隔越小,采集频率越高,性能开销越大
stackTrace- JDK 11 引入- 作用:控制是否在事件提交时采集堆栈跟踪
- 适用事件:所有事件类型(但是某些事件采集堆栈没有啥意义,比如定时事件)
- 运行时动态修改:可以在记录运行时通过
Recording.setSettings()或 JMX 修改,修改会立即生效
throttle- JDK 16 引入- 作用:控制事件的最大采集速率(每秒/每分钟等)
- 适用事件:所有事件类型
- 运行时动态修改:可以在记录运行时通过
Recording.setSettings()或 JMX 修改,修改会立即生效 - 示例:
jdk.ObjectAllocationSample#throttle=100/s表示每秒最多采集 100 个对象分配采样事件
filter- JDK 25 引入- 引入:JDK-8352738: Implement JEP 520: JFR Method Timing and Tracing (Fix Version: 25) - https://bugs.openjdk.org/browse/JDK-8352738
- 作用:指定方法过滤器,用于过滤要记录的方法
- 适用事件:仅对特定事件有效,主要是:
jdk.MethodTrace(方法追踪事件)jdk.MethodTiming(方法计时事件)
- 运行时动态修改:可以在记录运行时通过
Recording.setSettings()或 JMX 修改,修改会立即生效 - 语法:支持类名、方法名、注解等过滤规则
cutoff- JDK 11 引入- 作用:限制查找 GC root 路径的最大时间,用于控制
jdk.OldObjectSample事件中查找对象引用链的时间开销 - 默认值:
"infinity"(无时间限制) - 适用事件:仅对
jdk.OldObjectSample事件有效 - 配置值:
- 时间间隔格式:
"1 h"、"0 ns"、"infinity"等 "0 ns":不查找 GC root 路径,只记录对象本身(无引用链信息),性能开销最小"infinity":无时间限制,完整查找 GC root 路径- 其他时间值:限制查找路径的最大时间,超过时间限制可能无法获取完整的引用链
- 时间间隔格式:
- 与
path-to-gc-roots的关系:- 当
path-to-gc-roots=true时,cutoff自动设置为"infinity" - 当
path-to-gc-roots=false时,cutoff自动设置为"0 ns"
- 当
- 运行时动态修改:可以在记录运行时通过
Recording.setSettings()或 JMX 修改,修改会立即生效 - 示例:
jdk.OldObjectSample#cutoff=1 h表示查找 GC root 路径的最大时间为 1 小时 - 注意:
cutoff不直接过滤对象年龄,而是限制查找引用链的时间。对象年龄通过事件的objectAge字段记录
- 作用:限制查找 GC root 路径的最大时间,用于控制
level- JDK 22 引入- 引入:JDK-8211238: @Deprecated JFR event (Fix Version: 22) - https://bugs.openjdk.org/browse/JDK-8211238
- 作用:控制
jdk.DeprecatedInvocation事件记录哪些废弃方法调用 - 默认值:
"forRemoval" - 适用事件:仅对
jdk.DeprecatedInvocation事件有效 - 配置值:
"forRemoval"(level 0,默认值):只记录标记为@Deprecated(forRemoval=true)的方法调用,开销较小"all"(level 1):记录所有@Deprecated方法调用(包括forRemoval=false),信息更全但开销更大
- 运行时动态修改:可以在记录运行时通过
Recording.setSettings()或 JMX 修改,修改会立即生效 - 示例:
jdk.DeprecatedInvocation#level=all表示记录所有废弃方法调用 - 注意:GC Phase 相关事件(如
jdk.GCPhasePauseLevel1、jdk.GCPhasePauseLevel2等)的 level 是通过不同的事件名称区分的,不是通过level配置项控制
name:JDK 11 引入,默认值:null(系统生成),作用:Name that can be used to identify recording(记录名称):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:指定记录的标识名称,用于在多个记录中区分不同的记录。如果不指定名称,系统会自动生成一个名称。记录名称不能为纯数字,以避免与记录 ID 混淆。名称用于通过 jcmd 命令管理记录(如 JFR.dump、JFR.stop 等)。每个记录可以独立设置,互不影响
- 运行时动态修改:可以在记录运行时通过
Recording.setName()或 JMX 修改,只要记录不是 CLOSED 状态即可修改
delay:JDK 11 引入,默认值:0s,作用:Length of time to wait before starting to record(延迟启动时间):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:指定在 JVM 启动后等待多长时间再开始记录。时间格式:整数后跟 ’s’(秒)、’m’(分钟)、‘h’(小时)或 ’d’(天)。最小值为 1 秒。延迟启动可以用于跳过应用启动阶段,只记录应用运行稳定后的数据,减少记录文件大小和启动开销。每个记录可以独立设置,互不影响
- 运行时动态修改:不可以在记录运行时修改。只能在记录创建时(NEW 状态)通过
Recording.scheduleStart()设置,一旦记录进入 DELAYED 或 RUNNING 状态后无法修改
duration:JDK 11 引入,默认值:0s(无限制),作用:Length of time to record(自动停止时间):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:指定记录的持续时间,时间格式:整数后跟 ’s’(秒)、’m’(分钟)、‘h’(小时)或 ’d’(天)。0s 表示无限制,记录会一直运行直到手动停止。最小值为 1 秒。当达到指定时间时,记录会自动停止。如果同时指定了 filename,记录停止后会自动转储到文件。每个记录可以独立设置,互不影响
- 运行时动态修改:可以在记录运行时通过
Recording.setDuration()或 JMX 修改,只要记录不是 STOPPED 或 CLOSED 状态即可修改。修改后会重新计算停止时间并更新定时器
filename:JDK 11 引入,默认值:null(系统生成),作用:Name of the file to which the flight recording data is written when the recording is stopped(dump 时的文件名):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:指定记录停止时转储的文件路径和名称。如果未指定,系统会根据 PID 和当前日期自动生成文件名。如果指定的是目录,会在该目录中生成文件名。文件名中可以使用 %p(PID)和 %t(时间戳,格式:yyyy_MM_dd_HH_mm_ss)占位符。如果指定了 filename,默认 dumponexit=true。每个记录可以独立设置,互不影响
- 运行时动态修改:可以在记录运行时通过
Recording.setDestination()或 JMX 修改,只要记录不是 STOPPED 或 CLOSED 状态即可修改 - 相关 JBS:
- JDK-8323425:修复了自动生成的文件名在时间限制记录中不工作的问题(Fix Version: 23)- https://bugs.openjdk.org/browse/JDK-8323425
maxage:JDK 11 引入,默认值:0s(无限制),作用:Maximum time to keep the recorded data on disk(最大保留时间):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:指定磁盘上记录数据的最大保留时间,时间格式:整数后跟 ’s’(秒)、’m’(分钟)、‘h’(小时)或 ’d’(天)。0s 表示无限制。此参数仅在 disk=true 时有效。超过指定时间的旧 chunk 文件会被自动删除,用于控制磁盘空间占用。与 maxsize 配合使用可以同时限制时间和大小。注意:虽然不同记录的 chunk 文件存储在同一个 repository 目录中,但每个记录通过自己的 chunks 列表跟踪属于它的 chunk,每个记录独立应用 maxage 限制,只删除自己不再需要的 chunk(通过引用计数机制管理)
- 运行时动态修改:可以在记录运行时通过
Recording.setMaxAge()或 JMX 修改,只要记录不是 CLOSED 状态即可修改。修改后会立即触发trimToAge()清理超过保留时间的旧 chunk - 相关 JBS:
- JDK-8203929:为
JFR.dump命令添加了maxage参数,允许限制转储的数据量(Fix Version: 11)- https://bugs.openjdk.org/browse/JDK-8203929 - JDK-8294242:修复了
jfr print命令处理无限持续时间的问题,当 maxAge 为无限时显示更友好(Fix Version: 20)- https://bugs.openjdk.org/browse/JDK-8294242
- JDK-8203929:为
maxsize:JDK 11 引入,默认值:0(无限制),作用:Maximum size of the data to keep on disk(最大总大小):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:指定磁盘上记录数据的最大总大小,支持 ‘k’/‘K’(KB)、’m’/‘M’(MB)或 ‘g’/‘G’(GB)后缀。0 表示无限制。此参数仅在 disk=true 时有效。值不能小于全局配置中的 maxchunksize。当达到指定大小时,最旧的 chunk 文件会被自动删除,用于控制磁盘空间占用。与 maxage 配合使用可以同时限制大小和时间。注意:虽然不同记录的 chunk 文件存储在同一个 repository 目录中,但每个记录通过自己的 chunks 列表跟踪属于它的 chunk,每个记录独立应用 maxsize 限制,只删除自己不再需要的 chunk(通过引用计数机制管理)
- 运行时动态修改:可以在记录运行时通过
Recording.setMaxSize()或 JMX 修改,只要记录不是 CLOSED 状态即可修改。修改后会立即触发trimToSize()清理超过大小限制的旧 chunk - 相关 JBS:
- JDK-8203929:为
JFR.dump命令添加了maxsize参数,允许限制转储的数据量(Fix Version: 11)- https://bugs.openjdk.org/browse/JDK-8203929
- JDK-8203929:为
dumponexit:JDK 11 引入,默认值:false,作用:Flag for writing the recording to disk when the JVM shuts down(JVM 退出时是否 dump):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:控制 JVM 退出时是否将记录转储到文件。为 true 时,JVM 正常退出或异常退出(如 OOM)时会自动转储记录。如果未指定 filename,会在进程启动目录生成系统文件名(格式:
id-<recording-id>-<timestamp>.jfr)。如果指定了 filename,默认 dumponexit=true。这对于在异常情况下保存 JFR 数据用于问题诊断非常重要。每个记录可以独立设置,互不影响 - 运行时动态修改:可以在记录运行时通过
Recording.setDumpOnExit()或 JMX 修改,没有状态限制,可以在任何时候修改 - 相关 JBS:
- JDK-8198337:修复了使用
-XX:StartFlightRecording=dumponexit=true,disk=false启动内存记录时,退出时转储的文件大小为 0 的问题(Fix Version: 11)- https://bugs.openjdk.org/browse/JDK-8198337 - JDK-8282947:修复了在某些条件下,关闭时转储(dump on shutdown)导致活锁的问题(Fix Version: 20)- https://bugs.openjdk.org/browse/JDK-8282947
- JDK-8198337:修复了使用
path-to-gc-roots:JDK 11 引入,默认值:false,作用:Flag for saving the path to garbage collection (GC) roots at the end of a recording(是否记录 GC 根):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:控制是否在记录结束时保存到 GC 根的路径信息。为 true 时,会在记录结束时收集 OldObjectSample 事件中对象的 GC 根路径信息,这对于查找内存泄漏非常有用,但收集过程耗时较长。如果 settings 参数设置为 ‘profile’,收集的信息还包括潜在泄漏对象的分配堆栈跟踪。建议仅在怀疑存在内存泄漏时启用。每个记录可以独立设置,互不影响
- 运行时动态修改:可以在记录运行时通过
Recording.setSettings()修改jdk.OldObjectSample#path-to-gc-roots设置或 JMX 修改,修改会立即生效。注意:修改path-to-gc-roots会自动同步修改jdk.OldObjectSample#cutoff设置(true → infinity,false → 0 ns)
report-on-exit:JDK 25 引入,默认值:null,作用:Generate report view(s) when the JVM shuts down(JVM 退出时生成报告视图):- 引入:JDK-8351266: JFR: -XX:StartFlightRecording:report-on-exit (Fix Version: 25) - https://bugs.openjdk.org/browse/JDK-8351266
- 影响说明:指定在 JVM 退出时生成的报告视图标识符。可以重复指定多个视图,例如
report-on-exit=jvm-information,report-on-exit=system-properties。报告视图用于在退出时自动生成特定格式的分析报告,便于快速查看关键信息。支持的视图包括 jvm-information、system-properties 等。每个记录可以独立设置,互不影响 - 运行时动态修改:不可以在记录运行时修改。只能在记录创建时通过
-XX:StartFlightRecording或jcmd JFR.start设置 - 相关 JBS:
- JDK-8359248:改进了
-XX:StartFlightRecording:report-on-exit选项的帮助文本,说明该值可以重复(Fix Version: 26)- https://bugs.openjdk.org/browse/JDK-8359248
- JDK-8359248:改进了
- 记录级别但影响全局的配置:每个记录可以独立设置,但实际影响是全局的,多个记录共享底层资源
disk:JDK 11 引入,默认值:true,作用:Flag for also writing the data to disk while recording(是否保存到磁盘):- 引入:JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712
- 影响说明:控制记录是否写入磁盘。为 true 时,数据会持续写入磁盘 repository,支持 maxage 和 maxsize 限制;为 false 时,数据仅存储在内存中,适合短期记录或内存受限场景。内存记录在 JVM 退出或手动转储时才会写入文件。重要:虽然每个记录可以独立设置 disk 参数,但实际影响是全局的。如果任何一个运行中的记录设置为 disk=true,系统就会创建全局的 chunk 文件并写入磁盘 repository。所有运行中的记录共享同一个物理 chunk 文件(
PlatformRecorder.currentChunk),当一个 chunk 完成时,会分配给所有运行中的记录(通过finishChunk()方法)。这意味着即使某个记录设置为 disk=false,如果其他记录设置为 disk=true,该记录也会从共享的 chunk 中获取数据。影响磁盘 I/O 性能和存储空间管理 - 运行时动态修改:不可以在记录运行时修改。只能在记录创建时(NEW 或 DELAYED 状态)通过
Recording.setToDisk()设置,一旦记录进入 RUNNING 状态后无法修改 - 相关 JBS:
- JDK-8198337:修复了使用
-XX:StartFlightRecording=dumponexit=true,disk=false启动内存记录时,退出时转储的文件大小为 0 的问题(Fix Version: 11)- https://bugs.openjdk.org/browse/JDK-8198337 - JDK-8304844:修复了 ActiveRecording 事件缺少 disk 参数的问题(Fix Version: 22)- https://bugs.openjdk.org/browse/JDK-8304844
- JDK-8198337:修复了使用
flush-interval:JDK 17 引入,默认值:1s,作用:Minimum time before flushing buffers(定时 flush 间隔):- 引入:JDK-8265036: JFR: Add flush-interval option (Fix Version: 17) - https://bugs.openjdk.org/browse/JDK-8265036
- 影响说明:指定将缓冲区数据刷新到磁盘的最小时间间隔,时间格式:整数后跟 ’s’(秒)、’m’(分钟)、‘h’(小时)或 ’d’(天)。0 表示仅在记录结束时刷新。此参数仅在 disk=true 时有效。较小的间隔可以减少数据丢失风险,但会增加磁盘 I/O 开销;较大的间隔可以减少 I/O 开销,但可能在异常退出时丢失更多数据。需要平衡数据安全性和性能开销。重要:虽然每个记录可以独立设置 flush-interval 参数,但实际影响是全局的。系统使用所有运行中记录的最小 flush-interval 值作为全局刷新间隔(通过
Math.min()计算)。这是因为所有记录共享同一个物理 chunk 文件,只能有一个全局的刷新间隔。FlushTask是全局单例,所有记录共享同一个刷新任务。当启动或停止记录时,系统会重新计算所有运行中记录的最小 flush-interval,并更新全局刷新间隔 - 运行时动态修改:可以在记录运行时通过
Recording.setFlushInterval()或 JMX 修改,只要记录不是 CLOSED 状态即可修改。修改后系统会重新计算所有运行中记录的最小 flush-interval,并更新全局刷新间隔
- 单个记录配置:通过
1.4. 事件类型分类与配置适用性#
1.4.1. 按采集方式分类#
同步事件(Synchronous Events)
- 特点:在业务线程中直接提交,如
jdk.ThreadStart、jdk.ThreadEnd - 适用配置:
enabled、stackTrace、threshold(如果有持续时间)、throttle、filter(如果是方法相关事件)
- 特点:在业务线程中直接提交,如
异步事件(Asynchronous Events)
- 特点:在后台线程中采集,如采样事件
- 适用配置:
enabled、stackTrace、throttle
周期性事件(Periodic Events)
- 特点:按固定间隔采集,如
jdk.CPULoad、jdk.ClassLoadingStatistics - 适用配置:
enabled、period、stackTrace(但是没有意义,因为周期触发堆栈我们不关心)、throttle(也没有意义,周期就能控制频率了) - 特殊说明:
period配置只对这类事件有效
- 特点:按固定间隔采集,如
请求式事件(Requestable Events)
- 特点:按需触发,如 chunk 开始/结束事件
- 适用配置:
enabled、stackTrace
1.4.2. 按事件特性分类#
持续时间事件(Duration Events)
- 特点:有明确的开始和结束时间,如
jdk.ThreadSleep、jdk.GCPhase - 适用配置:
enabled、threshold、stackTrace、throttle - 特殊说明:
threshold配置只对这类事件有效
- 特点:有明确的开始和结束时间,如
瞬时事件(Instant Events)
- 特点:没有持续时间,如
jdk.ThreadStart、jdk.ClassLoad,另外上面的周期性事件和请求式事件也属于这里的瞬时事件 - 适用配置:
enabled、stackTrace、throttle - 特殊说明:不支持
threshold配置
- 特点:没有持续时间,如
采样事件(Sampling Events)
- 特点:通过采样机制采集,如
jdk.ExecutionSample、jdk.ObjectAllocationSample - 适用配置:
enabled、stackTrace、throttle - 特殊说明:通常使用
throttle控制采样频率
- 特点:通过采样机制采集,如
1.4.3. 按照实现层面分类#
JDK Java 层面事件(JDK Java-level Events)
- 特点:事件类定义在 JDK 的 Java 代码中,继承自
AbstractJDKEvent或jdk.jfr.Event - 实现位置:
- 事件类定义:
src/jdk.jfr/share/classes/jdk/jfr/events/包中 - 事件触发:在
jdk.jfr.internal.JDKEvents类中实现,或通过 Java 代码直接调用Event.commit()
- 事件类定义:
- 典型事件:
jdk.ActiveRecording:记录 JFR 记录配置信息jdk.ActiveSetting:记录事件配置设置jdk.ThreadStart、jdk.ThreadEnd:线程生命周期事件jdk.ClassLoad、jdk.ClassUnload:类加载/卸载事件jdk.FileRead、jdk.FileWrite:文件 I/O 事件(通过 JVMTI 插桩)jdk.SocketRead、jdk.SocketWrite:网络 I/O 事件(通过 JVMTI 插桩)
- 实现机制:
- 在 Java 代码的关键位置直接调用
Event.commit()提交事件 - 对于 I/O 相关事件,通过 JVMTI 在类加载时或运行时进行字节码插桩,在插桩代码中调用事件提交
- 事件类通过 JVMTI RetransformClasses 或 Eager Instrumentation 机制进行插桩
- 在 Java 代码的关键位置直接调用
- 优势:实现简单,易于维护和扩展;可以访问 Java 层的丰富信息
- 适用场景:JDK 库中的事件、应用程序自定义事件
- 特点:事件类定义在 JDK 的 Java 代码中,继承自
JVM C++ 层面事件(JVM C++-level Events)
- 特点:事件定义在 JVM 的 C++ 代码中,在 JVM 内部的关键点直接调用 JFR API
- 实现位置:
- 事件元数据定义:
src/hotspot/share/jfr/metadata/metadata.xml - 事件实现:JVM 的 C++ 代码中(
src/hotspot/share/jfr/目录下)
- 事件元数据定义:
- 子分类:
- 周期性事件(Periodic Events):
- 实现位置:
src/hotspot/share/jfr/periodic/jfrPeriodic.cpp - 实现方式:通过
TRACE_REQUEST_FUNC宏定义,在周期性任务中触发 - 典型事件:
jdk.JVMInformation:JVM 信息jdk.CPULoad:CPU 负载jdk.ClassLoadingStatistics:类加载统计jdk.ThreadAllocationStatistics:线程分配统计jdk.GCConfiguration、jdk.GCHeapConfiguration:GC 配置信息
- 实现位置:
- JVM 内部钩子事件(JVM Internal Hook Events):
- 实现方式:在 JVM 的关键执行路径中直接调用 JFR API
- 典型事件:
jdk.GCPhase:GC 阶段事件(在 GC 代码中触发)jdk.GCHeapSummary:GC 堆摘要(在 GC 代码中触发)jdk.ObjectAllocationInNewTLAB、jdk.ObjectAllocationOutsideTLAB:对象分配事件(在对象分配路径中触发)jdk.MonitorWait、jdk.MonitorWaited:锁等待事件(在同步代码中触发)jdk.ThreadSleep、jdk.ThreadPark:线程睡眠/挂起事件(在线程代码中触发)
- 周期性事件(Periodic Events):
- 优势:可以访问 JVM 内部状态,性能开销低(直接调用,无需 JNI 开销)
- 适用场景:JVM 内部事件、GC 事件、线程事件、对象分配事件等
基于 JVMTI 插桩的事件(JVMTI Instrumentation-based Events)
- 特点:通过 JVMTI 在类加载时或运行时进行字节码插桩,在插桩代码中触发事件
- 实现位置:
- 插桩逻辑:
src/hotspot/share/jfr/instrumentation/jfrEventClassTransformer.cpp - JVMTI Agent:
src/hotspot/share/jfr/instrumentation/jfrJvmtiAgent.cpp - Java 层回调:
src/jdk.jfr/share/classes/jdk/jfr/internal/JVMUpcalls.java
- 插桩逻辑:
- 实现机制:
- Eager Instrumentation:在类初始加载时进行插桩(当
retransform=false时) - Retransform Instrumentation:在类加载后通过 JVMTI RetransformClasses 进行插桩(当
retransform=true时,默认) - 插桩内容:在
Event.commit()方法中插入调用 JFR 缓冲区的代码
- Eager Instrumentation:在类初始加载时进行插桩(当
- 典型事件:
jdk.FileRead、jdk.FileWrite:文件 I/O 事件(插桩java.io.FileInputStream、FileOutputStream等)jdk.SocketRead、jdk.SocketWrite:网络 I/O 事件(插桩java.net.Socket、ServerSocket等)jdk.MethodTrace、jdk.MethodTiming:方法追踪/计时事件(JDK 25 引入,通过方法过滤器插桩)
- 优势:可以监控 JDK 库和应用程序代码的执行,无需修改源代码
- 适用场景:I/O 事件、方法追踪事件、需要监控应用程序代码的事件
基于采样机制的事件(Sampling-based Events)
- 特点:通过独立的采样线程定期采样目标线程的状态,生成事件
- 实现位置:
- 采样器实现:
src/hotspot/share/jfr/recorder/checkpoint/types/jfrThreadSampler.cpp - 采样线程:
JfrThreadSampler线程
- 采样器实现:
- 实现机制:
- 采样线程定期唤醒(默认间隔由
samplethreads配置控制) - 遍历所有 Java 线程,获取线程状态和堆栈跟踪
- 根据事件配置决定是否生成事件
- 采样线程定期唤醒(默认间隔由
- 典型事件:
jdk.ExecutionSample:方法执行采样事件(采样线程的堆栈跟踪)jdk.NativeMethodSample:本地方法采样事件(采样本地方法调用)
- 优势:开销低,不需要在业务代码中插桩,对性能影响小
- 适用场景:需要周期性采样线程状态的事件,如性能分析事件
混合实现事件(Hybrid Implementation Events)
- 特点:结合多种实现机制的事件
- 典型事件:
jdk.ObjectAllocationSample:- 分配路径触发:在对象分配路径中通过 C++ 代码触发(类似
jdk.ObjectAllocationInNewTLAB) - 采样机制:通过采样机制控制采集频率(通过
throttle配置)
- 分配路径触发:在对象分配路径中通过 C++ 代码触发(类似
jdk.OldObjectSample:- GC 触发:在 GC 过程中通过 C++ 代码识别旧对象
- 引用链查找:在 Java 层查找 GC root 路径(通过 Java 代码实现)
- 优势:结合不同实现机制的优势,实现复杂的事件采集逻辑
2. JFR 快速开始#
首先需要准备好如下工具或者环境:
- Java 25
- JDK Mission Control(JMC) 8.3+
- Jmeter
- 代码库:https://github.com/spring-projects/spring-petclinic.git
- maven 或者 gradle
git clone 下来代码库后,我们使用 maven 构建:
cd spring-petclinic
mvn clean package
然后,我们使用下面的 JVM 参数启动 spring-petclinic 应用:
-XX:StartFlightRecording=disk=true
-XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true
这里的配置含义是:
-XX:StartFlightRecording=disk=true:表示通过 JVM 参数 StartFlightRecording 启动一个 JFR 记录,并且将数据写入磁盘(即前面提到的 repository 目录)。后面我们会看到,可以使用多个StartFlightRecording参数启动多个记录。-XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true:表示配置 JFR 的全局选项,这里配置了chunk大小为 128MB,repository目录为当前目录,堆栈深度为 256,preserve-repository=true表示保留 repository 目录中的 chunk 文件(默认情况下,JFR 会在 JVM 退出时删除 repository 目录中的 chunk 文件)。
启动后,我们可以看到启动目录(我们指定为了当前目录)下有了下面结构的 repository 目录:
├── 2025_11_19_17_41_00_3833
│ ├── 2025_11_19_17_41_00.jfr
其中文件夹名称为 <yyyy_MM_dd_HH_mm_ss_pid> 格式,表示 JVM 启动时间和进程 ID,里面的 *.jfr 文件就是 JFR 记录的 chunk 文件,文件名为 <yyyy_MM_dd_HH_mm_ss>.jfr 格式,表示 chunk 生成的时间。
使用 jfr 命令可以查看文件的内容:
% jfr summary ./2025_11_19_17_41_00_3833/2025_11_19_17_41_00.jfr
Version: 2.1
Chunks: 1
Start: 2025-11-17 09:41:00 (UTC)
Duration: 165 s
Event Type Count Size (bytes)
=============================================================
jdk.NativeMethodSample 6992 79112
jdk.GCPhaseParallel 6127 150540
jdk.ModuleExport 1615 19253
jdk.SystemProcess 1032 119761
jdk.NativeLibrary 877 76574
......
如果你的进程在运行中,这个命令可能会提示 jfr 文件损坏(在你查看的是 repository 中的最后一个文件的时候),这个因为元数据还没有 flush 进去,不用担心,默认 1s 执行一次 flush,你多试几次就能看到正确的结果了。
查看某个事件的详情,可以使用 jfr print 命令:
% jfr print --events jdk.CPULoad ./2025_11_19_17_41_00_3833/2025_11_19_17_41_00.jfr
jdk.CPULoad {
startTime = 17:41:02.893 (2025-11-19)
jvmUser = 7.24%
jvmSystem = 0.93%
machineTotal = 20.19%
}
jdk.CPULoad {
startTime = 17:41:03.898 (2025-11-19)
jvmUser = 0.81%
jvmSystem = 0.25%
machineTotal = 14.56%
}
3. 通过 JVM 参数使用 JFR#
通过 JVM 参数使用 JFR 是最常用的方式,可以在 JVM 启动时直接配置和启动 JFR 记录。JFR 提供了两个主要的 JVM 参数:
- ``-XX:+FlightRecorder`:JDK 11 引入,默认值:false,这个配置已过期,并且虽然默认值是 false,但是如果没有设置,JVM 还是将其设置为 true,即默认启用 JFR 功能,只有在显式设置为 false 时才会禁用 JFR 功能
-XX:FlightRecorderOptions:配置 JFR 的全局选项-XX:StartFlightRecording:启动一个或多个 JFR 记录
3.1. -XX:FlightRecorderOptions(全局配置)#
-XX:FlightRecorderOptions 用于配置 JFR 的全局选项,这些选项影响所有 JFR 记录的行为。
语法:
-XX:FlightRecorderOptions=<option1>=<value1>,<option2>=<value2>,...
支持的选项(详见第 1.3 节对于配置的详细解析):
repository=<path>:Repository 路径(默认:临时目录)globalbuffersize=<size>:全局缓冲区大小(默认:512k)numglobalbuffers=<count>:全局缓冲区数量(默认:20)memorysize=<size>:JFR 占用总内存限制(默认:10m)threadbuffersize=<size>:线程缓冲区大小(默认:8k)maxchunksize=<size>:最大 Chunk 大小(默认:12m)stackdepth=<depth>:堆栈跟踪深度(默认:64)retransform=<true|false>:是否允许 JVMTI retransform(默认:true)old-object-queue-size=<size>:OldObjectSample 队列大小(默认:256)dumppath=<path>:紧急转储路径(JDK 17+,默认:当前工作目录)preserve-repository=<true|false>:JVM 退出时是否保留 Repository(JDK 21+,默认:false)sampleprotection=<true|false>:采样线程时堆栈遍历的保护措施(仅 DEBUG 模式)
示例:
# 配置全局选项
-XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true
# 配置内存相关选项
-XX:FlightRecorderOptions=memorysize=50m,globalbuffersize=1m,numglobalbuffers=50
# 配置缓冲区大小
-XX:FlightRecorderOptions=threadbuffersize=16k,globalbuffersize=1m
注意事项:
- 格式要求:选项之间使用逗号(
,)分隔,选项名和值之间使用等号(=)分隔 - 大小单位:内存大小支持
k/K(KB)、m/M(MB)、g/G(GB) - 路径格式:路径可以是绝对路径或相对路径(相对于当前工作目录)
- 运行时修改:部分选项(如
repository、maxchunksize)可以通过jcmd JFR.configure命令在运行时修改
3.2. -XX:StartFlightRecording(启动记录)#
-XX:StartFlightRecording 用于在 JVM 启动时启动一个或多个 JFR 记录。可以多次指定此参数来启动多个独立的记录。
语法:
-XX:StartFlightRecording[=<parameter1>=<value1>,<parameter2>=<value2>,...]
参数格式:
- 参数之间使用逗号(
,)分隔 - 参数名和值之间使用等号(
=)分隔 - 如果未指定任何参数,默认使用
dumponexit=false(即不自动转储)
支持的参数:
3.2.1. 基本参数#
name=<name>(可选):
- 作用:指定记录的标识名称
- 默认值:系统自动生成(格式:
Recording <id>) - 限制:名称不能为纯数字,以避免与记录 ID 混淆
- 示例:
name=MyRecording、name=Production-Monitoring
settings=<jfc-files>(可选):
- 作用:指定 JFC 配置文件(详见第 5 章)
- 默认值:
default.jfc - 格式:
- 单个文件:
settings=default、settings=profile、settings=/path/to/custom.jfc - 多个文件(合并):
settings=default,settings=profile(用逗号分隔,可重复指定) - 不使用配置:
settings=none
- 单个文件:
- 示例:
settings=defaultsettings=profilesettings=default,settings=/path/to/custom.jfcsettings=none
disk=<true|false>(可选):
- 作用:控制记录是否写入磁盘
- 默认值:
true - 说明:
true:数据持续写入磁盘 repository,支持maxage和maxsize限制false:数据仅存储在内存中,适合短期记录
- 示例:
disk=true、disk=false
3.2.2. 时间相关参数#
delay=<time>(可选):
- 作用:延迟启动时间
- 默认值:
0s(立即启动) - 格式:整数后跟单位(
s=秒、m=分钟、h=小时、d=天) - 最小值:
1s - 示例:
delay=5m(延迟 5 分钟)、delay=1h(延迟 1 小时)
duration=<time>(可选):
- 作用:自动停止时间
- 默认值:
0s(无限制) - 格式:整数后跟单位(
s=秒、m=分钟、h=小时、d=天) - 最小值:
1s - 说明:当达到指定时间时,记录会自动停止;如果同时指定了
filename,记录停止后会自动转储 - 示例:
duration=1h(记录 1 小时)、duration=30m(记录 30 分钟)
maxage=<time>(可选):
- 作用:最大保留时间(仅在
disk=true时有效) - 默认值:
0s(无限制) - 格式:整数后跟单位(
s=秒、m=分钟、h=小时、d=天) - 说明:超过指定时间的旧 chunk 文件会被自动删除
- 示例:
maxage=2d(保留 2 天)、maxage=24h(保留 24 小时)
flush-interval=<time>(可选,JDK 17+):
- 作用:定时 flush 间隔(仅在
disk=true时有效) - 默认值:
1s - 格式:整数后跟单位(
s=秒、m=分钟、h=小时、d=天) - 说明:
0表示仅在记录结束时刷新 - 示例:
flush-interval=5s、flush-interval=0(仅在结束时刷新)
3.2.3. 文件相关参数#
filename=<path>(可选):
- 作用:指定记录停止时转储的文件路径
- 默认值:系统自动生成(格式:
hotspot-pid-<pid>-id-<id>-<timestamp>.jfr) - 格式:
- 文件路径:
filename=/path/to/recording.jfr - 目录路径:
filename=/path/to/directory(会在该目录中生成文件名) - 占位符:
filename=recording-%p-%t.jfr(%p=PID,%t=时间戳yyyy_MM_dd_HH_mm_ss)
- 文件路径:
- 说明:如果指定了
filename,默认dumponexit=true - 示例:
filename=recording.jfrfilename=./recordings/filename=recording-%p-%t.jfr
dumponexit=<true|false>(可选):
- 作用:JVM 退出时是否 dump
- 默认值:
false(如果指定了filename,则为true) - 说明:为
true时,JVM 正常退出或异常退出时会自动转储记录 - 示例:
dumponexit=true
3.2.4. 其他参数#
maxsize=<size>(可选):
- 作用:最大总大小(仅在
disk=true时有效) - 默认值:
0(无限制,但如果duration、maxage、maxsize都未指定,默认使用250MB) - 格式:整数后跟单位(
k/K=KB、m/M=MB、g/G=GB) - 限制:值不能小于全局配置中的
maxchunksize - 示例:
maxsize=500M、maxsize=1G
path-to-gc-roots=<true|false>(可选):
- 作用:是否记录 GC 根
- 默认值:
false - 说明:为
true时,会在记录结束时收集 OldObjectSample 事件中对象的 GC 根路径信息,用于查找内存泄漏 - 示例:
path-to-gc-roots=true
report-on-exit=<view-names>(可选,JDK 25+):
- 作用:JVM 退出时生成报告视图
- 默认值:无(不生成报告)
- 格式:可以重复指定多个视图,用逗号分隔
- 说明:仅在
disk=true时有效 - 示例:
report-on-exit=jvm-information,report-on-exit=system-properties
3.2.5. 事件配置参数#
除了上述参数,还可以直接在 -XX:StartFlightRecording 中指定事件配置:
JFC 选项(<option>=<value>):
- 格式:
<jfc-option>=<value> - 说明:修改 JFC 文件中的选项(如
gc=high、method-profiling=high) - 示例:
gc=high、method-profiling=high
事件设置(<event-name>#<setting-name>=<value>):
- 格式:
<event-name>#<setting-name>=<value> - 说明:直接修改事件的配置项
- 添加新事件:使用
+前缀,如+jdk.CustomEvent#enabled=true - 示例:
jdk.ThreadSleep#threshold=50msjdk.CPULoad#period=2s+jdk.CustomEvent#enabled=true
时间间隔格式:
- 时间值可以省略空格,如
20ms等同于20 ms - 支持的单位:
ns(纳秒)、us(微秒)、ms(毫秒)、s(秒)、m(分钟)、h(小时)、d(天)
3.3. 使用示例#
3.3.1. 基本使用#
# 最简单的使用方式(使用默认配置)
-XX:StartFlightRecording
# 使用预定义配置
-XX:StartFlightRecording=settings=default
# 使用自定义配置并指定文件名
-XX:StartFlightRecording=settings=profile,filename=recording.jfr
# 指定记录名称和文件名
-XX:StartFlightRecording=name=MyRecording,filename=myrecording.jfr
3.3.2. 时间限制记录#
# 记录 1 小时后自动停止并转储
-XX:StartFlightRecording=duration=1h,filename=recording.jfr
# 延迟 5 分钟后开始记录,持续 30 分钟
-XX:StartFlightRecording=delay=5m,duration=30m,filename=recording.jfr
# 记录 1 小时,但保留最近 2 天的数据
-XX:StartFlightRecording=duration=1h,maxage=2d,maxsize=5GB
3.3.3. 内存记录(不写入磁盘)#
# 内存记录,仅在退出时转储
-XX:StartFlightRecording=disk=false,dumponexit=true,filename=recording.jfr
# 内存记录,手动转储(通过 jcmd)
-XX:StartFlightRecording=disk=false
3.3.4. 事件配置覆盖#
# 使用 default.jfc,但提高锁等待阈值
-XX:StartFlightRecording=settings=default,jdk.ThreadSleep#threshold=50ms
# 使用 profile.jfc,但禁用方法采样
-XX:StartFlightRecording=settings=profile,jdk.ExecutionSample#enabled=false
# 使用 JFC 选项(可以看 jfc 的 Control 部分,其中的 Selection 可以通过下面的方式修改值),后面第 5 章会详细介绍有哪些选项
-XX:StartFlightRecording=settings=default,gc=high,method-profiling=high
# 配置自定义事件
-XX:StartFlightRecording=settings=none,+jdk.CustomEvent#enabled=true,+jdk.CustomEvent#stackTrace=true
3.3.5. 多个记录(JDK 11+)#
重要:可以多次指定 -XX:StartFlightRecording 来启动多个独立的记录,每个记录有独立的配置和生命周期。
# 启动两个记录:一个用于长期监控,一个用于短期分析
-XX:StartFlightRecording=name=LongTerm,maxage=2d,maxsize=5GB
-XX:StartFlightRecording=name=ShortTerm,maxage=10m,dumponexit=true,filename=shortterm.jfr
# 启动三个记录,使用不同的配置
-XX:StartFlightRecording=name=Recording1,filename=recording1.jfr
-XX:StartFlightRecording=name=Recording2,filename=recording2.jfr,settings=profile
-XX:StartFlightRecording=name=Recording3,disk=false
# 混合使用冒号和等号(都支持)
-XX:StartFlightRecording:name=myrecording1,filename=myrecording1.jfr
-XX:StartFlightRecording=name=myrecording2,filename=myrecording2.jfr
多个记录的使用场景:长期监控 + 短期分析:
- 一个记录用于长期监控(
maxage=2d, maxsize=5GB),保留历史数据 - 另一个记录用于短期分析(
maxage=10m, dumponexit=true),在退出时转储
3.3.6. 完整示例#
# 生产环境配置示例
java \
-XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true \
-XX:StartFlightRecording=name=Production,settings=default,maxage=3d,maxsize=10GB \
-XX:StartFlightRecording=name=Debug,settings=profile,duration=1h,filename=debug.jfr \
-jar MyApp.jar
# 性能分析配置示例
java \
-XX:FlightRecorderOptions=stackdepth=128 \
-XX:StartFlightRecording=settings=profile,gc=high,method-profiling=high,duration=30m,filename=profile.jfr \
-jar MyApp.jar
# 内存泄漏分析配置示例
java \
-XX:StartFlightRecording=settings=default,path-to-gc-roots=true,duration=1h,filename=leak-analysis.jfr \
-jar MyApp.jar
3.4. 查看帮助信息#
可以通过以下方式查看 -XX:StartFlightRecording 的详细帮助信息:
# 查看帮助信息(会打印所有可用参数和示例)
java -XX:StartFlightRecording:help
4. 通过 jcmd 命令使用 JFR#
jcmd 是 JDK 提供的诊断命令工具,可以在运行时通过命令行管理 JVM 进程。JFR 提供了多个 jcmd 子命令来管理 JFR 记录和配置。
基本语法:
jcmd <pid> <command> [options]
其中:
<pid>:目标 JVM 进程的进程 ID<command>:JFR 命令(如JFR.start、JFR.stop等)[options]:命令参数(使用空格分隔,格式为key=value)
查看所有可用的 JFR 命令:
# 查看所有 jcmd 命令
jcmd <pid> help
# 查看 JFR 相关命令的帮助
jcmd <pid> help JFR.start
jcmd <pid> help JFR.stop
jcmd <pid> help JFR.dump
jcmd <pid> help JFR.check
jcmd <pid> help JFR.configure
jcmd <pid> help JFR.view
4.1. JFR.start(启动记录)#
JFR.start 命令用于在运行时启动一个新的 JFR 记录。
语法:
jcmd <pid> JFR.start [options]
重要:与 -XX:StartFlightRecording 不同,jcmd JFR.start 的参数之间使用空格分隔,而不是逗号。
支持的参数(与 -XX:StartFlightRecording 相同,区别只有分割符不一样,详见第 3.2 节):
name=<name>:记录名称settings=<jfc-files>:JFC 配置文件(多个文件用逗号分隔,可重复指定)disk=<true|false>:是否写入磁盘(默认:true)delay=<time>:延迟启动时间duration=<time>:自动停止时间filename=<path>:转储文件名maxage=<time>:最大保留时间(仅在disk=true时有效)maxsize=<size>:最大总大小(仅在disk=true时有效)flush-interval=<time>:定时 flush 间隔(JDK 17+,仅在disk=true时有效)dumponexit=<true|false>:JVM 退出时是否 dumppath-to-gc-roots=<true|false>:是否记录 GC 根report-on-exit=<view-names>:JVM 退出时生成报告视图(JDK 25+,仅在disk=true时有效)- JFC 选项:
<option>=<value>(如gc=high、method-profiling=high) - 事件设置:
<event-name>#<setting-name>=<value>(如jdk.ThreadSleep#threshold=50ms)
使用示例:
# 使用默认配置启动记录
jcmd <pid> JFR.start
# 使用预定义配置启动记录
jcmd <pid> JFR.start settings=default
# 指定记录名称和文件名
jcmd <pid> JFR.start name=MyRecording filename=recording.jfr
# 启动时间限制记录
jcmd <pid> JFR.start duration=1h filename=recording.jfr
# 启动内存记录(不写入磁盘)
jcmd <pid> JFR.start disk=false
# 使用 profile.jfc 并覆盖事件配置
jcmd <pid> JFR.start settings=profile jdk.ThreadSleep#threshold=50ms
# 使用 JFC 选项
jcmd <pid> JFR.start settings=default gc=high method-profiling=high
# 延迟启动并指定持续时间
jcmd <pid> JFR.start delay=5m duration=30m filename=recording.jfr
# 启动记录并配置保留策略
jcmd <pid> JFR.start maxage=2d maxsize=5GB
# 启动记录并启用 GC 根路径收集
jcmd <pid> JFR.start path-to-gc-roots=true duration=1h filename=leak-analysis.jfr
注意事项:
- 参数分隔符:参数之间使用空格分隔(不同于 JVM 参数使用逗号)
- 参数格式:参数名和值之间使用等号(
=)分隔 - 多次启动:可以多次执行
JFR.start启动多个独立的记录 - 记录 ID:启动成功后会显示记录 ID,用于后续管理
4.2. JFR.stop(停止记录)#
JFR.stop 命令用于停止一个正在运行的 JFR 记录。
语法:
jcmd <pid> JFR.stop name=<name> [filename=<path>]
参数:
name=<name>(必需):记录的标识名称或 IDfilename=<path>(可选):停止时转储的文件路径- 如果指定了
filename,会覆盖启动时设置的转储路径,记录停止后转储到新指定的文件 - 如果未指定
filename:- 如果启动时指定了
filename(文件路径或目录路径),记录停止后会自动转储到启动时指定的位置 - 如果启动时没有指定
filename,记录数据会被丢弃
- 如果启动时指定了
- 注意:
filename必须指定完整的文件路径,不支持目录路径(与JFR.start和JFR.dump不同) - 支持占位符:
%p(PID)、%t(时间戳yyyy_MM_dd_HH_mm_ss)
- 如果指定了
使用示例:
# 停止记录(如果启动时没有指定转储路径则不转储,数据丢弃)
jcmd <pid> JFR.stop name=1
# 停止记录(如果启动时指定了 filename,会自动转储到启动时指定的位置)
jcmd <pid> JFR.stop name=MyRecording
# 停止记录并转储到新文件(覆盖启动时设置的转储路径)
jcmd <pid> JFR.stop name=MyRecording filename=recording.jfr
# 使用占位符生成文件名
jcmd <pid> JFR.stop name=1 filename=recording-%p-%t.jfr
注意事项:
- 记录标识:可以使用记录名称或记录 ID(数字)来标识记录
- 数据保存行为:
- 如果
JFR.stop时指定了filename,会覆盖启动时设置的转储路径,转储到新指定的文件 - 如果
JFR.stop时未指定filename,但启动时指定了filename,会自动转储到启动时指定的位置(文件路径或目录路径) - 如果启动时和停止时都没有指定
filename,记录数据会被丢弃,无法恢复
- 如果
- 文件路径限制:
filename参数必须指定完整的文件路径,不能是目录。如果需要指定目录,应使用JFR.dump命令或JFR.start命令的filename参数(它们支持目录路径)
4.3. JFR.dump(转储记录)#
JFR.dump 命令用于将正在运行的记录转储到文件,记录会继续运行。
语法:
jcmd <pid> JFR.dump [options]
参数:
name=<name>(可选):记录的标识名称或 ID- 如果指定,只转储指定的记录
- 如果未指定,转储所有正在运行的记录
filename=<path>(可选):转储文件名- 如果未指定,系统自动生成文件名
- 可以指定文件路径或目录路径(如果指定目录,会在该目录中自动生成文件名)
- 支持占位符:
%p(PID)、%t(时间戳)
maxage=<time>(可选):转储的时间范围(从当前时间往前推)- 格式:整数后跟单位(
s=秒、m=分钟、h=小时、d=天) - 示例:
maxage=1h(转储最近 1 小时的数据) - 注意:不能与
begin或end同时使用
- 格式:整数后跟单位(
maxsize=<size>(可选):转储的大小限制- 格式:整数后跟单位(
k/K=KB、m/M=MB、g/G=GB) - 示例:
maxsize=500M
- 格式:整数后跟单位(
begin=<time>(可选):开始时间- 格式支持:
- ISO 8601 格式:
2020-03-17T09:00:00、2020-03-17T09:00:00Z - 本地时间:
13:20:15、2020-03-17T09:00:00 - 相对时间:
-12h(12 小时前)、-15m(15 分钟前)、-30s(30 秒前)
- ISO 8601 格式:
- 注意:不能与
maxage同时使用
- 格式支持:
end=<time>(可选):结束时间- 格式与
begin相同 - 注意:
end必须晚于begin
- 格式与
path-to-gc-roots=<true|false>(可选):是否收集 GC 根路径- 默认值:
false - 仅在需要内存泄漏分析时启用
- 默认值:
使用示例:
# 转储所有记录(使用自动生成的文件名)
jcmd <pid> JFR.dump
# 转储指定记录到文件
jcmd <pid> JFR.dump name=1 filename=recording.jfr
# 转储最近 1 小时的数据
jcmd <pid> JFR.dump name=1 maxage=1h filename=recording.jfr
# 转储最近 1 小时且最多 500MB 的数据
jcmd <pid> JFR.dump name=1 maxage=1h maxsize=500M filename=recording.jfr
# 转储指定时间范围的数据
jcmd <pid> JFR.dump name=1 begin=13:15 end=21:30:00 filename=recording.jfr
# 转储指定日期时间范围的数据
jcmd <pid> JFR.dump name=1 begin=2021-09-15T09:00:00 end=2021-09-15T10:00:00 filename=recording.jfr
# 转储相对时间范围的数据
jcmd <pid> JFR.dump name=1 begin=-1h end=-5m filename=recording.jfr
# 转储并收集 GC 根路径(用于内存泄漏分析)
jcmd <pid> JFR.dump name=1 filename=leaks.jfr path-to-gc-roots=true
# 转储到目录(自动生成文件名)
jcmd <pid> JFR.dump name=1 filename=./recordings/
时间格式说明:
ISO 8601 格式:
2020-03-17T09:00:00(本地时间)2020-03-17T09:00:00Z(UTC 时间)
本地时间格式:
13:20:15(今天的时间)2020-03-17T09:00:00(指定日期时间)
相对时间格式:
-12h:12 小时前-15m:15 分钟前-30s:30 秒前-1d:1 天前
注意事项:
- 记录继续运行:
JFR.dump不会停止记录,记录会继续运行 - 时间范围:
maxage与begin/end不能同时使用 - 数据过滤:转储的数据会根据
maxage、maxsize、begin、end进行过滤 - GC 根路径:收集 GC 根路径会导致应用短暂暂停,仅在需要时启用
4.4. JFR.check(检查记录)#
JFR.check 命令用于查看正在运行的 JFR 记录信息。
语法:
jcmd <pid> JFR.check [name=<name>] [verbose=<true|false>]
参数:
name=<name>(可选):记录的标识名称或 ID- 如果指定,只显示指定记录的信息
- 如果未指定,显示所有正在运行的记录信息
verbose=<true|false>(可选):是否显示详细的事件配置- 默认值:
false - 为
true时,会显示每个事件的详细配置(enabled、threshold、period等)
- 默认值:
使用示例:
# 查看所有记录的基本信息
jcmd <pid> JFR.check
# 查看所有记录的详细信息(包括事件配置)
jcmd <pid> JFR.check verbose=true
# 查看指定记录的基本信息
jcmd <pid> JFR.check name=1
# 查看指定记录的详细信息
jcmd <pid> JFR.check name=MyRecording verbose=true
输出示例:
# 基本输出
$ jcmd <pid> JFR.check
Recording 1: name=MyRecording duration=1h maxsize=500M maxage=2d (running)
# 详细输出(verbose=true)
$ jcmd <pid> JFR.check name=1 verbose=true
Recording 1: name=MyRecording duration=1h maxsize=500M maxage=2d (running)
Thread Sleep (jdk.ThreadSleep)
[enabled=true, threshold=20 ms, stackTrace=true]
CPU Load (jdk.CPULoad)
[enabled=true, period=1 s]
Java Monitor Enter (jdk.JavaMonitorEnter)
[enabled=true, threshold=20 ms, stackTrace=true]
注意事项:
- 记录状态:输出会显示记录的状态(
running、stopped等) - 记录信息:包括记录 ID、名称、持续时间、最大大小、最大保留时间等
- 事件配置:
verbose=true时会显示所有已配置事件的详细设置
4.5. JFR.configure(配置全局选项)#
JFR.configure 命令用于在运行时配置 JFR 的全局选项。
语法:
jcmd <pid> JFR.configure [options]
支持的选项(与 -XX:FlightRecorderOptions 相同,详见第 1.3 节):
repositorypath=<path>:Repository 路径dumppath=<path>:紧急转储路径(JDK 17+)stackdepth=<depth>:堆栈跟踪深度(仅在 JFR 初始化前可修改)globalbuffercount=<count>:全局缓冲区数量(仅在 JFR 初始化前可修改,遗留选项)globalbuffersize=<size>:全局缓冲区大小(仅在 JFR 初始化前可修改,遗留选项)thread_buffer_size=<size>:线程缓冲区大小(仅在 JFR 初始化前可修改)memorysize=<size>:JFR 占用总内存限制(仅在 JFR 初始化前可修改)maxchunksize=<size>:最大 Chunk 大小(仅在 JFR 初始化前可修改)preserve-repository=<true|false>:JVM 退出时是否保留 Repository(JDK 21+)
使用示例:
# 查看当前配置
jcmd <pid> JFR.configure
# 修改 Repository 路径
jcmd <pid> JFR.configure repositorypath=/tmp/jfr
# 注意:旧的 repository 数据会保留在原位置,不会被迁移
# 如果旧的 repository 目录已经在 cleanupDirectories 中,JFR 会继续追踪它,并在 JVM 关闭时自动删除(如果 preserve-repository=false)
# 如果有正在运行的磁盘记录,当前 chunk 会被标记为 final 并轮转,新的 chunk 会写入新的 repository 路径
# 修改紧急转储路径
jcmd <pid> JFR.configure dumppath=/tmp/emergency
# 修改堆栈深度(仅在 JFR 初始化前有效)
jcmd <pid> JFR.configure stackdepth=128
# 修改内存大小(仅在 JFR 初始化前有效)
jcmd <pid> JFR.configure memorysize=50M
# 修改最大 Chunk 大小(仅在 JFR 初始化前有效)
jcmd <pid> JFR.configure maxchunksize=128M
# 启用保留 Repository
jcmd <pid> JFR.configure preserve-repository=true
# 同时修改多个选项
jcmd <pid> JFR.configure repositorypath=/tmp/jfr dumppath=/tmp/emergency preserve-repository=true
注意事项:
- 初始化限制:部分选项(如
stackdepth、memorysize、maxchunksize)只能在 JFR 初始化前修改,如果 JFR 已经初始化,这些选项无法修改 - 运行时修改:
repositorypath、dumppath、preserve-repository可以在运行时修改 - 遗留选项:
globalbuffercount和globalbuffersize是遗留选项,建议使用memorysize统一调整 - 查看配置:不指定任何参数时,会显示当前配置
- Repository 路径修改的影响:
- 当修改
repositorypath时,旧的 repository 目录和其中的 chunk 文件会保留在原位置,不会被迁移。如果旧的 repository 目录已经在cleanupDirectories中(即之前通过newChunk()创建过),JFR 会继续追踪它,并在 JVM 关闭时自动删除(如果preserve-repository=false) - 如果有正在运行的磁盘记录,当前 chunk 会被标记为 final 并轮转,新的 chunk 会写入新的 repository 路径
- 新的 repository 目录会在新路径下创建(格式:
<base-path>/<timestamp>_<pid>或<base-path>/<timestamp>_<pid>_<index>) - 旧的 repository 目录的清理行为:
- JFR 内部使用
cleanupDirectories(Set<Path>)来跟踪需要在 JVM 关闭时清理的 repository 目录 - 只有当通过
Repository.newChunk()创建新的 repository 目录时,该目录才会被添加到cleanupDirectories - 如果旧的 repository 目录在成为"旧的"之前,已经通过
newChunk()被添加到cleanupDirectories,那么它仍然在cleanupDirectories中,会在 JVM 关闭时被清理(如果preserve-repository=false) - 如果旧的 repository 目录从来没有被添加到
cleanupDirectories(例如:在 JFR 初始化之前就设置了 repository 路径,但从来没有创建过 chunk),那么它不会在 JVM 关闭时被自动清理 - 如果修改路径后有正在运行的磁盘记录,
migrate()会调用rotateDisk()→newChunk(),此时新的 repository 目录会被创建并添加到cleanupDirectories - 在 JVM 关闭时,如果
preserve-repository=false,cleanupDirectories中的所有目录都会被清理(Repository.clear()方法,第173-180行) - 注意:
cleanupDirectories是 JFR 内部自动管理的机制,用户无法直接添加目录到该集合
- JFR 内部使用
- 当修改
4.6. JFR.view(查看记录)#
JFR.view 命令用于以预定义的视图格式显示 JFR 记录数据,无需转储文件。
语法:
jcmd <pid> JFR.view <view> [options]
参数:
<view>(必需):视图名称或事件类型名称- 预定义视图:
gc、hot-methods、allocation-by-class、contention-by-site等- 注意:视图在
view.ini中定义时使用完整名称(如[jvm.gc]、[application.hot-methods]),但在JFR.view命令中只需要使用视图名称部分,不需要加前缀。例如:- 使用
jcmd <pid> JFR.view gc而不是jcmd <pid> JFR.view jvm.gc - 使用
jcmd <pid> JFR.view hot-methods而不是jcmd <pid> JFR.view application.hot-methods
- 使用
- 视图名称匹配是大小写不敏感的(
equalsIgnoreCase)
- 注意:视图在
- 事件类型:
jdk.GarbageCollection、jdk.ThreadStart等- 可以直接使用事件类型名称查看该事件的所有数据
- 特殊值:
types:列出所有可用的事件类型all-views:显示所有预定义视图all-events:显示所有事件
- 预定义视图:
maxage=<time>(可选):查看的时间范围(默认:10m)- 格式:整数后跟单位(
s=秒、m=分钟、h=小时、d=天)
- 格式:整数后跟单位(
maxsize=<size>(可选):查看的大小限制(默认:32MB)- 格式:整数后跟单位(
m/M=MB、g/G=GB)
- 格式:整数后跟单位(
width=<integer>(可选):视图宽度(字符数,默认:100)truncate=<beginning|end>(可选):截断模式(默认:end)beginning:从开头截断end:从结尾截断
cell-height=<integer>(可选):表格单元格的最大行数verbose=<true|false>(可选):是否显示视图的查询信息(默认:false)
使用示例:
# 查看 GC 视图
jcmd <pid> JFR.view gc
# 查看热点方法视图
jcmd <pid> JFR.view hot-methods
# 查看分配视图(按类)
jcmd <pid> JFR.view allocation-by-class
# 查看锁竞争视图(按位置)
jcmd <pid> JFR.view contention-by-site
# 查看指定事件类型
jcmd <pid> JFR.view jdk.GarbageCollection
# 查看所有可用视图
jcmd <pid> JFR.view all-views
# 查看所有事件类型
jcmd <pid> JFR.view types
# 自定义视图参数
jcmd <pid> JFR.view width=160 hot-methods
# 查看最近 1 小时的数据
jcmd <pid> JFR.view maxage=1h gc
# 显示视图的查询信息
jcmd <pid> JFR.view verbose=true allocation-by-class
# 设置截断模式
jcmd <pid> JFR.view truncate=beginning SystemProcess
预定义视图详解:
JFR 提供了丰富的预定义视图,这些视图定义在 view.ini 配置文件中,使用自定义的查询语言来聚合和展示事件数据。视图分为三类:JVM 视图(jvm.*)、应用视图(application.*)和环境视图(environment.*)。
view.ini 文件位置:
- 源码位置:
src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini - 编译后位置:
view.ini文件会被打包到jdk.jfr模块的 JAR 文件中(通常在lib/jfr.jar或jmods/jdk.jfr.jmod中),作为资源文件存在 - 运行时访问:通过
ViewFile.class.getResourceAsStream("/jdk/jfr/internal/query/view.ini")在运行时读取 - 文件格式:INI 格式配置文件,包含视图定义和查询语句
- 用户访问:用户可以通过解压
jdk.jfr模块的 JAR 文件来查看view.ini文件内容,但不建议直接修改
视图结构:
每个视图定义包含以下部分:
- 视图名称:格式为
<category>.<name>,例如jvm.gc、application.hot-methods - 标签(label):视图的显示名称
- 查询类型:
table:表格视图,用于显示多行数据,支持排序、分组、聚合form:表单视图,用于显示单行汇总数据,通常使用LAST()函数获取最新值
查询语言语法:
视图使用类似 SQL 的查询语言,支持以下语法:
- COLUMN:定义列标题
- FORMAT:定义列格式(
none、normalized、cell-height、truncate-beginning/end、missing:whitespace等) - SELECT:选择字段和聚合函数
- FROM:指定事件类型(支持多个事件类型的并集)
- WHERE:过滤条件
- GROUP BY:分组聚合
- ORDER BY:排序(
ASC/DESC) - LIMIT:限制结果数量
支持的聚合函数:
COUNT(*):计数SUM(field):求和AVG(field):平均值MIN(field):最小值MAX(field):最大值MEDIAN(field):中位数P90/P95/P99/P999(field):百分位数FIRST(field):第一个值LAST(field):最后一个值LAST_BATCH(field):最后一批值(相同结束时间戳)DIFF(field):差值(最后一个值减去第一个值)STDEV(field):标准差LIST(field):所有值的逗号分隔列表SET(field):所有唯一值的逗号分隔列表UNIQUE(field):唯一值数量
堆栈跟踪访问:
查询语言支持访问堆栈跟踪的特定帧:
stackTrace.topFrame:堆栈顶部帧(最外层方法)stackTrace.topApplicationFrame:堆栈顶部应用帧(排除 JDK 内部方法)stackTrace.topNotInitFrame:堆栈顶部非初始化帧(排除<init>方法)
常用预定义视图分类:
JVM 视图(jvm.*):
gc:垃圾回收统计- 事件:
GarbageCollection、GCHeapSummary - 设计:表格视图,显示每次 GC 的开始时间、GC ID、GC 名称、GC 前后堆使用情况、最长暂停时间
- 聚合:按
gcId分组,关联 GC 前后的堆摘要数据
- 事件:
gc-pauses:GC 暂停统计- 事件:
GCPhasePause - 设计:表单视图,显示总暂停时间、暂停次数、最小/中位数/平均/P90/P95/P99/P99.9/最大暂停时间
- 聚合:使用
SUM、COUNT、MIN、MEDIAN、AVG、P90/P95/P99/P999、MAX函数
- 事件:
gc-configuration:GC 配置信息- 事件:
GCConfiguration - 设计:表单视图,显示年轻代/老年代收集器、GC 线程数、显式 GC 设置等
- 聚合:使用
LAST()获取最新配置值
- 事件:
gc-parallel-phases:并行 GC 阶段- 事件:
GCPhaseParallel - 设计:表格视图,按阶段名称分组,显示平均、P95、最长、计数、总时间
- 事件:
gc-concurrent-phases:并发 GC 阶段- 事件:
GCPhaseConcurrent、GCPhaseConcurrentLevel1 - 设计:表格视图,按阶段名称分组,显示平均、P95、最长、计数、总时间
- 事件:
gc-pause-phases:GC 暂停阶段- 事件:
GCPhasePause、GCPhasePauseLevel1-4 - 设计:表格视图,按阶段名称分组,显示事件类型、平均、P95、最长、计数、总时间
- 事件:
gc-references:GC 引用统计- 事件:
GCReferenceStatistics - 设计:表格视图,按 GC ID 分组,显示软引用、弱引用、虚引用、终结引用的数量
- 事件:
gc-allocation-trigger:GC 分配触发- 事件:
AllocationRequiringGC - 设计:表格视图,按应用方法分组,显示触发次数和总请求大小
- 堆栈:使用
stackTrace.topApplicationFrame获取应用层方法
- 事件:
gc-cpu-time:GC CPU 时间- 事件:
GCCPUTime - 设计:表单视图,显示 GC 用户时间、系统时间、挂钟时间、总时间、GC 次数
- 事件:
heap-configuration:堆配置- 事件:
GCHeapConfiguration - 设计:表单视图,显示初始大小、最小大小、最大大小、压缩 OOPs 设置
- 事件:
compiler-configuration:编译器配置- 事件:
CompilerConfiguration - 设计:表单视图,显示编译器线程数、动态线程数、分层编译设置
- 事件:
compiler-statistics:编译器统计- 事件:
CompilerStatistics - 设计:表单视图,显示编译次数、峰值时间、总时间、回退次数、OSR/标准编译统计等
- 事件:
compiler-phases:编译器阶段- 事件:
CompilerPhase - 设计:表格视图,按编译级别和阶段分组,显示平均、P95、最长、计数、总时间
- 事件:
longest-compilations:最长编译- 事件:
Compilation - 设计:表格视图,显示编译时间最长的 25 个方法
- 事件:
safepoints:安全点- 事件:
SafepointBegin、SafepointEnd、SafepointStateSynchronization - 设计:表格视图,按安全点 ID 分组,显示开始时间、持续时间、状态同步时间、JNI 关键线程数、总线程数
- 事件:
vm-operations:VM 操作- 事件:
jdk.ExecuteVMOperation - 设计:表格视图,按操作类型分组,显示平均持续时间、最长持续时间、次数、总持续时间
- 事件:
deoptimizations-by-reason:按原因的反优化- 事件:
Deoptimization - 设计:表格视图,按反优化原因分组,显示次数
- 事件:
deoptimizations-by-site:按位置的反优化- 事件:
Deoptimization - 设计:表格视图,按方法、行号、BCI 分组,显示次数
- 事件:
class-modifications:类修改- 事件:
RetransformClasses、RedefineClasses - 设计:表格视图,按重定义 ID 分组,显示持续时间、请求者(应用方法)、操作类型、类数量
- 堆栈:使用
stackTrace.topApplicationFrame获取请求者
- 事件:
blocked-by-system-gc:被 System.gc() 阻塞- 事件:
SystemGC - 设计:表格视图,过滤非并发调用,显示开始时间、持续时间、堆栈跟踪
- 事件:
native-memory-committed:已提交的本地内存- 事件:
NativeMemoryUsage - 设计:表格视图,按内存类型分组,显示首次观察值、平均值、最后观察值、最大值
- 事件:
native-memory-reserved:保留的本地内存- 事件:
NativeMemoryUsage - 设计:表格视图,按内存类型分组,显示首次观察值、平均值、最后观察值、最大值
- 事件:
tlabs:线程本地分配缓冲区- 事件:
ObjectAllocationInNewTLAB、ObjectAllocationOutsideTLAB - 设计:表单视图,显示 TLAB 内外的分配统计(计数、最小/平均/最大大小、总分配)
- 事件:
应用视图(application.*):
hot-methods:热点方法- 事件:
ExecutionSample - 设计:表格视图,按堆栈顶部帧分组,显示方法、样本数、百分比,限制前 25 个
- 聚合:使用
COUNT(*)计数,normalized格式显示百分比 - 堆栈:使用
stackTrace.topFrame获取最外层方法
- 事件:
cpu-time-hot-methods:CPU 时间采样热点方法- 事件:
CPUTimeSample - 设计:表格视图,按堆栈顶部帧分组,显示方法、样本数、百分比,限制前 25 个
- 聚合:使用
COUNT(*)计数,normalized格式显示百分比
- 事件:
cpu-time-statistics:CPU 时间采样统计- 事件:
CPUTimeSample、CPUTimeSamplesLost - 设计:表单视图,显示成功样本、失败样本、有偏样本、总样本、丢失样本数
- 过滤:使用
WHERE条件过滤不同状态的样本
- 事件:
allocation-by-class:按类分配统计- 事件:
ObjectAllocationSample - 设计:表格视图,按对象类分组,显示对象类型、分配压力,限制前 25 个
- 聚合:使用
SUM(weight)计算分配压力,normalized格式显示
- 事件:
allocation-by-thread:按线程分配统计- 事件:
ObjectAllocationSample - 设计:表格视图,按事件线程分组,显示线程、分配压力,限制前 25 个
- 聚合:使用
SUM(weight)计算分配压力
- 事件:
allocation-by-site:按位置分配统计- 事件:
ObjectAllocationSample - 设计:表格视图,按堆栈顶部帧分组,显示方法、分配压力,限制前 25 个
- 堆栈:使用
stackTrace.topFrame获取分配位置
- 事件:
contention-by-thread:按线程锁竞争统计- 事件:
JavaMonitorEnter - 设计:表格视图,按事件线程分组,显示线程、次数、平均、P90、最大持续时间
- 事件:
contention-by-class:按锁类竞争统计- 事件:
JavaMonitorEnter - 设计:表格视图,按监视器类分组,显示锁类、次数、平均、P90、最大持续时间
- 事件:
contention-by-site:按位置锁竞争统计- 事件:
JavaMonitorEnter - 设计:表格视图,按堆栈跟踪分组,显示堆栈跟踪、次数、平均、最大持续时间
- 事件:
contention-by-address:按监视器地址竞争统计- 事件:
JavaMonitorEnter - 设计:表格视图,按监视器类分组,显示地址、类、线程数、最大持续时间
- 聚合:使用
FIRST()获取第一个类名,UNIQUE()获取唯一线程数
- 事件:
memory-leaks-by-class:按类内存泄漏候选- 事件:
OldObjectSample - 设计:表格视图,按对象类型分组,显示分配时间、对象类、对象年龄、堆使用情况
- 聚合:使用
LAST_BATCH()获取最后一批值 - 注意:使用
memory-leaks视图时会自动触发 OldObjectSample 事件收集
- 事件:
memory-leaks-by-site:按位置内存泄漏候选- 事件:
OldObjectSample - 设计:表格视图,按应用方法分组,显示分配时间、应用方法、对象年龄、堆使用情况
- 堆栈:使用
stackTrace.topApplicationFrame获取应用层分配位置
- 事件:
exception-by-type:按类型异常统计- 事件:
JavaErrorThrow、JavaExceptionThrow - 设计:表格视图,按抛出类分组,显示类、次数
- 事件:
exception-by-message:按消息异常统计- 事件:
JavaErrorThrow、JavaExceptionThrow - 设计:表格视图,按消息分组,显示消息、次数
- 事件:
exception-by-site:按位置异常统计- 事件:
JavaErrorThrow、JavaExceptionThrow - 设计:表格视图,按堆栈顶部非初始化帧分组,显示方法、次数
- 堆栈:使用
stackTrace.topNotInitFrame排除<init>方法
- 事件:
exception-count:异常统计- 事件:
ExceptionStatistics - 设计:表单视图,显示抛出的异常总数(使用
DIFF(throwables)计算差值)
- 事件:
thread-allocation:线程分配统计- 事件:
ThreadAllocationStatistics - 设计:表格视图,按线程分组,显示线程、已分配、百分比
- 聚合:使用
LAST()获取最新值,normalized格式显示百分比
- 事件:
thread-cpu-load:线程 CPU 负载- 事件:
ThreadCPULoad - 设计:表格视图,按事件线程分组,显示线程、系统 CPU、用户 CPU
- 事件:
thread-start:平台线程启动- 事件:
ThreadStart、ThreadEnd - 设计:表格视图,按事件线程分组,显示开始时间、堆栈跟踪、线程、持续时间
- 聚合:使用
DIFF(startTime)计算线程生命周期
- 事件:
thread-count:Java 线程统计- 事件:
JavaThreadStatistics - 设计:表格视图,显示所有字段(
SELECT *)
- 事件:
pinned-threads:固定虚拟线程- 事件:
VirtualThreadPinned - 设计:表格视图,按应用方法分组,显示方法、固定次数、最长固定时间、总固定时间
- 堆栈:使用
stackTrace.topApplicationFrame获取应用层方法
- 事件:
file-reads-by-path:按路径文件读取统计- 事件:
FileRead - 设计:表格视图,按路径分组,显示路径、读取次数、总读取字节数
- 格式:
cell-height:5支持多行路径显示
- 事件:
file-writes-by-path:按路径文件写入统计- 事件:
FileWrite - 设计:表格视图,按路径分组,显示路径、写入次数、总写入字节数
- 事件:
socket-reads-by-host:按主机 Socket 读取统计- 事件:
SocketRead - 设计:表格视图,按主机分组,显示主机、读取次数、总读取字节数
- 事件:
socket-writes-by-host:按主机 Socket 写入统计- 事件:
SocketWrite - 设计:表格视图,按主机分组,显示主机、写入次数、总写入字节数
- 事件:
latencies-by-type:按类型延迟统计- 事件:
JavaMonitorWait、JavaMonitorEnter、ThreadPark、ThreadSleep、SocketRead、SocketWrite、FileWrite、FileRead - 设计:表格视图,按事件类型分组,显示事件类型、次数、平均、P99、最长、总持续时间
- 多事件:使用多个事件类型的并集
- 事件:
object-statistics:对象统计(占用超过 1%)- 事件:
ObjectCountAfterGC、ObjectCount - 设计:表格视图,按对象类分组,显示类、计数、堆空间、增长量
- 聚合:使用
LAST_BATCH()获取最后一批值,DIFF()计算增长量
- 事件:
class-loaders:类加载器- 事件:
ClassLoaderStatistics - 设计:表格视图,按类加载器分组,显示类加载器、隐藏类数、类数
- 格式:
missing:null-bootstrap将引导类加载器显示为 null
- 事件:
longest-class-loading:最长类加载- 事件:
ClassLoad - 设计:表格视图,显示加载时间最长的 25 个类
- 事件:
finalizers:终结器- 事件:
FinalizerStatistics - 设计:表格视图,按可终结类分组,显示类、对象数、总运行次数
- 聚合:使用
LAST_BATCH()获取最后一批值
- 事件:
deprecated-methods-for-removal:标记为移除的废弃方法- 事件:
DeprecatedInvocation - 设计:表格视图,按方法分组,显示废弃方法、调用来源类集合
- 过滤:
WHERE forRemoval = 'true' - 聚合:使用
SET()获取唯一调用来源类集合 - 格式:
truncate-beginning和cell-height:10000支持长内容显示
- 事件:
method-timing:方法计时- 事件:
jdk.MethodTiming - 设计:表格视图,按方法分组,显示方法、调用次数、最小/平均/最大时间
- 聚合:使用
LAST_BATCH()获取最后一批值 - 格式:
ms-precision:6显示微秒精度
- 事件:
method-calls:方法调用- 事件:
jdk.MethodTrace - 设计:表格视图,按被跟踪方法和调用者分组,显示被跟踪方法、调用者、调用次数
- 堆栈:使用
stackTrace.topFrame.method获取调用者方法
- 事件:
monitor-inflation:监视器膨胀- 事件:
jdk.JavaMonitorInflate - 设计:表格视图,按堆栈跟踪和监视器类分组,显示堆栈跟踪、监视器类、次数、总持续时间
- 事件:
modules:模块- 事件:
ModuleRequire - 设计:表格视图,按源模块名称分组,显示模块名称
- 事件:
native-methods:本地方法- 事件:
NativeMethodSample - 设计:表格视图,按堆栈顶部帧分组,显示方法、样本数
- 事件:
环境视图(environment.*):
recording:记录信息- 事件:所有事件(
FROM *) - 设计:表单视图,显示事件计数、第一个/最后一个记录事件、记录长度、转储原因
- 聚合:使用
COUNT()、FIRST()、LAST()、DIFF()函数,从jdk.Shutdown获取转储原因
- 事件:所有事件(
active-recordings:活动记录- 事件:
ActiveRecording - 设计:表格视图,按记录 ID 分组,显示开始时间、持续时间、名称、目标、最大年龄、最大大小
- 格式:
cell-height:5支持多行目标路径显示
- 事件:
active-settings:活动设置- 事件:
ActiveSetting - 设计:表格视图,按事件类型 ID 分组,显示事件类型、启用状态、阈值、堆栈跟踪、周期、截止时间、节流
- 多源:使用多个
ActiveSetting别名(E、T、S、P、C、U)分别获取不同设置 - 过滤:使用
WHERE条件过滤不同设置名称
- 事件:
cpu-load:CPU 负载统计- 事件:
CPULoad - 设计:表单视图,显示 JVM 用户/系统 CPU 和机器总 CPU 的最小/平均/最大值
- 事件:
cpu-load-samples:CPU 负载样本- 事件:
CPULoad - 设计:表格视图,显示每个样本的开始时间、JVM 用户/系统 CPU、机器总 CPU
- 事件:
cpu-information:CPU 信息- 事件:
CPUInformation - 设计:表单视图,显示 CPU、插槽数、核心数、硬件线程数、描述
- 事件:
cpu-tsc:CPU 时间戳计数器- 事件:
CPUTimeStampCounter - 设计:表单视图,显示快速时间自动启用、快速时间启用、快速时间频率、OS 频率
- 事件:
system-information:系统信息- 事件:
CPUInformation、PhysicalMemory、OSInformation、VirtualizationInformation - 设计:表单视图,显示物理内存大小、OS 版本、虚拟化、CPU 类型、核心数、硬件线程数、插槽数、CPU 描述
- 多事件:使用多个事件类型的并集
- 事件:
system-properties:系统属性- 事件:
InitialSystemProperty - 设计:表格视图,按键分组,显示键、值
- 格式:
cell-height:25支持长属性值显示
- 事件:
system-processes:系统进程- 事件:
SystemProcess - 设计:表格视图,按 PID 分组,显示首次/最后观察时间、PID、命令行
- 格式:
truncate-beginning截断长命令行
- 事件:
environment-variables:环境变量- 事件:
InitialEnvironmentVariable - 设计:表格视图,按键分组,显示键、值
- 格式:
cell-height:20支持长环境变量值显示
- 事件:
network-utilization:网络利用率- 事件:
NetworkUtilization - 设计:表格视图,按网络接口分组,显示接口、平均/最大读取速率、平均/最大写入速率
- 事件:
native-libraries:本地库- 事件:
NativeLibrary - 设计:表格视图,按名称分组,显示名称、基地址、顶部地址
- 事件:
native-library-failures:本地库加载/卸载失败- 事件:
NativeLibraryLoad、NativeLibraryUnload - 设计:表格视图,过滤失败操作,显示操作类型、库名称、错误消息
- 过滤:
WHERE success = 'false'
- 事件:
jvm-flags:JVM 标志- 事件:
IntFlag、UnsignedIntFlag、BooleanFlag、LongFlag、UnsignedLongFlag、DoubleFlag、StringFlag及其变更事件 - 设计:表格视图,按名称分组,显示名称、最后值
- 多事件:使用多个标志事件类型的并集
- 事件:
jvm-information:JVM 信息- 事件:
JVMInformation - 设计:表单视图,显示 PID、VM 启动时间、名称、版本、VM 参数、程序参数
- 事件:
jdk-agents:JDK 代理- 事件:
JavaAgent、NativeAgent - 设计:表格视图,显示初始化时间、初始化持续时间、名称、选项
- 格式:
truncate-beginning和cell-height:10支持长选项显示
- 事件:
container-configuration:容器配置- 事件:
ContainerConfiguration - 设计:表单视图,显示容器类型、CPU 切片周期、CPU 配额、CPU 份额、有效 CPU 数、内存软限制、内存限制、交换内存限制、主机总内存
- 事件:
container-cpu-usage:容器 CPU 使用- 事件:
ContainerCPUUsage - 设计:表单视图,显示 CPU 时间、用户时间、系统时间
- 事件:
container-memory-usage:容器内存使用- 事件:
ContainerMemoryUsage - 设计:表单视图,显示内存失败次数、内存使用、交换内存使用
- 事件:
container-io-usage:容器 I/O 使用- 事件:
ContainerIOUsage - 设计:表单视图,显示服务请求数、数据传输量
- 事件:
container-cpu-throttling:容器 CPU 节流- 事件:
ContainerCPUThrottling - 设计:表单视图,显示 CPU 已用切片、CPU 节流切片、CPU 节流时间
- 事件:
events-by-count:按计数的事件类型- 事件:所有事件(
FROM *) - 设计:表格视图,按事件类型标签分组,显示事件类型、计数,按计数降序排序
- 事件:所有事件(
events-by-name:按名称的事件类型- 事件:所有事件(
FROM *) - 设计:表格视图,按事件类型标签分组,显示事件类型、计数,按名称升序排序
- 事件:所有事件(
视图设计特点:
- 分类组织:视图按功能分类(JVM、应用、环境),便于查找和使用
- 查询优化:使用
LIMIT限制结果数量,避免输出过多数据 - 格式化选项:使用
FORMAT控制显示格式,提高可读性 - 多事件支持:支持从多个事件类型查询数据(使用并集)
- 堆栈跟踪访问:提供多种堆栈帧访问方式,适应不同分析场景
- 聚合函数丰富:支持统计、百分位、集合等多种聚合函数
- 实时查询:直接从 repository 读取数据,无需转储文件
注意事项:
- 实时查看:
JFR.view直接从 repository 读取数据,无需转储文件 - 内存泄漏视图:使用
memory-leaks视图时,会自动触发 OldObjectSample 事件收集(调用OldObjectSample.emit(0)并等待刷新) - 时间范围:默认查看最近 10 分钟的数据,可以通过
maxage调整 - 大小限制:默认最多查看 32MB 的数据,可以通过
maxsize调整 - 查询执行:视图查询通过
QueryExecutor执行,使用EventStream读取事件数据 - 性能考虑:复杂查询(如
all-views)可能消耗较多时间和内存 - 事件缺失:如果记录中缺少视图所需的事件,会显示错误信息但不会中断执行
- 视图名称格式:视图在
view.ini中定义时使用完整名称(如[jvm.gc]、[application.hot-methods]),但在JFR.view命令中只需要使用视图名称部分(如gc、hot-methods),不需要加类别前缀(jvm.、application.、environment.)。视图名称匹配是大小写不敏感的。
自定义查询与查询语言:
JFR.view 命令只能使用预定义的视图,不支持直接执行 SQL 查询。
查询语言的使用场景:
- 预定义视图(
JFR.view):使用view.ini中预定义的视图,通过视图名称调用 - 视图定义(
view.ini):在配置文件中定义视图,供JFR.view使用 - 自定义查询(
jfr query):针对已转储的 JFR 文件,使用jfr query命令直接编写查询语句(注意:jfr query命令仅在 debug builds 中可用,面向 OpenJDK 开发者,可能被移除或语法改变)
关于自定义查询的说明:
jcmd JFR.query命令不存在:虽然源代码中存在DCmdQuery.java实现,但在jfrDcmds.cpp中该命令的注册被注释掉了(// JFR.query Uncomment when developing new queries for the JFR.view command),因此在实际的 JDK 构建中无法使用jcmd JFR.query命令。jfr query命令:如果需要执行自定义查询,可以使用jfr query命令(针对已转储的 JFR 文件),该命令支持与view.ini中相同的查询语言语法。但需要注意:jfr query命令仅在 debug builds 中可用- 该工具面向 OpenJDK 开发者,可能被移除或语法改变
- 使用方式:
jfr query "<query>" <file.jfr>
可以使用 verbose=true 选项查看预定义视图的查询语句,然后复制到 jfr query 中使用或修改:
# 查看预定义视图的查询语句
jcmd <pid> JFR.view verbose=true gc
# 使用自定义查询(jfr query)- 针对已转储的 JFR 文件(仅在 debug builds 中可用)
jfr query "SELECT * FROM GarbageCollection" recording.jfr
jfr query "SELECT stackTrace.topFrame AS T, SUM(weight) FROM ObjectAllocationSample GROUP BY T" recording.jfr
4.7. 命令使用流程示例#
以下是一个完整的使用流程示例:
# 1. 查看当前运行的记录
jcmd <pid> JFR.check
# 2. 启动一个新记录
jcmd <pid> JFR.start name=PerformanceAnalysis settings=profile duration=30m filename=analysis.jfr
# 3. 查看记录状态
jcmd <pid> JFR.check name=PerformanceAnalysis
# 4. 在记录运行期间转储数据(记录继续运行)
jcmd <pid> JFR.dump name=PerformanceAnalysis maxage=10m filename=snapshot.jfr
# 5. 查看实时视图
jcmd <pid> JFR.view hot-methods
jcmd <pid> JFR.view gc
# 6. 停止记录并转储
jcmd <pid> JFR.stop name=PerformanceAnalysis filename=final.jfr
# 7. 再次查看所有记录
jcmd <pid> JFR.check
5. jfc 配置文件格式与配置和使用方式#
JFC(Java Flight Recorder Configuration)文件是 XML 格式的配置文件,用于定义 JFR 记录中要采集的事件及其配置参数。JFC 文件是 JFR 的核心配置机制,通过它可以精确控制哪些事件被记录、事件的采集频率、阈值等。
5.1. JFC 文件格式#
JFC 文件采用 XML 格式,必须符合 jfc.xsd Schema 定义。文件的基本结构如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0" label="配置名称" description="配置描述" provider="提供者">
<event name="事件名称">
<setting name="配置项名称">配置值</setting>
<setting name="配置项名称" control="控制标识符">配置值</setting>
</event>
<!-- 更多事件配置 -->
</configuration>
根元素 <configuration> 属性:
version(必需):JFC 文件格式版本,当前版本为"2.0"。JFR 只能读取 2.x 版本的 JFC 文件label(必需):配置文件的显示名称,用于在工具中标识此配置description(可选):配置文件的描述信息provider(可选):配置文件的提供者,如"Oracle"、"OpenJDK"等
<event> 元素:
name(必需):事件名称,如"jdk.ThreadStart"、"jdk.GCPhase"等label(可选):事件的显示标签description(可选):事件的描述信息
<setting> 元素:
name(必需):配置项名称,如"enabled"、"threshold"、"period"、"stackTrace"、"throttle"等control(可选):控制标识符,用于关联<control>元素中定义的控件。当控件值改变时,关联的 setting 值会自动更新- 元素内容:配置值,如
"true"、"20 ms"、"100/s"等
<control> 元素(可选,JVM 不读取):
- 位置:位于
<configuration>根元素下,与<event>元素同级 - 作用:定义 UI 控件和条件逻辑,用于 JDK Mission Control 和
jfr configure工具 - 包含的子元素:
<text>:文本输入控件<selection>:选择控件(下拉菜单)<flag>:布尔标志控件<condition>:条件逻辑,根据其他控件的值动态设置配置值
示例:
<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0" label="Custom" description="Custom JFR configuration" provider="User">
<!-- 启用线程睡眠事件,只记录超过 20ms 的睡眠,并采集堆栈跟踪 -->
<event name="jdk.ThreadSleep">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
<setting name="threshold">20 ms</setting>
</event>
<!-- 启用 CPU 负载事件,每秒采集一次 -->
<event name="jdk.CPULoad">
<setting name="enabled">true</setting>
<setting name="period">1 s</setting>
</event>
<!-- 禁用类加载统计事件 -->
<event name="jdk.ClassLoadingStatistics">
<setting name="enabled">false</setting>
</event>
</configuration>
5.2. Control 元素详解#
<control> 元素是 JFC 文件中的一个特殊部分,用于定义 UI 控件和条件逻辑。重要:JVM 在运行时不读取 <control> 元素的内容,它仅用于 JDK Mission Control(JMC)和 jfr configure 工具,提供友好的配置界面和自动化配置逻辑。
5.2.1. Control 的作用机制#
- UI 控件定义:为 JMC 和
jfr configure工具提供配置界面元素 - 配置关联:通过
<setting>元素的control属性,将事件配置关联到控件 - 自动更新:当控件值改变时,所有关联的 setting 值会自动更新
- 条件逻辑:通过
<condition>元素实现基于其他控件值的动态配置
5.2.2. Control 元素类型#
1. <text> 元素(文本输入控件)
用于定义文本输入控件,支持不同类型的内容:
<text name="控件名称" label="显示标签" contentType="内容类型" minimum="最小值" maximum="最大值">默认值</text>
属性:
name(必需):控件标识符,用于在<setting>的control属性中引用label(必需):控件的显示标签description(可选):控件描述contentType(可选):内容类型,如"timespan"(时间间隔)、"method-filter"(方法过滤器)、"text"(普通文本)等minimum(可选):最小值(适用于可排序的内容类型)maximum(可选):最大值(适用于可排序的内容类型)
示例:
<!-- 定义锁等待阈值控件 -->
<text name="locking-threshold" label="Locking Threshold" contentType="timespan" minimum="0 s">20 ms</text>
<!-- 定义方法过滤器控件 -->
<text name="method-trace" label="Method Trace Filter" contentType="method-filter"
description="A filter can be an annotation (@jakarta.ws.rs.GET), a full qualified class name (com.example.Foo), a fully qualified method reference (java.lang.HashMap::resize) or a class initializer (::<clinit>). Use <init> for constructors. Separate multiple filters with semicolon."></text>
使用方式:
<!-- 在事件配置中关联控件 -->
<event name="jdk.ThreadSleep">
<setting name="threshold" control="locking-threshold">20 ms</setting>
</event>
<event name="jdk.JavaMonitorEnter">
<setting name="threshold" control="locking-threshold">20 ms</setting>
</event>
当用户通过 JMC 或 jfr configure 修改 locking-threshold 控件的值时,所有关联的 setting 值会自动更新。
2. <selection> 元素(选择控件)
用于定义下拉选择控件,提供多个预定义选项:
<selection name="控件名称" label="显示标签" default="默认选项名称">
<option label="选项显示标签" name="选项名称">选项值</option>
<!-- 更多选项 -->
</selection>
属性:
name(必需):控件标识符label(必需):控件的显示标签description(可选):控件描述default(必需):默认选项的名称(必须匹配某个<option>的name属性)
<option> 元素:
name(必需):选项标识符,用于在default属性中引用label(必需):选项的显示标签description(可选):选项描述- 元素内容:选项的实际值
示例:
<!-- 定义方法分析级别选择控件 -->
<selection name="method-profiling" default="normal" label="Method Profiling">
<option label="Off" name="off">off</option>
<option label="Normal" name="normal">normal</option>
<option label="High" name="high">high</option>
<option label="Maximum (High Overhead)" name="max">max</option>
</selection>
<!-- 定义 GC 事件级别选择控件 -->
<selection name="gc" default="normal" label="GC">
<option label="Off" name="off">off</option>
<option label="Normal" name="normal">normal</option>
<option label="Detailed" name="detailed">detailed</option>
<option label="High, incl. TLABs/PLABs (may cause many events)" name="high">high</option>
<option label="All, incl. Heap Statistics (may cause long GCs)" name="all">all</option>
</selection>
3. <flag> 元素(布尔标志控件)
用于定义布尔类型的开关控件:
<flag name="控件名称" label="显示标签">true|false</flag>
属性:
name(必需):控件标识符label(必需):控件的显示标签description(可选):控件描述- 元素内容:默认值(
"true"或"false")
示例:
<!-- 定义类加载事件开关 -->
<flag name="class-loading" label="Class Loading">false</flag>
使用方式:
<!-- 在事件配置中关联控件 -->
<event name="jdk.ClassLoad">
<setting name="enabled" control="class-loading">false</setting>
</event>
<event name="jdk.ClassUnload">
<setting name="enabled" control="class-loading">false</setting>
</event>
4. <condition> 元素(条件逻辑)
用于根据其他控件的值动态设置配置值,实现条件逻辑:
<condition name="控件名称" true="条件为真时的值" false="条件为假时的值">
<!-- 条件表达式:<test>、<and>、<or>、<not> -->
</condition>
属性:
name(必需):控件标识符,用于在<setting>的control属性中引用true(可选):当条件表达式为真时返回的值false(可选):当条件表达式为假时返回的值
条件表达式元素:
<test>:测试条件,检查某个控件的值name:要测试的控件名称operator:操作符,目前只支持"equal"value:要比较的值
<and>:逻辑与,所有子条件都为真时返回真<or>:逻辑或,任一子条件为真时返回真<not>:逻辑非,反转子条件的结果
示例:
<!-- 根据 method-profiling 选择控件的值,动态设置方法采样间隔 -->
<condition name="method-sampling-java-interval" true="999 d">
<test name="method-profiling" operator="equal" value="off"/>
</condition>
<condition name="method-sampling-java-interval" true="20 ms">
<test name="method-profiling" operator="equal" value="normal"/>
</condition>
<condition name="method-sampling-java-interval" true="10 ms">
<test name="method-profiling" operator="equal" value="high"/>
</condition>
<condition name="method-sampling-java-interval" true="1 ms">
<test name="method-profiling" operator="equal" value="max"/>
</condition>
<!-- 根据 GC 选择控件的值,动态启用/禁用 GC 事件 -->
<condition name="gc-enabled-normal" true="true" false="false">
<or>
<test name="gc" operator="equal" value="normal"/>
<test name="gc" operator="equal" value="detailed"/>
<test name="gc" operator="equal" value="high"/>
<test name="gc" operator="equal" value="all"/>
</or>
</condition>
<!-- 根据 thread-dump 选择控件的值,动态启用/禁用线程转储 -->
<condition name="thread-dump-enabled" true="false" false="true">
<test name="thread-dump" operator="equal" value="999 d"/>
</condition>
使用方式:
<!-- 在事件配置中关联条件控件 -->
<event name="jdk.ExecutionSample">
<setting name="enabled" control="method-sampling-enabled">true</setting>
<setting name="period" control="method-sampling-java-interval">20 ms</setting>
</event>
<event name="jdk.GCPhase">
<setting name="enabled" control="gc-enabled-normal">true</setting>
</event>
5.2.3. Control 的完整示例#
以下是一个完整的 control 示例,展示了各种控件类型的组合使用:
<configuration version="2.0" label="Custom" description="Custom JFR configuration">
<!-- 事件配置 -->
<event name="jdk.ThreadSleep">
<setting name="enabled">true</setting>
<setting name="threshold" control="locking-threshold">20 ms</setting>
</event>
<event name="jdk.ExecutionSample">
<setting name="enabled" control="method-sampling-enabled">true</setting>
<setting name="period" control="method-sampling-java-interval">20 ms</setting>
</event>
<event name="jdk.GCPhase">
<setting name="enabled" control="gc-enabled-normal">true</setting>
</event>
<!-- Control 部分 -->
<control>
<!-- 文本控件:锁等待阈值 -->
<text name="locking-threshold" label="Locking Threshold" contentType="timespan" minimum="0 s">20 ms</text>
<!-- 选择控件:方法分析级别 -->
<selection name="method-profiling" default="normal" label="Method Profiling">
<option label="Off" name="off">off</option>
<option label="Normal" name="normal">normal</option>
<option label="High" name="high">high</option>
<option label="Maximum (High Overhead)" name="max">max</option>
</selection>
<!-- 条件控件:根据方法分析级别设置采样间隔 -->
<condition name="method-sampling-java-interval" true="999 d">
<test name="method-profiling" operator="equal" value="off"/>
</condition>
<condition name="method-sampling-java-interval" true="20 ms">
<test name="method-profiling" operator="equal" value="normal"/>
</condition>
<condition name="method-sampling-java-interval" true="10 ms">
<test name="method-profiling" operator="equal" value="high"/>
</condition>
<condition name="method-sampling-java-interval" true="1 ms">
<test name="method-profiling" operator="equal" value="max"/>
</condition>
<!-- 条件控件:根据方法分析级别启用/禁用采样 -->
<condition name="method-sampling-enabled" true="false" false="true">
<test name="method-profiling" operator="equal" value="off"/>
</condition>
<!-- 选择控件:GC 事件级别 -->
<selection name="gc" default="normal" label="GC">
<option label="Off" name="off">off</option>
<option label="Normal" name="normal">normal</option>
<option label="Detailed" name="detailed">detailed</option>
<option label="High" name="high">high</option>
<option label="All" name="all">all</option>
</selection>
<!-- 条件控件:根据 GC 级别启用/禁用 GC 事件 -->
<condition name="gc-enabled-normal" true="true" false="false">
<or>
<test name="gc" operator="equal" value="normal"/>
<test name="gc" operator="equal" value="detailed"/>
<test name="gc" operator="equal" value="high"/>
<test name="gc" operator="equal" value="all"/>
</or>
</condition>
</control>
</configuration>
5.2.4. Control 的工作原理#
根据源代码分析(JFCModel.java),control 的工作流程如下:
解析阶段(
addControls()):- 解析所有
<control>元素及其子元素(<text>、<selection>、<flag>、<condition>) - 将控件按名称索引到
controlsMap 中
- 解析所有
关联阶段(
wireSettings()):- 遍历所有事件的 setting
- 如果 setting 有
control属性,查找对应的控件 - 建立监听关系:当控件值改变时,自动更新关联的 setting 值
条件计算阶段(
wireConditions()):- 解析所有
<condition>元素的表达式 - 建立依赖关系:当被测试的控件值改变时,重新计算条件值
- 条件值计算后,自动更新关联的 setting 值
- 解析所有
运行时:
- JVM 不读取 control 部分,只读取最终的 setting 值
- Control 仅用于配置工具(JMC、
jfr configure)提供友好的配置界面
5.2.5. Control 的优势#
- 简化配置:通过高级控件(如"Method Profiling"级别)自动设置多个相关配置
- 一致性保证:相关事件的配置通过同一个控件统一管理,确保配置一致
- 用户友好:在 JMC 中提供图形化配置界面,无需手动编辑 XML
- 条件逻辑:通过
<condition>实现复杂的配置依赖关系
5.2.6. 注意事项#
- JVM 不读取:Control 元素对 JVM 运行时没有影响,JVM 只读取最终的 setting 值
- 唯一性要求:多个 JFC 文件合并时,control 名称不能重复(会抛出异常)
- 引用完整性:如果 setting 的
control属性引用了不存在的控件,会记录警告但不会报错 - 手动编辑:如果手动编辑 JFC 文件,需要确保 control 名称和 setting 的
control属性匹配
5.4. 预定义的 JFC 配置文件#
JDK 提供了两个预定义的 JFC 配置文件,位于 JAVA_HOME/lib/jfr/ 目录:
default.jfc(默认配置):- 标签:
"Continuous" - 描述:
"Low overhead configuration safe for continuous use in production environments, typically less than 1 % overhead." - 特点:低开销配置,适合生产环境持续使用,开销通常小于 1%
- 适用场景:生产环境持续监控
- 标签:
profile.jfc(性能分析配置):- 标签:
"Profiling" - 描述:
"Low overhead configuration for profiling, typically around 2 % overhead." - 特点:性能分析配置,开销约为 2%,包含更多事件和堆栈跟踪
- 适用场景:性能分析和问题诊断
- 标签:
查看预定义配置:
# 查看 default.jfc 的内容
cat $JAVA_HOME/lib/jfr/default.jfc
# 查看 profile.jfc 的内容
cat $JAVA_HOME/lib/jfr/profile.jfc
# 列出 JAVA_HOME/lib/jfr/ 目录中的所有 .jfc 文件
ls $JAVA_HOME/lib/jfr/*.jfc
# 查看 jfr configure 命令的帮助信息(会显示可用的输入文件选项)
jfr help configure
注意:jfr configure 命令没有 --list 选项。要列出所有可用的预定义配置,可以通过以下方式:
- 直接查看目录:
JAVA_HOME/lib/jfr/目录中通常包含default.jfc和profile.jfc - 使用 JDK API:通过
Configuration.getConfigurations()方法获取配置列表(需要编写 Java 代码) - 查看帮助信息:
jfr help configure会显示可用的输入文件选项
5.5. 创建和编辑 JFC 配置文件#
5.5.1. 使用 jfr configure 命令创建#
jfr configure 命令提供了交互式和命令行两种方式创建 JFC 文件:
交互式创建:
交互式模式需要显式指定 --interactive 参数,会启动配置向导,逐步询问配置选项。向导会遍历 JFC 文件中所有 <control> 元素定义的输入项(<selection>、<text>、<flag>),逐个询问用户输入。
# 基于 default.jfc 创建自定义配置(交互式)
jfr configure --interactive --input default.jfc --output custom.jfc
# 基于 profile.jfc 创建自定义配置(交互式)
jfr configure --interactive --input profile.jfc --output custom.jfc
# 仅使用 --interactive,不指定 --input(默认使用 default.jfc)
jfr configure --interactive
# 示例交互:
============== .jfc Configuration Wizard ============
This wizard will generate a JFR configuration file by
asking 12 questions. Press ENTER to use the default
value, or type Q to abort the wizard.
Garbage Collector: Normal (default)
1. Off
2. Normal
3. Detailed
4. High, incl. TLABs/PLABs (may cause many events)
5. All, incl. Heap Statistics (may cause long GCs)
交互式模式的工作方式:
- 启动向导:显示欢迎信息,告知将询问多少个问题
- 遍历输入项:对于每个
<control>中的输入项(<selection>、<text>、<flag>),依次询问:<selection>:显示选项列表(1, 2, 3…),用户输入数字选择,或按 ENTER 使用默认值<text>:显示标签和默认值,用户输入文本,或按 ENTER 使用默认值<flag>:显示 Y/N 选择,用户输入 Y 或 N,或按 ENTER 使用默认值
- 输入验证:对于时间跨度(timespan)和方法过滤器(method-filter)类型,会进行格式验证
- 退出方式:输入
Q可随时退出向导 - 保存文件:最后询问输出文件名(默认
custom.jfc)
命令行创建:
# 基于 default.jfc 创建,并修改特定事件的配置
jfr configure \
--input default.jfc \
--output custom.jfc \
jdk.ThreadSleep#threshold=50ms \
jdk.CPULoad#period=2s
# 使用 + 前缀添加新的事件配置(如果事件不在基础配置中)
jfr configure \
--input default.jfc \
--output custom.jfc \
+jdk.CustomEvent#enabled=true
# 注意:不能合并包含相同 control 名称的 JFC 文件
# 例如 default.jfc 和 profile.jfc 都定义了 'gc' control,不能直接合并
# 如果尝试合并会报错:Control with 'gc' is declared in multiple files
# 正确做法:只使用一个基础 JFC 文件,然后通过命令行参数覆盖配置
jfr configure \
--input default.jfc \
--output custom.jfc \
gc=high \
method-profiling=high
# 或者使用 profile.jfc 作为基础
jfr configure \
--input profile.jfc \
--output custom.jfc \
gc=high
配置选项格式:
- 选项格式:
<选项名称>=<值>(如gc=high、method-profiling=high) - 事件设置格式:
<事件名称>#<配置项名称>=<配置值>(如jdk.ThreadSleep#threshold=50ms、jdk.CPULoad#period=2s) - 添加新事件:使用
+前缀,如+jdk.CustomEvent#enabled=true - 时间跨度格式:支持
20ms、1s、5m等格式,空格可省略
注意事项:
- 交互式模式的前提:JFC 文件中必须包含
<control>元素定义的输入项,否则交互式模式不会询问任何问题 - 默认 JFC 文件:如果不指定
--input,默认使用default.jfc - 空配置:可以使用
--input none从空配置开始创建 - 显示修改:使用
--verbose参数可以显示所有被修改的设置 - 多个 JFC 文件合并的限制:
jfr configure命令的限制:不能合并包含相同 control 名称的 JFC 文件。例如,default.jfc和profile.jfc都定义了gc、method-profiling等 control,尝试使用jfr configure --input default.jfc,profile.jfc会报错:Control with 'gc' is declared in multiple files- 正确的做法:只使用一个基础 JFC 文件(如
default.jfc或profile.jfc),然后通过命令行参数覆盖配置 - JVM 参数和 jcmd 命令的区别:虽然
jfr configure不能合并有冲突的 JFC 文件,但可以通过 JVM 参数(-XX:StartFlightRecording=settings=default,settings=profile)或jcmd命令(settings=default,settings=profile)合并,因为 JVM 运行时只读取最终的 setting 值,不关心 control 元素
5.5.2. 手动编辑 JFC 文件#
可以直接编辑 XML 文件,但需要注意:
- 必须符合
jfc.xsdSchema - 建议使用支持 XML Schema 验证的编辑器
- 修改后可以使用
jfr configure --input <file>验证格式
验证 JFC 文件:
# 方法 1:尝试加载并输出到临时文件(验证后删除)
# 注意:输出文件必须以 .jfc 结尾,不能使用 /dev/null
jfr configure --input custom.jfc --output /tmp/validate.jfc && rm /tmp/validate.jfc
# 方法 2:输出到当前目录的临时文件(验证后删除)
jfr configure --input custom.jfc --output validate-temp.jfc && rm validate-temp.jfc
验证原理:
jfr configure命令在解析和保存 JFC 文件时会进行格式验证- 如果 JFC 文件格式错误(如 XML 格式错误、不符合 Schema、control 引用错误等),会在解析或保存阶段抛出异常
- 输出文件必须是以
.jfc结尾的有效文件路径,不能使用/dev/null等特殊设备文件
5.6. 使用 JFC 配置文件#
5.6.1. 通过 JVM 参数使用#
指定单个 JFC 文件:
# 使用预定义配置(default.jfc)
-XX:StartFlightRecording=settings=default
# 使用预定义配置(profile.jfc)
-XX:StartFlightRecording=settings=profile
# 使用自定义 JFC 文件(完整路径)
-XX:StartFlightRecording=settings=/path/to/custom.jfc
# 使用自定义 JFC 文件(相对路径,相对于当前工作目录)
-XX:StartFlightRecording=settings=./custom.jfc
# 不使用任何预定义配置(从空白配置开始)
-XX:StartFlightRecording=settings=none
指定多个 JFC 文件(合并配置):
# 合并多个 JFC 文件,后面的配置会覆盖前面的同名配置
# 注意:只能合并不包含相同 control 名称的 JFC 文件
-XX:StartFlightRecording=settings=default,settings=profile
# 合并自定义配置和预定义配置
-XX:StartFlightRecording=settings=default,settings=/path/to/custom.jfc
合并规则:
- 多个 JFC 文件按顺序加载和合并
- 后面文件中的配置会覆盖前面文件中的同名配置
- 如果多个文件定义了同一个事件的同一个配置项,最后一个文件的值生效
- 重要限制:控制(control)元素不能重复定义。如果多个文件定义了相同的 control 名称(如
gc、method-profiling等),jfr configure命令会抛出异常:Control with 'gc' is declared in multiple files - 实际影响:由于
default.jfc和profile.jfc都定义了相同的 control 名称(如gc、method-profiling等),它们不能通过jfr configure --input default.jfc,profile.jfc合并。但可以通过 JVM 参数或jcmd命令的settings=default,settings=profile方式合并,因为 JVM 在运行时只读取最终的 setting 值,不关心 control 元素
同时指定 JFC 文件和覆盖配置:
# 使用 default.jfc,并覆盖特定事件的配置
-XX:StartFlightRecording=settings=default,jdk.ThreadSleep#threshold=50ms,jdk.CPULoad#period=2s
5.6.2. 通过 jcmd 命令使用#
# 启动记录,使用 default.jfc
jcmd <pid> JFR.start settings=default
# 启动记录,使用自定义 JFC 文件
jcmd <pid> JFR.start settings=/path/to/custom.jfc
# 启动记录,使用多个 JFC 文件
# 注意:虽然 default.jfc 和 profile.jfc 不能通过 jfr configure 合并(因为 control 名称冲突),
# 但可以通过 JVM 参数或 jcmd 命令合并,因为 JVM 运行时只读取最终的 setting 值
jcmd <pid> JFR.start settings=default,settings=profile
# 启动记录,使用 JFC 文件并覆盖配置
jcmd <pid> JFR.start settings=default,jdk.ThreadSleep#threshold=50ms
5.6.3. 通过 JDK API 使用#
import jdk.jfr.Configuration;
import jdk.jfr.Recording;
// 加载预定义配置
Configuration config = Configuration.getConfiguration("default");
// 从文件加载配置
Configuration config = Configuration.create(Path.of("/path/to/custom.jfc"));
// 从 Reader 加载配置
Configuration config = Configuration.create(new FileReader("/path/to/custom.jfc"));
// 使用配置创建记录
Recording recording = new Recording(config);
recording.start();
5.6.4. 通过 JMX 使用#
import javax.management.MBeanServer;
import com.sun.management.HotSpotDiagnosticMXBean;
// 获取 FlightRecorderMXBean
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
FlightRecorderMXBean frBean = ManagementFactory.newPlatformMXBeanProxy(
server,
"jdk.management.jfr:type=FlightRecorder",
FlightRecorderMXBean.class
);
// 启动记录,使用 default.jfc
long recordingId = frBean.newRecording();
frBean.setConfiguration(recordingId, "default");
frBean.startRecording(recordingId);
5.7. JFC 文件查找路径#
当指定 JFC 文件时,JFR 按以下顺序查找:
- 预定义配置名称:如果名称是
"default"或"profile",从JAVA_HOME/lib/jfr/目录加载 - JAVA_HOME/lib/jfr/ 目录:如果文件在
JAVA_HOME/lib/jfr/目录中,可以直接使用文件名(不需要完整路径) - 完整路径:如果提供了完整路径,直接使用该路径
- 相对路径:如果提供了相对路径,相对于当前工作目录
示例:
# 以下方式等价(如果 custom.jfc 在 JAVA_HOME/lib/jfr/ 目录中)
-XX:StartFlightRecording=settings=custom
-XX:StartFlightRecording=settings=custom.jfc
-XX:StartFlightRecording=settings=$JAVA_HOME/lib/jfr/custom.jfc
# 使用完整路径
-XX:StartFlightRecording=settings=/home/user/custom.jfc
# 使用相对路径(相对于当前工作目录)
-XX:StartFlightRecording=settings=./config/custom.jfc
5.8. 常用配置示例#
5.8.1. 生产环境低开销配置#
基于 default.jfc,禁用高开销事件:
<configuration version="2.0" label="Production" description="Low overhead production configuration">
<!-- 禁用方法执行采样(开销较大) -->
<event name="jdk.ExecutionSample">
<setting name="enabled">false</setting>
</event>
<!-- 禁用对象分配采样(开销较大) -->
<event name="jdk.ObjectAllocationSample">
<setting name="enabled">false</setting>
</event>
<!-- 启用关键事件,但提高阈值以减少事件数量 -->
<event name="jdk.ThreadSleep">
<setting name="enabled">true</setting>
<setting name="threshold">100 ms</setting>
<setting name="stackTrace">false</setting>
</event>
</configuration>
5.8.2. 性能分析配置#
基于 profile.jfc,启用更多事件和堆栈跟踪:
<configuration version="2.0" label="Profiling" description="Performance profiling configuration">
<!-- 启用方法执行采样,降低采样频率 -->
<event name="jdk.ExecutionSample">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
<setting name="throttle">50/s</setting>
</event>
<!-- 启用对象分配采样 -->
<event name="jdk.ObjectAllocationSample">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
<setting name="throttle">100/s</setting>
</event>
<!-- 降低锁等待阈值,捕获更多锁竞争 -->
<event name="jdk.JavaMonitorEnter">
<setting name="enabled">true</setting>
<setting name="threshold">1 ms</setting>
<setting name="stackTrace">true</setting>
</event>
</configuration>
5.8.3. 内存泄漏分析配置#
启用 OldObjectSample 事件并配置 GC root 路径:
<configuration version="2.0" label="Memory Leak Analysis" description="Configuration for memory leak analysis">
<!-- 启用旧对象采样 -->
<event name="jdk.OldObjectSample">
<setting name="enabled">true</setting>
<setting name="cutoff">infinity</setting>
</event>
<!-- 启用 GC 相关事件 -->
<event name="jdk.GCPhase">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
</event>
</configuration>
使用方式:
# 启动记录时指定 path-to-gc-roots=true(在记录级别配置中)
-XX:StartFlightRecording=settings=memory-leak.jfc,path-to-gc-roots=true
5.9. 最佳实践#
生产环境:
- 使用
default.jfc或基于它创建的低开销配置 - 禁用高开销事件(如
ExecutionSample、ObjectAllocationSample) - 提高事件阈值,减少事件数量
- 禁用不必要的堆栈跟踪
- 使用
问题诊断:
- 使用
profile.jfc或基于它创建的配置 - 启用相关事件和堆栈跟踪
- 根据需要调整采样频率和阈值
- 使用
配置管理:
- 将自定义 JFC 文件纳入版本控制
- 为不同场景创建不同的配置文件
- 使用
jfr configure命令创建和修改配置,避免手动编辑 XML
配置验证:
- 使用
jfr configure --input <file>验证配置文件格式 - 在测试环境验证配置效果后再用于生产环境
- 使用
6. 通过 JDK API 使用 JFR#
JDK 提供了完整的 JFR API,允许在应用程序中直接控制 JFR 记录、创建自定义事件、读取和分析 JFR 数据。JFR API 位于 jdk.jfr 包中,主要包含以下核心类:
jdk.jfr.Recording:用于创建、配置、启动和停止 JFR 记录jdk.jfr.FlightRecorder:用于访问 Flight Recorder 实例和管理记录jdk.jfr.Configuration:用于加载和管理 JFC 配置文件jdk.jfr.Event:用于定义自定义事件jdk.jfr.consumer.*:用于读取和分析 JFR 数据
6.1. 基本 API 类#
6.1.1. FlightRecorder 类#
FlightRecorder 是访问 Flight Recorder 实例的入口点。
主要方法:
static FlightRecorder getFlightRecorder():获取 Flight Recorder 实例List<Recording> getRecordings():获取所有正在运行的记录Recording takeSnapshot():创建所有记录数据的快照static void register(Class<? extends Event> eventClass):注册自定义事件类static void unregister(Class<? extends Event> eventClass):注销事件类static boolean isAvailable():检查 JFR 是否可用static boolean isInitialized():检查 JFR 是否已初始化
使用示例:
import jdk.jfr.FlightRecorder;
import jdk.jfr.Recording;
// 检查 JFR 是否可用
if (!FlightRecorder.isAvailable()) {
System.out.println("JFR is not available");
return;
}
// 获取 Flight Recorder 实例
FlightRecorder recorder = FlightRecorder.getFlightRecorder();
// 获取所有记录
List<Recording> recordings = recorder.getRecordings();
System.out.println("Active recordings: " + recordings.size());
// 创建快照
Recording snapshot = recorder.takeSnapshot();
6.1.2. Recording 类#
Recording 类用于创建和管理 JFR 记录。
记录状态(RecordingState 枚举):
NEW:初始状态,记录已创建但未启动DELAYED:已调度延迟启动RUNNING:正在运行STOPPED:已停止,数据仍可用CLOSED:已关闭,资源已释放
主要方法:
void start():启动记录void scheduleStart(Duration delay):延迟启动boolean stop():停止记录void close():关闭记录并释放资源void dump(Path destination):转储记录数据到文件Recording copy(boolean stop):创建记录的副本void setSettings(Map<String, String> settings):设置事件配置Map<String, String> getSettings():获取当前配置void setName(String name):设置记录名称void setDestination(Path destination):设置停止时转储的目标路径void setMaxAge(Duration maxAge):设置最大保留时间void setMaxSize(long maxSize):设置最大大小RecordingState getState():获取记录状态
6.2. 创建和启动记录#
6.2.1. 使用默认配置创建记录#
import jdk.jfr.Recording;
import java.nio.file.Path;
import java.nio.file.Paths;
// 创建记录(使用默认配置)
Recording recording = new Recording();
// 设置记录名称
recording.setName("My Recording");
// 启动记录
recording.start();
// ... 应用程序运行 ...
// 停止记录
recording.stop();
// 转储到文件
Path destination = Paths.get("recording.jfr");
recording.dump(destination);
// 关闭记录
recording.close();
6.2.2. 使用预定义配置创建记录#
import jdk.jfr.Configuration;
import jdk.jfr.Recording;
import java.io.IOException;
import java.text.ParseException;
try {
// 加载预定义配置
Configuration config = Configuration.getConfiguration("default");
// 使用配置创建记录
Recording recording = new Recording(config);
recording.setName("Default Configuration Recording");
recording.start();
// ... 应用程序运行 ...
recording.stop();
recording.dump(Paths.get("recording.jfr"));
recording.close();
} catch (IOException | ParseException e) {
e.printStackTrace();
}
6.2.3. 使用自定义配置创建记录#
import jdk.jfr.Configuration;
import jdk.jfr.Recording;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.text.ParseException;
try {
// 从文件加载配置
Path configPath = Paths.get("custom.jfc");
Configuration config = Configuration.create(configPath);
// 使用配置创建记录
Recording recording = new Recording(config);
recording.setName("Custom Configuration Recording");
recording.start();
// ... 应用程序运行 ...
recording.stop();
recording.dump(Paths.get("recording.jfr"));
recording.close();
} catch (IOException | ParseException e) {
e.printStackTrace();
}
6.2.4. 使用 Map 配置创建记录#
import jdk.jfr.Recording;
import java.util.HashMap;
import java.util.Map;
// 创建配置 Map
Map<String, String> settings = new HashMap<>();
settings.put("jdk.ThreadSleep#enabled", "true");
settings.put("jdk.ThreadSleep#threshold", "20 ms");
settings.put("jdk.CPULoad#enabled", "true");
settings.put("jdk.CPULoad#period", "1 s");
// 使用配置创建记录
Recording recording = new Recording(settings);
recording.setName("Custom Settings Recording");
recording.start();
// ... 应用程序运行 ...
recording.stop();
recording.dump(Paths.get("recording.jfr"));
recording.close();
6.3. 配置记录#
6.3.1. 使用 EventSettings API 配置事件#
import jdk.jfr.Recording;
import java.time.Duration;
Recording recording = new Recording();
// 启用事件并配置
recording.enable("jdk.ThreadSleep")
.withThreshold(Duration.ofMillis(20))
.withStackTrace();
recording.enable("jdk.CPULoad")
.withPeriod(Duration.ofSeconds(1));
// 禁用事件
recording.disable("jdk.ExecutionSample");
recording.start();
// ... 应用程序运行 ...
recording.stop();
recording.dump(Paths.get("recording.jfr"));
recording.close();
6.3.2. 使用 Map 配置事件#
import jdk.jfr.Recording;
import java.util.HashMap;
import java.util.Map;
Recording recording = new Recording();
// 获取当前配置
Map<String, String> settings = recording.getSettings();
// 添加或修改配置
settings.put("jdk.ThreadSleep#enabled", "true");
settings.put("jdk.ThreadSleep#threshold", "20 ms");
settings.put("jdk.ThreadSleep#stackTrace", "true");
settings.put("jdk.CPULoad#enabled", "true");
settings.put("jdk.CPULoad#period", "1 s");
// 应用配置
recording.setSettings(settings);
recording.start();
// ... 应用程序运行 ...
recording.stop();
recording.dump(Paths.get("recording.jfr"));
recording.close();
6.3.3. 配置记录选项#
import jdk.jfr.Recording;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
Recording recording = new Recording();
// 设置记录名称
recording.setName("Production Monitoring");
// 设置最大保留时间(2 天)
recording.setMaxAge(Duration.ofDays(2));
// 设置最大大小(500MB)
recording.setMaxSize(500 * 1024 * 1024);
// 设置停止时转储的目标路径
recording.setDestination(Paths.get("production-recording.jfr"));
// 启动记录
recording.start();
// ... 应用程序运行 ...
// 停止记录(会自动转储到 destination)
recording.stop();
recording.close();
6.4. 延迟启动和自动停止#
import jdk.jfr.Recording;
import java.time.Duration;
Recording recording = new Recording();
recording.setName("Delayed Recording");
// 延迟 5 分钟后启动
recording.scheduleStart(Duration.ofMinutes(5));
// 或者立即启动,但设置自动停止时间
recording.start();
// 注意:Recording 类本身不提供自动停止功能
// 需要在单独的线程中等待指定时间后调用 stop()
// 在单独线程中自动停止
new Thread(() -> {
try {
Thread.sleep(Duration.ofMinutes(30).toMillis());
recording.stop();
recording.dump(Paths.get("recording.jfr"));
recording.close();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
6.5. 创建记录快照#
import jdk.jfr.FlightRecorder;
import jdk.jfr.Recording;
import java.nio.file.Paths;
FlightRecorder recorder = FlightRecorder.getFlightRecorder();
// 创建所有记录数据的快照
Recording snapshot = recorder.takeSnapshot();
snapshot.setName("Snapshot");
// 转储快照
snapshot.dump(Paths.get("snapshot.jfr"));
snapshot.close();
6.6. 复制记录#
import jdk.jfr.Recording;
Recording recording = new Recording();
recording.setName("Original Recording");
recording.start();
// ... 应用程序运行 ...
// 创建记录的副本(不停止原记录)
Recording copy = recording.copy(false);
copy.setName("Copy of Original");
copy.dump(Paths.get("copy.jfr"));
copy.close();
// 原记录继续运行
// ...
recording.stop();
recording.dump(Paths.get("original.jfr"));
recording.close();
6.7. 创建自定义事件#
6.7.1. 基本自定义事件#
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Description;
@Label("User Action")
@Description("Records user actions in the application")
public class UserActionEvent extends Event {
@Label("Action Type")
@Description("Type of user action")
public String actionType;
@Label("User ID")
@Description("Identifier of the user")
public String userId;
@Label("Timestamp")
@Description("When the action occurred")
public long timestamp;
}
// 使用事件
public void performUserAction(String userId, String actionType) {
UserActionEvent event = new UserActionEvent();
event.actionType = actionType;
event.userId = userId;
event.timestamp = System.currentTimeMillis();
event.commit(); // 提交事件
}
6.7.2. 持续时间事件#
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Description;
@Label("Database Query")
@Description("Records database query execution")
public class DatabaseQueryEvent extends Event {
@Label("Query SQL")
public String sql;
@Label("Rows Returned")
public int rowsReturned;
public void begin() {
super.begin();
}
public void end() {
super.end();
}
}
// 使用持续时间事件
public List<Row> executeQuery(String sql) {
DatabaseQueryEvent event = new DatabaseQueryEvent();
event.sql = sql;
event.begin();
try {
List<Row> rows = database.execute(sql);
event.rowsReturned = rows.size();
return rows;
} finally {
event.end();
event.commit();
}
}
6.7.3. 使用 shouldCommit() 优化性能#
import jdk.jfr.Event;
public class ExpensiveEvent extends Event {
public String expensiveData;
public void recordExpensiveOperation() {
ExpensiveEvent event = new ExpensiveEvent();
// 检查事件是否会被记录
if (event.shouldCommit()) {
// 只有会被记录时才执行昂贵的操作
event.expensiveData = computeExpensiveData();
event.commit();
}
}
private String computeExpensiveData() {
// 昂贵的计算操作
return "expensive data";
}
}
6.7.4. 注册自定义事件#
import jdk.jfr.FlightRecorder;
import jdk.jfr.Event;
// 注册自定义事件类(可选,通常会自动注册)
FlightRecorder.register(UserActionEvent.class);
// 使用事件
UserActionEvent event = new UserActionEvent();
event.actionType = "login";
event.userId = "user123";
event.commit();
6.8. 读取 JFR 文件#
6.8.1. 使用 RecordingFile 读取事件#
import jdk.jfr.consumer.RecordingFile;
import jdk.jfr.consumer.RecordedEvent;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
Path recordingPath = Paths.get("recording.jfr");
try (RecordingFile recordingFile = new RecordingFile(recordingPath)) {
// 读取所有事件
while (recordingFile.hasMoreEvents()) {
RecordedEvent event = recordingFile.readEvent();
// 获取事件信息
String eventName = event.getEventType().getName();
long startTime = event.getStartTime().toEpochMilli();
// 获取事件字段
if (eventName.equals("jdk.ThreadSleep")) {
Duration duration = event.getDuration();
System.out.println("Thread sleep: " + duration.toMillis() + " ms");
} else if (eventName.equals("jdk.CPULoad")) {
double jvmUser = event.getDouble("jvmUser");
double jvmSystem = event.getDouble("jvmSystem");
System.out.println("CPU Load - User: " + jvmUser + "%, System: " + jvmSystem + "%");
}
}
} catch (IOException e) {
e.printStackTrace();
}
6.8.2. 过滤特定事件#
import jdk.jfr.consumer.RecordingFile;
import jdk.jfr.consumer.RecordedEvent;
import java.nio.file.Paths;
import java.io.IOException;
try (RecordingFile recordingFile = new RecordingFile(Paths.get("recording.jfr"))) {
while (recordingFile.hasMoreEvents()) {
RecordedEvent event = recordingFile.readEvent();
// 只处理特定事件
String eventName = event.getEventType().getName();
if (eventName.equals("jdk.GarbageCollection")) {
String gcName = event.getString("name");
Duration duration = event.getDuration();
System.out.println("GC: " + gcName + ", Duration: " + duration.toMillis() + " ms");
}
}
} catch (IOException e) {
e.printStackTrace();
}
6.8.3. 读取事件类型信息#
import jdk.jfr.consumer.RecordingFile;
import jdk.jfr.EventType;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.List;
try (RecordingFile recordingFile = new RecordingFile(Paths.get("recording.jfr"))) {
// 获取所有事件类型
List<EventType> eventTypes = recordingFile.readEventTypes();
for (EventType eventType : eventTypes) {
System.out.println("Event: " + eventType.getName());
System.out.println(" Label: " + eventType.getLabel());
System.out.println(" Description: " + eventType.getDescription());
}
} catch (IOException e) {
e.printStackTrace();
}
6.9. 流式处理(RecordingStream)#
RecordingStream(JDK 14+)提供了流式处理 JFR 事件的能力,可以从当前 JVM 实时读取事件。
6.9.1. 基本流式处理#
import jdk.jfr.consumer.RecordingStream;
import jdk.jfr.consumer.RecordedEvent;
import java.time.Duration;
try (RecordingStream stream = new RecordingStream()) {
// 启用事件
stream.enable("jdk.ThreadSleep");
stream.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
// 注册事件处理器
stream.onEvent("jdk.ThreadSleep", event -> {
Duration duration = event.getDuration();
System.out.println("Thread sleep: " + duration.toMillis() + " ms");
});
stream.onEvent("jdk.CPULoad", event -> {
double jvmUser = event.getDouble("jvmUser");
System.out.println("CPU Load: " + jvmUser + "%");
});
// 启动流(异步处理)
stream.startAsync();
// 运行一段时间
Thread.sleep(Duration.ofMinutes(5).toMillis());
// 停止流
stream.stop();
} catch (Exception e) {
e.printStackTrace();
}
6.9.2. 使用配置创建流#
import jdk.jfr.Configuration;
import jdk.jfr.consumer.RecordingStream;
import jdk.jfr.consumer.RecordedEvent;
import java.io.IOException;
import java.text.ParseException;
try {
// 加载配置
Configuration config = Configuration.getConfiguration("default");
// 使用配置创建流
try (RecordingStream stream = new RecordingStream(config)) {
// 注册所有事件处理器
stream.onEvent(event -> {
System.out.println("Event: " + event.getEventType().getName());
});
// 设置最大保留时间
stream.setMaxAge(Duration.ofMinutes(10));
// 启动流
stream.startAsync();
// 运行
Thread.sleep(Duration.ofMinutes(5).toMillis());
stream.stop();
}
} catch (IOException | ParseException e) {
e.printStackTrace();
}
6.9.3. 流式处理并转储#
import jdk.jfr.consumer.RecordingStream;
import jdk.jfr.consumer.RecordedEvent;
import java.nio.file.Paths;
import java.time.Duration;
try (RecordingStream stream = new RecordingStream()) {
stream.enable("jdk.ThreadSleep");
stream.enable("jdk.CPULoad");
// 设置最大保留时间
stream.setMaxAge(Duration.ofMinutes(10));
// 注册事件处理器
stream.onEvent(event -> {
// 处理事件
System.out.println("Event: " + event.getEventType().getName());
});
// 启动流
stream.start();
// 运行一段时间
Thread.sleep(Duration.ofMinutes(5).toMillis());
// 转储到文件
stream.dump(Paths.get("stream-recording.jfr"));
// 停止流
stream.stop();
} catch (Exception e) {
e.printStackTrace();
}
6.10. 完整示例#
以下是一个完整的示例,展示了如何使用 JFR API 进行性能监控:
import jdk.jfr.*;
import jdk.jfr.consumer.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
public class JFRExample {
// 自定义事件
@Label("Business Operation")
@Description("Records business operations")
static class BusinessOperationEvent extends Event {
@Label("Operation Name")
String operationName;
@Label("Result")
String result;
}
public static void main(String[] args) throws Exception {
// 1. 创建记录
Recording recording = new Recording();
recording.setName("Performance Monitoring");
// 2. 配置事件
recording.enable("jdk.ThreadSleep")
.withThreshold(Duration.ofMillis(20))
.withStackTrace();
recording.enable("jdk.CPULoad")
.withPeriod(Duration.ofSeconds(1));
// 3. 设置记录选项
recording.setMaxAge(Duration.ofHours(1));
recording.setMaxSize(100 * 1024 * 1024); // 100MB
// 4. 启动记录
recording.start();
System.out.println("Recording started");
// 5. 执行业务逻辑
performBusinessOperations();
// 6. 停止记录
recording.stop();
System.out.println("Recording stopped");
// 7. 转储记录
Path destination = Paths.get("performance.jfr");
recording.dump(destination);
System.out.println("Recording dumped to: " + destination);
// 8. 读取和分析记录
analyzeRecording(destination);
// 9. 关闭记录
recording.close();
}
private static void performBusinessOperations() throws InterruptedException {
// 模拟业务操作
for (int i = 0; i < 10; i++) {
BusinessOperationEvent event = new BusinessOperationEvent();
event.begin();
// 模拟操作
Thread.sleep(100);
event.operationName = "Operation " + i;
event.result = "Success";
event.end();
event.commit();
}
}
private static void analyzeRecording(Path recordingPath) {
try (RecordingFile recordingFile = new RecordingFile(recordingPath)) {
int threadSleepCount = 0;
int businessOpCount = 0;
while (recordingFile.hasMoreEvents()) {
RecordedEvent event = recordingFile.readEvent();
String eventName = event.getEventType().getName();
if (eventName.equals("jdk.ThreadSleep")) {
threadSleepCount++;
} else if (eventName.equals("Business Operation")) {
businessOpCount++;
String operationName = event.getString("operationName");
System.out.println("Business Operation: " + operationName);
}
}
System.out.println("Thread Sleep events: " + threadSleepCount);
System.out.println("Business Operation events: " + businessOpCount);
} catch (Exception e) {
e.printStackTrace();
}
}
}
6.11. 注意事项#
资源管理:
Recording和RecordingFile实现了Closeable接口,应该使用 try-with-resources 语句确保资源被正确释放记录状态:在调用方法前检查记录状态,某些操作只能在特定状态下执行
性能开销:虽然 JFR 开销很低,但在高频率路径中创建事件对象仍会有开销,使用
shouldCommit()可以优化性能事件字段类型:事件字段只支持特定类型(基本类型、String、Thread、Class),其他类型会被忽略
线程安全:
Recording类的方法不是线程安全的,如果从多个线程访问,需要同步流式处理:
RecordingStream适合实时监控场景,但要注意设置maxAge或maxSize以避免内存占用过大
7. 通过 JMX 使用 JFR#
JMX(Java Management Extensions)是 Java 平台提供的管理和监控框架。JFR 通过 FlightRecorderMXBean 接口暴露了完整的 JMX 管理接口,允许通过 JMX 远程或本地管理 JFR 记录。
ObjectName:jdk.management.jfr:type=FlightRecorder
7.1. 获取 FlightRecorderMXBean#
7.1.1. 本地获取(同一 JVM 进程)#
import java.lang.management.ManagementFactory;
import jdk.management.jfr.FlightRecorderMXBean;
// 通过 ManagementFactory 获取(推荐)
FlightRecorderMXBean bean = ManagementFactory.getPlatformMXBean(FlightRecorderMXBean.class);
// 或者通过 MBeanServer 获取
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
ObjectName objectName = new ObjectName("jdk.management.jfr:type=FlightRecorder");
FlightRecorderMXBean bean = JMX.newMXBeanProxy(server, objectName, FlightRecorderMXBean.class);
7.1.2. 远程获取(不同 JVM 进程)#
方式 1:通过 JMX Service URL 连接
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.management.JMX;
import javax.management.ObjectName;
import jdk.management.jfr.FlightRecorderMXBean;
// 创建 JMX Service URL
String host = "localhost";
int port = 9999;
String url = "service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi";
// 连接并获取 MBeanServerConnection
JMXServiceURL jmxURL = new JMXServiceURL(url);
JMXConnector connector = JMXConnectorFactory.connect(jmxURL);
MBeanServerConnection connection = connector.getMBeanServerConnection();
// 获取 FlightRecorderMXBean
ObjectName objectName = new ObjectName("jdk.management.jfr:type=FlightRecorder");
FlightRecorderMXBean bean = JMX.newMXBeanProxy(connection, objectName, FlightRecorderMXBean.class);
方式 2:通过 Attach API 连接(本地进程)
import com.sun.tools.attach.VirtualMachine;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.management.JMX;
import javax.management.ObjectName;
import jdk.management.jfr.FlightRecorderMXBean;
// 通过 PID 附加到目标 JVM
String pid = "12345";
VirtualMachine vm = VirtualMachine.attach(pid);
// 启动本地管理代理并获取 JMX Service URL
String jmxServiceUrl = vm.startLocalManagementAgent();
// 连接并获取 FlightRecorderMXBean
JMXServiceURL jmxURL = new JMXServiceURL(jmxServiceUrl);
JMXConnector connector = JMXConnectorFactory.connect(jmxURL);
MBeanServerConnection connection = connector.getMBeanServerConnection();
ObjectName objectName = new ObjectName("jdk.management.jfr:type=FlightRecorder");
FlightRecorderMXBean bean = JMX.newMXBeanProxy(connection, objectName, FlightRecorderMXBean.class);
启用远程 JMX 连接:
目标 JVM 需要启用 JMX 远程连接:
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
7.2. 记录管理#
7.2.1. 创建记录#
// 创建新记录(不启动)
long recordingId = bean.newRecording();
7.2.2. 启动和停止记录#
// 启动记录
bean.startRecording(recordingId);
// 停止记录(返回 true 表示成功停止)
boolean stopped = bean.stopRecording(recordingId);
7.2.3. 关闭记录#
// 关闭记录并释放资源
bean.closeRecording(recordingId);
7.2.4. 复制记录#
// 复制记录(不停止原记录)
long cloneId = bean.cloneRecording(recordingId, false);
// 复制记录并停止副本
long cloneId = bean.cloneRecording(recordingId, true);
7.2.5. 创建快照#
// 创建所有记录数据的快照
long snapshotId = bean.takeSnapshot();
7.3. 记录配置#
7.3.1. 记录选项(Recording Options)#
记录选项控制记录的行为(如持续时间、最大大小、转储路径等)。
支持的选项:
name:记录名称(String)maxAge:最大保留时间(格式:"2 h"、"24 h"、"2 d"、"0"表示无限制)maxSize:最大总大小(格式:字节数,如"1000000000","0"表示无限制)dumpOnExit:JVM 退出时是否转储("true"或"false")destination:转储文件路径(String,相对路径相对于 JVM 启动目录)disk:是否写入磁盘("true"或"false")duration:记录持续时间(格式:"60 s"、"10 m"、"4 h"、"0"表示无限制)
设置记录选项:
import java.util.HashMap;
import java.util.Map;
Map<String, String> options = new HashMap<>();
options.put("name", "My Recording");
options.put("maxAge", "2 h");
options.put("maxSize", "500000000"); // 500MB
options.put("dumpOnExit", "true");
options.put("destination", "/path/to/recording.jfr");
options.put("disk", "true");
options.put("duration", "1 h");
bean.setRecordingOptions(recordingId, options);
获取记录选项:
Map<String, String> options = bean.getRecordingOptions(recordingId);
System.out.println("Name: " + options.get("name"));
System.out.println("Max Age: " + options.get("maxAge"));
System.out.println("Max Size: " + options.get("maxSize"));
7.3.2. 记录设置(Recording Settings)#
记录设置控制哪些事件被记录以及如何记录(如事件阈值、采样间隔等)。
设置格式:<event-name>#<setting-name>=<value>
设置记录设置:
Map<String, String> settings = new HashMap<>();
settings.put("jdk.ThreadSleep#enabled", "true");
settings.put("jdk.ThreadSleep#threshold", "20 ms");
settings.put("jdk.ThreadSleep#stackTrace", "true");
settings.put("jdk.CPULoad#enabled", "true");
settings.put("jdk.CPULoad#period", "1 s");
bean.setRecordingSettings(recordingId, settings);
获取记录设置:
Map<String, String> settings = bean.getRecordingSettings(recordingId);
for (Map.Entry<String, String> entry : settings.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
7.3.3. 使用预定义配置#
// 使用预定义配置(如 "default" 或 "profile")
bean.setPredefinedConfiguration(recordingId, "default");
7.3.4. 使用自定义配置(JFC 文件内容)#
// 从字符串加载 JFC 配置内容
String jfcContents = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<configuration version=\"2.0\" label=\"Custom\" description=\"Custom configuration\">\n" +
" <event name=\"jdk.ThreadSleep\">\n" +
" <setting name=\"enabled\">true</setting>\n" +
" <setting name=\"threshold\">20 ms</setting>\n" +
" </event>\n" +
"</configuration>";
bean.setConfiguration(recordingId, jfcContents);
7.4. 查询信息#
7.4.1. 获取所有记录#
import java.util.List;
import jdk.management.jfr.RecordingInfo;
List<RecordingInfo> recordings = bean.getRecordings();
for (RecordingInfo info : recordings) {
System.out.println("ID: " + info.getId());
System.out.println("Name: " + info.getName());
System.out.println("State: " + info.getState());
System.out.println("Size: " + info.getSize());
System.out.println("Start Time: " + info.getStartTime());
}
RecordingInfo 字段:
id:记录 IDname:记录名称state:记录状态(NEW、DELAYED、RUNNING、STOPPED、CLOSED)size:记录大小(字节)startTime:开始时间(毫秒时间戳)stopTime:停止时间(毫秒时间戳)duration:持续时间(秒)maxAge:最大保留时间(秒)maxSize:最大大小(字节)dumpOnExit:是否在退出时转储toDisk:是否写入磁盘destination:转储目标路径settings:事件设置(Map<String, String>)
7.4.2. 获取预定义配置#
import java.util.List;
import jdk.management.jfr.ConfigurationInfo;
List<ConfigurationInfo> configs = bean.getConfigurations();
for (ConfigurationInfo config : configs) {
System.out.println("Name: " + config.getName());
System.out.println("Label: " + config.getLabel());
System.out.println("Description: " + config.getDescription());
}
ConfigurationInfo 字段:
name:配置名称(如"default"、"profile")label:显示标签description:描述信息provider:提供者(如"OpenJDK")contents:JFC 文件内容(XML 字符串)settings:配置的事件设置(Map<String, String>)
7.4.3. 获取事件类型#
import java.util.List;
import jdk.management.jfr.EventTypeInfo;
List<EventTypeInfo> eventTypes = bean.getEventTypes();
for (EventTypeInfo eventType : eventTypes) {
System.out.println("Name: " + eventType.getName());
System.out.println("Label: " + eventType.getLabel());
System.out.println("Description: " + eventType.getDescription());
// 获取事件设置描述符
List<SettingDescriptorInfo> settings = eventType.getSettingDescriptors();
for (SettingDescriptorInfo setting : settings) {
System.out.println(" Setting: " + setting.getName() +
" (default: " + setting.getDefaultValue() + ")");
}
}
EventTypeInfo 字段:
id:事件类型 IDname:事件名称(如"jdk.ThreadSleep")label:显示标签description:描述信息categoryNames:分类名称列表settingDescriptors:设置描述符列表
7.5. 流式处理(Streaming)#
JMX 提供了流式处理接口,可以从正在运行的记录中实时读取数据。
7.5.1. 打开流#
import java.util.HashMap;
import java.util.Map;
import java.time.Instant;
// 流选项
Map<String, String> streamOptions = new HashMap<>();
streamOptions.put("startTime", "2020-03-17T09:00:00"); // ISO-8601 格式
streamOptions.put("endTime", "2020-03-17T10:00:00");
streamOptions.put("blockSize", "50000"); // 每次读取的最大字节数
streamOptions.put("streamVersion", "1.0"); // 必须为 "1.0" 才能读取正在运行的记录
// 打开流(recordingId=0 表示读取所有记录的数据)
long streamId = bean.openStream(recordingId, streamOptions);
流选项:
startTime:开始时间(ISO-8601 格式或毫秒时间戳,默认:Instant.MIN)endTime:结束时间(ISO-8601 格式或毫秒时间戳,默认:Instant.MAX)blockSize:每次读取的最大字节数(默认:50000)streamVersion:流版本("1.0"表示可以从正在运行的记录读取数据)
注意:
- 如果
streamVersion为"1.0",可以从正在运行的记录读取数据 - 如果未指定
streamVersion,记录必须已停止才能打开流
7.5.2. 读取流数据#
import java.io.FileOutputStream;
import java.io.IOException;
// 读取流数据并写入文件
try (FileOutputStream fos = new FileOutputStream("recording.jfr")) {
while (true) {
byte[] data = bean.readStream(streamId);
if (data == null) {
break; // 没有更多数据
}
fos.write(data);
}
}
7.5.3. 关闭流#
bean.closeStream(streamId);
7.6. 转储记录#
// 转储记录到文件(在目标 JVM 的机器上)
bean.copyTo(recordingId, "/path/to/recording.jfr");
注意:如果通过远程 JMX 调用,文件会写入到目标 JVM 运行的机器上,而不是客户端机器。
7.7. 通知机制#
FlightRecorderMXBean 实现了 NotificationEmitter 接口,可以监听记录状态变化。
import javax.management.Notification;
import javax.management.NotificationListener;
import javax.management.NotificationFilter;
// 创建通知监听器
NotificationListener listener = new NotificationListener() {
@Override
public void handleNotification(Notification notification, Object handback) {
System.out.println("Notification: " + notification.getMessage());
System.out.println("Type: " + notification.getType());
System.out.println("User Data: " + notification.getUserData());
}
};
// 添加通知监听器
bean.addNotificationListener(listener, null, null);
// 移除通知监听器
bean.removeNotificationListener(listener);
通知类型:AttributeChangeNotification.ATTRIBUTE_CHANGE
通知内容:当记录状态改变时(如启动、停止、关闭),会发送通知。
7.8. RemoteRecordingStream(JDK 16+)#
RemoteRecordingStream 提供了更高级的流式处理接口,类似于本地的 RecordingStream,但可以通过 JMX 远程连接使用。
7.8.1. 基本使用#
import jdk.management.jfr.RemoteRecordingStream;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.management.MBeanServerConnection;
import java.time.Duration;
// 连接远程 JVM
String host = "localhost";
int port = 9999;
String url = "service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi";
JMXServiceURL jmxURL = new JMXServiceURL(url);
JMXConnector connector = JMXConnectorFactory.connect(jmxURL);
MBeanServerConnection connection = connector.getMBeanServerConnection();
// 创建远程记录流
try (RemoteRecordingStream stream = new RemoteRecordingStream(connection)) {
// 启用事件
stream.enable("jdk.GCPhasePause").withoutThreshold();
stream.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
// 注册事件处理器
stream.onEvent("jdk.CPULoad", event -> {
System.out.println("CPU Load: " + event);
});
stream.onEvent("jdk.GCPhasePause", event -> {
System.out.println("GC Pause: " + event);
});
// 启动流
stream.start();
// 运行一段时间
Thread.sleep(Duration.ofMinutes(5).toMillis());
}
7.8.2. 使用配置#
try (RemoteRecordingStream stream = new RemoteRecordingStream(connection)) {
// 监听元数据事件以获取配置
stream.onMetadata(metadataEvent -> {
for (Configuration config : metadataEvent.getConfigurations()) {
if (config.getName().equals("default")) {
stream.setSettings(config.getSettings());
}
}
});
stream.onEvent(System.out::println);
stream.start();
Thread.sleep(Duration.ofMinutes(5).toMillis());
}
7.8.3. 指定临时目录#
import java.nio.file.Path;
import java.nio.file.Paths;
Path tempDir = Paths.get("/tmp/jfr-remote");
try (RemoteRecordingStream stream = new RemoteRecordingStream(connection, tempDir)) {
stream.enable("jdk.ThreadSleep");
stream.onEvent(System.out::println);
stream.start();
Thread.sleep(Duration.ofMinutes(5).toMillis());
}
7.9. 完整示例#
以下是一个完整的示例,展示了如何通过 JMX 管理 JFR 记录:
import java.lang.management.ManagementFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jdk.management.jfr.FlightRecorderMXBean;
import jdk.management.jfr.RecordingInfo;
public class JFRJMXExample {
public static void main(String[] args) throws Exception {
// 1. 获取 FlightRecorderMXBean
FlightRecorderMXBean bean = ManagementFactory.getPlatformMXBean(FlightRecorderMXBean.class);
// 2. 创建新记录
long recordingId = bean.newRecording();
System.out.println("Created recording: " + recordingId);
// 3. 配置记录选项
Map<String, String> options = new HashMap<>();
options.put("name", "JMX Recording");
options.put("maxAge", "1 h");
options.put("maxSize", "100000000"); // 100MB
options.put("dumpOnExit", "true");
options.put("destination", "jmx-recording.jfr");
options.put("disk", "true");
options.put("duration", "30 m");
bean.setRecordingOptions(recordingId, options);
// 4. 配置记录设置
Map<String, String> settings = new HashMap<>();
settings.put("jdk.ThreadSleep#enabled", "true");
settings.put("jdk.ThreadSleep#threshold", "20 ms");
settings.put("jdk.ThreadSleep#stackTrace", "true");
settings.put("jdk.CPULoad#enabled", "true");
settings.put("jdk.CPULoad#period", "1 s");
bean.setRecordingSettings(recordingId, settings);
// 5. 启动记录
bean.startRecording(recordingId);
System.out.println("Recording started");
// 6. 查询记录信息
List<RecordingInfo> recordings = bean.getRecordings();
for (RecordingInfo info : recordings) {
if (info.getId() == recordingId) {
System.out.println("Recording Name: " + info.getName());
System.out.println("Recording State: " + info.getState());
System.out.println("Recording Size: " + info.getSize());
}
}
// 7. 运行一段时间
Thread.sleep(60000); // 1 分钟
// 8. 停止记录
bean.stopRecording(recordingId);
System.out.println("Recording stopped");
// 9. 转储记录
bean.copyTo(recordingId, "jmx-recording.jfr");
System.out.println("Recording dumped to jmx-recording.jfr");
// 10. 关闭记录
bean.closeRecording(recordingId);
System.out.println("Recording closed");
}
}
7.10. 注意事项#
远程文件路径:通过远程 JMX 调用
copyTo()时,文件会写入到目标 JVM 运行的机器上,而不是客户端机器。流版本:要从正在运行的记录读取数据,必须设置
streamVersion="1.0"。资源管理:使用完流后,应该调用
closeStream()释放资源。通知监听器:如果不再需要监听通知,应该移除监听器以避免内存泄漏。
异常处理:JMX 操作可能抛出
IOException、IllegalArgumentException、IllegalStateException等异常,需要适当处理。连接管理:使用远程 JMX 时,需要管理
JMXConnector的生命周期,使用完后应该关闭连接。权限要求:远程 JMX 连接可能需要配置认证和 SSL,具体取决于目标 JVM 的配置。
RemoteRecordingStream:
RemoteRecordingStream实现了AutoCloseable接口,应该使用 try-with-resources 语句确保资源被正确释放。
8. 通过 jfr 工具分析 JFR 文件#
jfr 是 JDK 提供的命令行工具,用于打印、分析和操作 JFR 记录文件(.jfr)。jfr 工具的主要功能是将二进制格式的 JFR 文件转换为人类可读的格式,并提供过滤、汇总、清理、合并和拆分等功能。
基本语法:
jfr <command> [options] [file]
查看帮助信息:
# 查看所有可用命令
jfr help
# 查看特定命令的帮助
jfr help print
jfr help view
jfr help summary
jfr help metadata
jfr help scrub
jfr help assemble
jfr help disassemble
jfr help configure
# 查看版本信息
jfr version
# 或
jfr --version
8.1. jfr print(打印事件)#
jfr print 命令用于将 JFR 记录文件的内容打印到标准输出。
语法:
jfr print [--xml|--json|--exact] [--categories <filter>] [--events <filter>] [--stack-depth <depth>] <file>
选项:
--xml:以 XML 格式打印记录--json:以 JSON 格式打印记录--exact:以完整精度打印数字和时间戳--categories <filter>:选择匹配分类名称的事件- 过滤器是逗号分隔的名称列表,支持简单名称、限定名称和引用的 glob 模式
- 示例:
--categories "GC,JVM,Java*"
--events <filter>:选择匹配事件名称的事件- 过滤器是逗号分隔的名称列表,支持简单名称、限定名称和引用的 glob 模式
- 示例:
--events "jdk.ThreadSleep,jdk.CPULoad"、--events "jdk.*"
--stack-depth <depth>:堆栈跟踪的帧数(默认:5)
默认格式:如果不指定 --xml 或 --json,默认使用人类可读格式。
过滤器说明:
- 过滤器可以基于事件的符号名称(通过
@Name注解设置)或分类名称(通过@Category注解设置) - 如果使用多个过滤器,会包含所有匹配的事件(并集)
- 如果同时使用分类过滤器和事件过滤器,选择的事件是两个过滤器的并集
- 如果不使用过滤器,会打印所有事件
使用示例:
# 打印所有事件(默认格式)
jfr print recording.jfr
# 打印特定事件
jfr print --events jdk.ThreadSleep recording.jfr
# 打印多个事件
jfr print --events CPULoad,GarbageCollection recording.jfr
# 打印特定分类的事件
jfr print --categories "GC,JVM,Java*" recording.jfr
# 同时使用分类和事件过滤器
jfr print --categories GC --events CPULoad recording.jfr
# 使用 glob 模式
jfr print --events "jdk.*" --stack-depth 64 recording.jfr
# 以 JSON 格式输出
jfr print --json --events CPULoad recording.jfr
# 以 XML 格式输出
jfr print --xml --events jdk.ThreadSleep recording.jfr
# 使用完整精度
jfr print --exact --events "jdk.*" --stack-depth 64 recording.jfr
输出格式说明:
- 人类可读格式(默认):格式化的事件值,例如带有
@Percentage注解的字段值0.52会显示为52% - XML 格式:机器可读的 XML 格式,可以进一步解析或处理
- JSON 格式:机器可读的 JSON 格式,可以进一步解析或处理
堆栈跟踪:默认情况下,堆栈跟踪被截断为 5 帧,可以通过 --stack-depth 选项增加或减少。
8.2. jfr view(查看聚合视图)#
jfr view 命令用于以预定义的视图格式聚合和显示事件数据。
语法:
jfr view [--verbose] [--width <integer>] [--truncate <mode>] [--cell-height <integer>] <view> <file>
选项:
--verbose:显示构成视图的查询信息--width <integer>:视图的宽度(字符数,默认值取决于视图)--truncate <mode>:如何截断超出表格单元格空间的内容beginning:从开头截断end:从结尾截断(默认)
--cell-height <integer>:表格单元格的最大行数(默认值取决于视图)
视图参数:
<view>(必需):视图名称或事件类型名称- 预定义视图:
gc、hot-methods、allocation-by-class、contention-by-site等 - 事件类型:
jdk.GarbageCollection、jdk.ThreadStart等 - 特殊值:
types:列出所有可用的事件类型all-views:显示所有预定义视图all-events:显示所有事件
- 预定义视图:
预定义视图分类:
JVM 视图(jvm.*):
gc:垃圾回收统计gc-pauses:GC 暂停统计gc-configuration:GC 配置信息gc-parallel-phases:并行 GC 阶段gc-concurrent-phases:并发 GC 阶段gc-pause-phases:GC 暂停阶段gc-references:GC 引用统计gc-allocation-trigger:GC 分配触发gc-cpu-time:GC CPU 时间heap-configuration:堆配置compiler-configuration:编译器配置compiler-statistics:编译器统计compiler-phases:编译器阶段longest-compilations:最长编译safepoints:安全点vm-operations:VM 操作deoptimizations-by-reason:按原因的反优化deoptimizations-by-site:按位置的反优化class-modifications:类修改blocked-by-system-gc:被 System.gc() 阻塞native-memory-committed:已提交的本地内存native-memory-reserved:保留的本地内存tlabs:线程本地分配缓冲区
环境视图(environment.*):
cpu-load:CPU 负载cpu-load-samples:CPU 负载样本cpu-information:CPU 信息cpu-tsc:CPU 时间戳计数器system-information:系统信息system-properties:系统属性system-processes:系统进程environment-variables:环境变量network-utilization:网络利用率native-libraries:本地库native-library-failures:本地库加载/卸载失败container-configuration:容器配置container-cpu-usage:容器 CPU 使用container-memory-usage:容器内存使用container-io-usage:容器 I/O 使用container-cpu-throttling:容器 CPU 节流recording:记录信息active-recordings:活动记录active-settings:活动设置jvm-flags:JVM 标志jvm-information:JVM 信息events-by-count:按计数的事件类型events-by-name:按名称的事件类型
应用视图(application.*):
hot-methods:热点方法(执行最多的 Java 方法)cpu-time-hot-methods:CPU 时间热点方法cpu-time-statistics:CPU 时间采样统计allocation-by-class:按类分配allocation-by-thread:按线程分配allocation-by-site:按位置分配contention-by-thread:按线程的锁竞争contention-by-class:按锁类的锁竞争contention-by-site:按位置的锁竞争contention-by-address:按监视器地址的锁竞争exception-count:异常统计exception-by-type:按类型的异常exception-by-message:按消息的异常exception-by-site:按位置的异常memory-leaks-by-class:按类的内存泄漏候选memory-leaks-by-site:按位置的内存泄漏候选thread-count:Java 线程统计thread-allocation:线程分配统计thread-cpu-load:线程 CPU 负载thread-start:平台线程启动pinned-threads:固定的虚拟线程file-reads-by-path:按路径的文件读取file-writes-by-path:按路径的文件写入socket-reads-by-host:按主机的套接字读取socket-writes-by-host:按主机的套接字写入class-loaders:类加载器longest-class-loading:最长的类加载modules:模块monitor-inflation:监视器膨胀native-methods:等待或执行本地方法object-statistics:占用超过 1% 的对象finalizers:终结器deprecated-methods-for-removal:标记为移除的废弃方法method-timing:方法计时method-calls:方法调用latencies-by-type:按类型的延迟
使用示例:
# 查看 GC 视图
jfr view gc recording.jfr
# 查看热点方法视图
jfr view hot-methods recording.jfr
# 查看分配视图(按类)
jfr view allocation-by-class recording.jfr
# 查看锁竞争视图(按位置)
jfr view contention-by-site recording.jfr
# 查看指定事件类型
jfr view jdk.GarbageCollection recording.jfr
# 查看所有可用视图
jfr view all-views recording.jfr
# 查看所有事件类型
jfr view types recording.jfr
# 查看所有事件
jfr view all-events recording.jfr
# 自定义视图参数
jfr view --width 160 hot-methods recording.jfr
# 显示视图的查询信息
jfr view --verbose allocation-by-class recording.jfr
# 设置截断模式
jfr view --truncate beginning SystemProcess recording.jfr
# 设置单元格高度
jfr view --cell-height 10 ThreadStart recording.jfr
注意事项:
- 视图查询:每个视图都基于一个查询语句,可以通过
--verbose选项查看 - 事件类型:可以直接使用事件类型名称作为视图,会显示该事件类型的所有事件
- 性能:对于大型记录文件,某些视图可能需要较长时间处理
8.3. jfr summary(摘要统计)#
jfr summary 命令用于打印记录的统计信息,例如记录的事件数量和它们使用的磁盘空间。
语法:
jfr summary <file>
输出内容:
- 版本信息:JFR 文件格式版本(如
2.1) - Chunks 数量:记录文件包含的 chunk 数量
- 开始时间:记录开始时间(UTC)
- 持续时间:记录持续时间(秒)
- 事件统计表:
- 事件类型名称
- 事件数量(Count)
- 事件大小(Size,字节)
使用示例:
# 查看记录摘要
jfr summary recording.jfr
输出示例:
Version: 2.1
Chunks: 1
Start: 2025-11-17 09:41:00 (UTC)
Duration: 165 s
Event Type Count Size (bytes)
=============================================================
jdk.NativeMethodSample 6992 79112
jdk.GCPhaseParallel 6127 150540
jdk.ModuleExport 1615 19253
jdk.SystemProcess 1032 119761
jdk.NativeLibrary 877 76574
...
注意事项:
- 文件完整性:如果查看的是 repository 中的最后一个文件,可能会提示文件损坏(因为元数据还没有 flush),这是正常的,默认 1 秒执行一次 flush,多试几次就能看到正确的结果
- 排序:事件统计表按事件数量降序排序
8.4. jfr metadata(元数据信息)#
jfr metadata 命令用于显示事件元数据信息,如事件名称、分类和字段布局。
语法:
jfr metadata [--categories <filter>] [--events <filter>] [<file>]
选项:
--categories <filter>:选择匹配分类名称的事件--events <filter>:选择匹配事件名称的事件<file>(可选):JFR 记录文件路径- 如果省略,使用运行
jfr工具的 JDK 中的元数据
- 如果省略,使用运行
输出内容:
- 事件类型信息:
- 事件名称、标签、描述
- 分类名称
- 字段信息(字段名、类型、标签、描述)
- 设置描述符(如
enabled、threshold、period等)
使用示例:
# 显示所有事件元数据(使用 JDK 中的元数据)
jfr metadata
# 显示记录文件中的事件元数据
jfr metadata recording.jfr
# 显示特定事件的元数据
jfr metadata --events jdk.ThreadStart recording.jfr
# 显示多个事件的元数据
jfr metadata --events CPULoad,GarbageCollection recording.jfr
# 显示特定分类的事件元数据
jfr metadata --categories "GC,JVM,Java*" recording.jfr
# 使用 glob 模式
jfr metadata --events "Thread*" recording.jfr
注意事项:
- 元数据来源:如果不指定文件,使用运行
jfr工具的 JDK 中的元数据;如果指定文件,使用文件中的元数据 - 字段布局:元数据包含事件的完整字段布局信息,有助于理解事件结构
8.5. jfr scrub(清理记录)#
jfr scrub 命令用于从记录文件中移除敏感内容或减少文件大小。
语法:
jfr scrub [--include-events <filter>] [--exclude-events <filter>]
[--include-categories <filter>] [--exclude-categories <filter>]
[--include-threads <filter>] [--exclude-threads <filter>]
<input-file> [<output-file>]
选项:
--include-events <filter>:选择匹配事件名称的事件(只保留这些事件)--exclude-events <filter>:排除匹配事件名称的事件(移除这些事件)--include-categories <filter>:选择匹配分类名称的事件(只保留这些事件)--exclude-categories <filter>:排除匹配分类名称的事件(移除这些事件)--include-threads <filter>:选择匹配线程名称的事件(只保留这些线程的事件)--exclude-threads <filter>:排除匹配线程名称的事件(移除这些线程的事件)<input-file>(必需):输入文件路径<output-file>(可选):输出文件路径- 如果未指定,会在输入文件路径后追加
-scrubbed作为输出文件名
- 如果未指定,会在输入文件路径后追加
过滤器说明:
- 过滤器是逗号分隔的名称列表,支持简单名称、限定名称和引用的 glob 模式
- 如果使用多个过滤器,会按指定顺序应用
include和exclude可以组合使用
使用示例:
# 只保留套接字相关事件
jfr scrub --include-events 'jdk.Socket*' recording.jfr socket-only.jfr
# 移除包含密码的环境变量事件
jfr scrub --exclude-events InitialEnvironmentVariable recording.jfr no-psw.jfr
# 只保留主线程的事件
jfr scrub --include-threads main recording.jfr
# 排除特定线程的事件
jfr scrub --exclude-threads 'Foo*' recording.jfr
# 只保留特定分类的事件
jfr scrub --include-categories 'My App' recording.jfr
# 排除特定分类的事件
jfr scrub --exclude-categories JVM,OS recording.jfr
# 组合使用多个过滤器
jfr scrub --include-events 'jdk.Socket*' --exclude-threads 'Worker*' recording.jfr filtered.jfr
输出信息:
命令执行后会显示:
- 清理后的文件路径
- 移除的事件统计(事件名称和移除的百分比)
注意事项:
- 文件大小:清理后的文件通常比原文件小,因为移除了不需要的事件
- 数据完整性:清理后的文件仍然是有效的 JFR 文件,可以正常使用其他
jfr命令分析 - 敏感信息:可以用于移除包含敏感信息的事件(如环境变量、系统属性等)
8.6. jfr assemble(组装记录)#
jfr assemble 命令用于将 repository 中的 chunk 文件组装成完整的 JFR 记录文件。
语法:
jfr assemble <repository> <file>
参数:
<repository>(必需):包含 chunk 文件的 repository 目录路径<file>(必需):要创建的 JFR 记录文件路径(.jfr扩展名)
工作原理:
- 扫描 repository 目录中的所有
.jfrchunk 文件 - 按文件名排序(保持时间顺序)
- 排除未完成的 chunk 文件(
.part文件) - 按时间顺序连接所有 chunk 文件
- 生成完整的 JFR 记录文件
使用场景:
- JVM 崩溃恢复:如果 JVM 崩溃,repository 中的 chunk 文件可以恢复并组装成完整的记录文件
- 手动组装:从 repository 目录手动创建记录文件
使用示例:
# 从 repository 目录组装记录文件
jfr assemble ./2025_11_19_17_41_00_3833 assembled-recording.jfr
# 从指定路径的 repository 组装
jfr assemble /path/to/repository recording.jfr
注意事项:
- Chunk 文件:只会处理完整的
.jfr文件,.part文件会被排除 - 时间顺序:Chunk 文件按文件名排序,确保时间顺序正确
- 文件完整性:组装后的文件是有效的 JFR 文件,可以正常使用其他
jfr命令分析
8.7. jfr disassemble(拆分记录)#
jfr disassemble 命令用于将 JFR 记录文件拆分成多个较小的文件/chunk。
语法:
jfr disassemble [--output <directory>] [--max-chunks <chunks>] [--max-size <size>] <file>
选项:
--output <directory>:输出目录(默认:当前目录)--max-chunks <chunks>:每个拆分文件的最大 chunk 数量(默认:5)- Chunk 大小因记录而异,通常约为 15 MB
--max-size <size>:每个拆分文件的最大字节数<file>(必需):要拆分的 JFR 记录文件路径
工作原理:
- 读取记录文件,识别所有 chunk
- 根据
--max-chunks或--max-size参数将 chunk 分组 - 为每个分组创建一个新的 JFR 文件
- 文件命名:
<原文件名>_1.jfr、<原文件名>_2.jfr等 - 如果 chunk 数量超过 100,文件名会补零以保持时间顺序(如
myfile_001.jfr)
使用场景:
- 修复损坏文件:通过移除损坏的 chunk 来修复损坏的记录文件
- 减小文件大小:将过大的文件拆分成较小的文件,便于传输
- 分段分析:将大文件拆分成小文件,便于分段分析
使用示例:
# 拆分记录文件(使用默认设置:每个文件最多 5 个 chunk)
jfr disassemble recording.jfr
# 指定输出目录
jfr disassemble --output ./split recording.jfr
# 指定每个文件的最大 chunk 数量
jfr disassemble --max-chunks 10 recording.jfr
# 指定每个文件的最大大小(字节)
jfr disassemble --max-size 50000000 recording.jfr
# 组合使用多个选项
jfr disassemble --output ./split --max-chunks 3 recording.jfr
输出文件命名:
- 如果记录包含 ≤ 100 个 chunk:
recording_1.jfr、recording_2.jfr、… - 如果记录包含 > 100 个 chunk:
recording_001.jfr、recording_002.jfr、…
注意事项:
- 文件完整性:拆分后的每个文件都是有效的 JFR 文件,可以独立分析
- Chunk 边界:拆分在 chunk 边界进行,不会破坏 chunk 的完整性
- 时间顺序:文件名中的数字保持时间顺序
8.8. jfr configure(配置 JFC 文件)#
jfr configure 命令用于创建和编辑 JFC 配置文件。此命令在第 3.5 节中已有详细介绍,这里仅作简要说明。
语法:
jfr configure [--interactive] [--verbose] [--input <files>] [--output <file>] [option=value]* [event-setting=value]*
主要选项:
--interactive:交互式模式--verbose:显示修改的设置--input <files>:输入 JFC 文件(逗号分隔)--output <file>:输出文件名(默认:custom.jfc)option=value:修改 JFC 选项(如gc=high)event-setting=value:修改事件设置(如jdk.ThreadSleep#threshold=50ms)
使用示例:
# 交互式创建配置(需要 --interactive 参数)
jfr configure --interactive --input default.jfc --output custom.jfc
# 命令行创建配置
jfr configure --input default.jfc --output custom.jfc \
jdk.ThreadSleep#threshold=50ms \
jdk.CPULoad#period=2s
详细说明请参考第 3.5 节。
8.9. 完整使用流程示例#
以下是一个完整的使用流程示例:
# 1. 查看记录摘要
jfr summary recording.jfr
# 2. 查看所有可用视图
jfr view all-views recording.jfr
# 3. 查看 GC 视图
jfr view gc recording.jfr
# 4. 查看热点方法
jfr view hot-methods recording.jfr
# 5. 打印特定事件
jfr print --events jdk.ThreadSleep recording.jfr
# 6. 以 JSON 格式导出事件
jfr print --json --events jdk.CPULoad recording.jfr > cpuload.json
# 7. 查看事件元数据
jfr metadata --events jdk.ThreadSleep recording.jfr
# 8. 清理记录(移除敏感信息)
jfr scrub --exclude-events InitialEnvironmentVariable recording.jfr cleaned.jfr
# 9. 从 repository 组装记录
jfr assemble ./repository assembled.jfr
# 10. 拆分大文件
jfr disassemble --max-chunks 5 large-recording.jfr
8.10. 注意事项#
文件路径:文件路径可以是绝对路径或相对路径(相对于当前工作目录)
文件格式:输入文件必须是有效的 JFR 文件(
.jfr扩展名)过滤器语法:
- 支持简单名称:
CPULoad - 支持限定名称:
jdk.CPULoad - 支持 glob 模式:
jdk.*、Thread* - 多个值用逗号分隔:
CPULoad,GarbageCollection - 包含特殊字符时使用引号:
"GC,JVM,Java*"
- 支持简单名称:
输出格式:
jfr print默认输出到标准输出,可以重定向到文件jfr view输出到标准输出jfr scrub、jfr assemble、jfr disassemble会创建新文件
性能考虑:
- 对于大型记录文件,某些操作(如
jfr print、jfr view)可能需要较长时间 - 使用过滤器可以减少处理的数据量,提高性能
- 对于大型记录文件,某些操作(如
文件完整性:
- 如果查看的是 repository 中正在写入的文件,可能会提示文件损坏,这是正常的
- 等待文件写入完成后再查看,或使用
jfr assemble组装完整的文件
帮助信息:使用
jfr help <command>可以查看每个命令的详细帮助信息



