跳过正文
全网最硬核 JDK 解析 - 7. JFR 事件采集原理与演进
  1. 文章/

全网最硬核 JDK 解析 - 7. JFR 事件采集原理与演进

·62525 字·125 分钟
NeatGuyCoding
作者
NeatGuyCoding
目录

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 的核心价值体现在以下几个方面:

  1. 问题发生时的应急处理:当 JVM 进程出现问题时,首要任务是尽快恢复业务(重启实例、下线问题实例、扩容等),而不是立即进行问题分析。此时需要的是能够持续记录、事后分析的工具。
  2. 问题现场的时间窗口:问题出现时,往往已经错过了问题发生的关键时间点。即使通过下线实例来保护现场,使用实时分析工具(如 JVisualVM、Arthas)可能已经无法获取到问题发生时的现场数据。
  3. 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:
      • 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:
      • 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:
      • 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 会自动计算 globalbuffersizenumglobalbuffers 的最优组合
        • 运行时动态修改:不可以在运行时修改。只能在 JFR 初始化前通过 -XX:FlightRecorderOptions 设置。如果 JFR 已初始化,通过 jcmd JFR.configure 修改此选项会被忽略(参数不会被传递到 Java 层)
        • 相关 JBS:
      • 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
      • 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:
      • samplethreads:JDK 11 引入,JDK 19 废弃(但仍可用),默认值:true,作用:Thread sampling enable / disable (only sampling when event enabled and sampling enabled)(线程采样启用/禁用):
      • 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:
      • 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:
      • 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>.jfrhs_oom_pid<pid>.jfrhs_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.cppbefore_exit() 中调用),当 JVM 正常关闭时也会触发
            • 注意:如果 WatcherThread 崩溃,不会生成紧急转储(因为 WatcherThread 是安全网,用于在紧急转储死锁时超时退出)
          • 转储内容
            • Repository 数据:将 repository 目录中的所有 .jfr chunk 文件按时间顺序(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 分析)
          • 文件命名规则
            • 一般错误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:
      • 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。主要用于开发和调试场景,生产环境通常不可用
  • 记录级别配置:控制单个记录的事件配置和存储选项,一个 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:
        • 单个事件的配置:
          • 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.ClassLoadingStatisticsjdk.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 相关值,按以下规则:
                • 同时指定 beginChunkendChunk → 等同于 everyChunk
                • 只指定 beginChunk → 返回 beginChunk
                • 只指定 endChunk → 返回 endChunk
                • 默认返回 everyChunk
            • 示例:
              • jdk.CPULoad#period=1 s 表示每秒采集一次 CPU 负载事件
              • jdk.ClassLoadingStatistics#period=everyChunk 表示每次 chunk 轮转时采集类加载统计信息
            • 运行时动态修改:可以在记录运行时通过 Recording.setSettings() 或 JMX 修改,修改会立即生效
            • 注意事项:
              • period 只对周期性事件有效:只有标注了 @Period 的事件才支持此配置
              • Chunk 轮转频率:everyChunkbeginChunkendChunk 的采集频率取决于 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 字段记录
          • 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.GCPhasePauseLevel1jdk.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:
      • 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:
      • 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:
      • 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:
      • 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:StartFlightRecordingjcmd JFR.start 设置
        • 相关 JBS:
    • 记录级别但影响全局的配置:每个记录可以独立设置,但实际影响是全局的,多个记录共享底层资源
      • 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:
      • 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. 按采集方式分类
#

  1. 同步事件(Synchronous Events)

    • 特点:在业务线程中直接提交,如 jdk.ThreadStartjdk.ThreadEnd
    • 适用配置:enabledstackTracethreshold(如果有持续时间)、throttlefilter(如果是方法相关事件)
  2. 异步事件(Asynchronous Events)

    • 特点:在后台线程中采集,如采样事件
    • 适用配置:enabledstackTracethrottle
  3. 周期性事件(Periodic Events)

    • 特点:按固定间隔采集,如 jdk.CPULoadjdk.ClassLoadingStatistics
    • 适用配置:enabledperiodstackTrace(但是没有意义,因为周期触发堆栈我们不关心)、throttle(也没有意义,周期就能控制频率了)
    • 特殊说明:period 配置只对这类事件有效
  4. 请求式事件(Requestable Events)

    • 特点:按需触发,如 chunk 开始/结束事件
    • 适用配置:enabledstackTrace

1.4.2. 按事件特性分类
#

  1. 持续时间事件(Duration Events)

    • 特点:有明确的开始和结束时间,如 jdk.ThreadSleepjdk.GCPhase
    • 适用配置:enabledthresholdstackTracethrottle
    • 特殊说明:threshold 配置只对这类事件有效
  2. 瞬时事件(Instant Events)

    • 特点:没有持续时间,如 jdk.ThreadStartjdk.ClassLoad,另外上面的周期性事件和请求式事件也属于这里的瞬时事件
    • 适用配置:enabledstackTracethrottle
    • 特殊说明:不支持 threshold 配置
  3. 采样事件(Sampling Events)

    • 特点:通过采样机制采集,如 jdk.ExecutionSamplejdk.ObjectAllocationSample
    • 适用配置:enabledstackTracethrottle
    • 特殊说明:通常使用 throttle 控制采样频率

1.4.3. 按照实现层面分类
#

  1. JDK Java 层面事件(JDK Java-level Events)

    • 特点:事件类定义在 JDK 的 Java 代码中,继承自 AbstractJDKEventjdk.jfr.Event
    • 实现位置
      • 事件类定义:src/jdk.jfr/share/classes/jdk/jfr/events/ 包中
      • 事件触发:在 jdk.jfr.internal.JDKEvents 类中实现,或通过 Java 代码直接调用 Event.commit()
    • 典型事件
      • jdk.ActiveRecording:记录 JFR 记录配置信息
      • jdk.ActiveSetting:记录事件配置设置
      • jdk.ThreadStartjdk.ThreadEnd:线程生命周期事件
      • jdk.ClassLoadjdk.ClassUnload:类加载/卸载事件
      • jdk.FileReadjdk.FileWrite:文件 I/O 事件(通过 JVMTI 插桩)
      • jdk.SocketReadjdk.SocketWrite:网络 I/O 事件(通过 JVMTI 插桩)
    • 实现机制
      • 在 Java 代码的关键位置直接调用 Event.commit() 提交事件
      • 对于 I/O 相关事件,通过 JVMTI 在类加载时或运行时进行字节码插桩,在插桩代码中调用事件提交
      • 事件类通过 JVMTI RetransformClasses 或 Eager Instrumentation 机制进行插桩
    • 优势:实现简单,易于维护和扩展;可以访问 Java 层的丰富信息
    • 适用场景:JDK 库中的事件、应用程序自定义事件
  2. 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.GCConfigurationjdk.GCHeapConfiguration:GC 配置信息
      • JVM 内部钩子事件(JVM Internal Hook Events)
        • 实现方式:在 JVM 的关键执行路径中直接调用 JFR API
        • 典型事件:
          • jdk.GCPhase:GC 阶段事件(在 GC 代码中触发)
          • jdk.GCHeapSummary:GC 堆摘要(在 GC 代码中触发)
          • jdk.ObjectAllocationInNewTLABjdk.ObjectAllocationOutsideTLAB:对象分配事件(在对象分配路径中触发)
          • jdk.MonitorWaitjdk.MonitorWaited:锁等待事件(在同步代码中触发)
          • jdk.ThreadSleepjdk.ThreadPark:线程睡眠/挂起事件(在线程代码中触发)
    • 优势:可以访问 JVM 内部状态,性能开销低(直接调用,无需 JNI 开销)
    • 适用场景:JVM 内部事件、GC 事件、线程事件、对象分配事件等
  3. 基于 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 缓冲区的代码
    • 典型事件
      • jdk.FileReadjdk.FileWrite:文件 I/O 事件(插桩 java.io.FileInputStreamFileOutputStream 等)
      • jdk.SocketReadjdk.SocketWrite:网络 I/O 事件(插桩 java.net.SocketServerSocket 等)
      • jdk.MethodTracejdk.MethodTiming:方法追踪/计时事件(JDK 25 引入,通过方法过滤器插桩)
    • 优势:可以监控 JDK 库和应用程序代码的执行,无需修改源代码
    • 适用场景:I/O 事件、方法追踪事件、需要监控应用程序代码的事件
  4. 基于采样机制的事件(Sampling-based Events)

    • 特点:通过独立的采样线程定期采样目标线程的状态,生成事件
    • 实现位置
      • 采样器实现:src/hotspot/share/jfr/recorder/checkpoint/types/jfrThreadSampler.cpp
      • 采样线程:JfrThreadSampler 线程
    • 实现机制
      • 采样线程定期唤醒(默认间隔由 samplethreads 配置控制)
      • 遍历所有 Java 线程,获取线程状态和堆栈跟踪
      • 根据事件配置决定是否生成事件
    • 典型事件
      • jdk.ExecutionSample:方法执行采样事件(采样线程的堆栈跟踪)
      • jdk.NativeMethodSample:本地方法采样事件(采样本地方法调用)
    • 优势:开销低,不需要在业务代码中插桩,对性能影响小
    • 适用场景:需要周期性采样线程状态的事件,如性能分析事件
  5. 混合实现事件(Hybrid Implementation Events)

    • 特点:结合多种实现机制的事件
    • 典型事件
      • jdk.ObjectAllocationSample
        • 分配路径触发:在对象分配路径中通过 C++ 代码触发(类似 jdk.ObjectAllocationInNewTLAB
        • 采样机制:通过采样机制控制采集频率(通过 throttle 配置)
      • 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 参数:

  1. ``-XX:+FlightRecorder`:JDK 11 引入,默认值:false,这个配置已过期,并且虽然默认值是 false,但是如果没有设置,JVM 还是将其设置为 true,即默认启用 JFR 功能,只有在显式设置为 false 时才会禁用 JFR 功能
  2. -XX:FlightRecorderOptions:配置 JFR 的全局选项
  3. -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

注意事项

  1. 格式要求:选项之间使用逗号(,)分隔,选项名和值之间使用等号(=)分隔
  2. 大小单位:内存大小支持 k/K(KB)、m/M(MB)、g/G(GB)
  3. 路径格式:路径可以是绝对路径或相对路径(相对于当前工作目录)
  4. 运行时修改:部分选项(如 repositorymaxchunksize)可以通过 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=MyRecordingname=Production-Monitoring

settings=<jfc-files>(可选):

  • 作用:指定 JFC 配置文件(详见第 5 章)
  • 默认值:default.jfc
  • 格式:
    • 单个文件:settings=defaultsettings=profilesettings=/path/to/custom.jfc
    • 多个文件(合并):settings=default,settings=profile(用逗号分隔,可重复指定)
    • 不使用配置:settings=none
  • 示例:
    • settings=default
    • settings=profile
    • settings=default,settings=/path/to/custom.jfc
    • settings=none

disk=<true|false>(可选):

  • 作用:控制记录是否写入磁盘
  • 默认值:true
  • 说明:
    • true:数据持续写入磁盘 repository,支持 maxagemaxsize 限制
    • false:数据仅存储在内存中,适合短期记录
  • 示例:disk=truedisk=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=5sflush-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.jfr
    • filename=./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(无限制,但如果 durationmaxagemaxsize 都未指定,默认使用 250MB
  • 格式:整数后跟单位(k/K=KB、m/M=MB、g/G=GB)
  • 限制:值不能小于全局配置中的 maxchunksize
  • 示例:maxsize=500Mmaxsize=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=highmethod-profiling=high
  • 示例:gc=highmethod-profiling=high

事件设置<event-name>#<setting-name>=<value>):

  • 格式:<event-name>#<setting-name>=<value>
  • 说明:直接修改事件的配置项
  • 添加新事件:使用 + 前缀,如 +jdk.CustomEvent#enabled=true
  • 示例:
    • jdk.ThreadSleep#threshold=50ms
    • jdk.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.startJFR.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 退出时是否 dump
  • path-to-gc-roots=<true|false>:是否记录 GC 根
  • report-on-exit=<view-names>:JVM 退出时生成报告视图(JDK 25+,仅在 disk=true 时有效)
  • JFC 选项:<option>=<value>(如 gc=highmethod-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

注意事项

  1. 参数分隔符:参数之间使用空格分隔(不同于 JVM 参数使用逗号)
  2. 参数格式:参数名和值之间使用等号(=)分隔
  3. 多次启动:可以多次执行 JFR.start 启动多个独立的记录
  4. 记录 ID:启动成功后会显示记录 ID,用于后续管理

4.2. JFR.stop(停止记录)
#

JFR.stop 命令用于停止一个正在运行的 JFR 记录。

语法

jcmd <pid> JFR.stop name=<name> [filename=<path>]

参数

  • name=<name>(必需):记录的标识名称或 ID
  • filename=<path>(可选):停止时转储的文件路径
    • 如果指定了 filename,会覆盖启动时设置的转储路径,记录停止后转储到新指定的文件
    • 如果未指定 filename
      • 如果启动时指定了 filename(文件路径或目录路径),记录停止后会自动转储到启动时指定的位置
      • 如果启动时没有指定 filename,记录数据会被丢弃
    • 注意filename 必须指定完整的文件路径,不支持目录路径(与 JFR.startJFR.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

注意事项

  1. 记录标识:可以使用记录名称或记录 ID(数字)来标识记录
  2. 数据保存行为
    • 如果 JFR.stop 时指定了 filename,会覆盖启动时设置的转储路径,转储到新指定的文件
    • 如果 JFR.stop 时未指定 filename,但启动时指定了 filename,会自动转储到启动时指定的位置(文件路径或目录路径)
    • 如果启动时和停止时都没有指定 filename,记录数据会被丢弃,无法恢复
  3. 文件路径限制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 小时的数据)
    • 注意:不能与 beginend 同时使用
  • maxsize=<size>(可选):转储的大小限制
    • 格式:整数后跟单位(k/K=KB、m/M=MB、g/G=GB)
    • 示例:maxsize=500M
  • begin=<time>(可选):开始时间
    • 格式支持:
      • ISO 8601 格式:2020-03-17T09:00:002020-03-17T09:00:00Z
      • 本地时间:13:20:152020-03-17T09:00:00
      • 相对时间:-12h(12 小时前)、-15m(15 分钟前)、-30s(30 秒前)
    • 注意:不能与 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/

时间格式说明

  1. ISO 8601 格式

    • 2020-03-17T09:00:00(本地时间)
    • 2020-03-17T09:00:00Z(UTC 时间)
  2. 本地时间格式

    • 13:20:15(今天的时间)
    • 2020-03-17T09:00:00(指定日期时间)
  3. 相对时间格式

    • -12h:12 小时前
    • -15m:15 分钟前
    • -30s:30 秒前
    • -1d:1 天前

注意事项

  1. 记录继续运行JFR.dump 不会停止记录,记录会继续运行
  2. 时间范围maxagebegin/end 不能同时使用
  3. 数据过滤:转储的数据会根据 maxagemaxsizebeginend 进行过滤
  4. 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 时,会显示每个事件的详细配置(enabledthresholdperiod 等)

使用示例

# 查看所有记录的基本信息
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]

注意事项

  1. 记录状态:输出会显示记录的状态(runningstopped 等)
  2. 记录信息:包括记录 ID、名称、持续时间、最大大小、最大保留时间等
  3. 事件配置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

注意事项

  1. 初始化限制:部分选项(如 stackdepthmemorysizemaxchunksize)只能在 JFR 初始化前修改,如果 JFR 已经初始化,这些选项无法修改
  2. 运行时修改repositorypathdumppathpreserve-repository 可以在运行时修改
  3. 遗留选项globalbuffercountglobalbuffersize 是遗留选项,建议使用 memorysize 统一调整
  4. 查看配置:不指定任何参数时,会显示当前配置
  5. 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 内部使用 cleanupDirectoriesSet<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=falsecleanupDirectories 中的所有目录都会被清理(Repository.clear() 方法,第173-180行)
      • 注意cleanupDirectories 是 JFR 内部自动管理的机制,用户无法直接添加目录到该集合

4.6. JFR.view(查看记录)
#

JFR.view 命令用于以预定义的视图格式显示 JFR 记录数据,无需转储文件

语法

jcmd <pid> JFR.view <view> [options]

参数

  • <view>(必需):视图名称或事件类型名称
    • 预定义视图gchot-methodsallocation-by-classcontention-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.GarbageCollectionjdk.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.jarjmods/jdk.jfr.jmod 中),作为资源文件存在
  • 运行时访问:通过 ViewFile.class.getResourceAsStream("/jdk/jfr/internal/query/view.ini") 在运行时读取
  • 文件格式:INI 格式配置文件,包含视图定义和查询语句
  • 用户访问:用户可以通过解压 jdk.jfr 模块的 JAR 文件来查看 view.ini 文件内容,但不建议直接修改

视图结构

每个视图定义包含以下部分:

  • 视图名称:格式为 <category>.<name>,例如 jvm.gcapplication.hot-methods
  • 标签(label):视图的显示名称
  • 查询类型
    • table:表格视图,用于显示多行数据,支持排序、分组、聚合
    • form:表单视图,用于显示单行汇总数据,通常使用 LAST() 函数获取最新值

查询语言语法

视图使用类似 SQL 的查询语言,支持以下语法:

  • COLUMN:定义列标题
  • FORMAT:定义列格式(nonenormalizedcell-heighttruncate-beginning/endmissing: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:垃圾回收统计
    • 事件GarbageCollectionGCHeapSummary
    • 设计:表格视图,显示每次 GC 的开始时间、GC ID、GC 名称、GC 前后堆使用情况、最长暂停时间
    • 聚合:按 gcId 分组,关联 GC 前后的堆摘要数据
  • gc-pauses:GC 暂停统计
    • 事件GCPhasePause
    • 设计:表单视图,显示总暂停时间、暂停次数、最小/中位数/平均/P90/P95/P99/P99.9/最大暂停时间
    • 聚合:使用 SUMCOUNTMINMEDIANAVGP90/P95/P99/P999MAX 函数
  • gc-configuration:GC 配置信息
    • 事件GCConfiguration
    • 设计:表单视图,显示年轻代/老年代收集器、GC 线程数、显式 GC 设置等
    • 聚合:使用 LAST() 获取最新配置值
  • gc-parallel-phases:并行 GC 阶段
    • 事件GCPhaseParallel
    • 设计:表格视图,按阶段名称分组,显示平均、P95、最长、计数、总时间
  • gc-concurrent-phases:并发 GC 阶段
    • 事件GCPhaseConcurrentGCPhaseConcurrentLevel1
    • 设计:表格视图,按阶段名称分组,显示平均、P95、最长、计数、总时间
  • gc-pause-phases:GC 暂停阶段
    • 事件GCPhasePauseGCPhasePauseLevel1-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:安全点
    • 事件SafepointBeginSafepointEndSafepointStateSynchronization
    • 设计:表格视图,按安全点 ID 分组,显示开始时间、持续时间、状态同步时间、JNI 关键线程数、总线程数
  • vm-operations:VM 操作
    • 事件jdk.ExecuteVMOperation
    • 设计:表格视图,按操作类型分组,显示平均持续时间、最长持续时间、次数、总持续时间
  • deoptimizations-by-reason:按原因的反优化
    • 事件Deoptimization
    • 设计:表格视图,按反优化原因分组,显示次数
  • deoptimizations-by-site:按位置的反优化
    • 事件Deoptimization
    • 设计:表格视图,按方法、行号、BCI 分组,显示次数
  • class-modifications:类修改
    • 事件RetransformClassesRedefineClasses
    • 设计:表格视图,按重定义 ID 分组,显示持续时间、请求者(应用方法)、操作类型、类数量
    • 堆栈:使用 stackTrace.topApplicationFrame 获取请求者
  • blocked-by-system-gc:被 System.gc() 阻塞
    • 事件SystemGC
    • 设计:表格视图,过滤非并发调用,显示开始时间、持续时间、堆栈跟踪
  • native-memory-committed:已提交的本地内存
    • 事件NativeMemoryUsage
    • 设计:表格视图,按内存类型分组,显示首次观察值、平均值、最后观察值、最大值
  • native-memory-reserved:保留的本地内存
    • 事件NativeMemoryUsage
    • 设计:表格视图,按内存类型分组,显示首次观察值、平均值、最后观察值、最大值
  • tlabs:线程本地分配缓冲区
    • 事件ObjectAllocationInNewTLABObjectAllocationOutsideTLAB
    • 设计:表单视图,显示 TLAB 内外的分配统计(计数、最小/平均/最大大小、总分配)

应用视图application.*):

  • hot-methods:热点方法
    • 事件ExecutionSample
    • 设计:表格视图,按堆栈顶部帧分组,显示方法、样本数、百分比,限制前 25 个
    • 聚合:使用 COUNT(*) 计数,normalized 格式显示百分比
    • 堆栈:使用 stackTrace.topFrame 获取最外层方法
  • cpu-time-hot-methods:CPU 时间采样热点方法
    • 事件CPUTimeSample
    • 设计:表格视图,按堆栈顶部帧分组,显示方法、样本数、百分比,限制前 25 个
    • 聚合:使用 COUNT(*) 计数,normalized 格式显示百分比
  • cpu-time-statistics:CPU 时间采样统计
    • 事件CPUTimeSampleCPUTimeSamplesLost
    • 设计:表单视图,显示成功样本、失败样本、有偏样本、总样本、丢失样本数
    • 过滤:使用 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:按类型异常统计
    • 事件JavaErrorThrowJavaExceptionThrow
    • 设计:表格视图,按抛出类分组,显示类、次数
  • exception-by-message:按消息异常统计
    • 事件JavaErrorThrowJavaExceptionThrow
    • 设计:表格视图,按消息分组,显示消息、次数
  • exception-by-site:按位置异常统计
    • 事件JavaErrorThrowJavaExceptionThrow
    • 设计:表格视图,按堆栈顶部非初始化帧分组,显示方法、次数
    • 堆栈:使用 stackTrace.topNotInitFrame 排除 <init> 方法
  • exception-count:异常统计
    • 事件ExceptionStatistics
    • 设计:表单视图,显示抛出的异常总数(使用 DIFF(throwables) 计算差值)
  • thread-allocation:线程分配统计
    • 事件ThreadAllocationStatistics
    • 设计:表格视图,按线程分组,显示线程、已分配、百分比
    • 聚合:使用 LAST() 获取最新值,normalized 格式显示百分比
  • thread-cpu-load:线程 CPU 负载
    • 事件ThreadCPULoad
    • 设计:表格视图,按事件线程分组,显示线程、系统 CPU、用户 CPU
  • thread-start:平台线程启动
    • 事件ThreadStartThreadEnd
    • 设计:表格视图,按事件线程分组,显示开始时间、堆栈跟踪、线程、持续时间
    • 聚合:使用 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:按类型延迟统计
    • 事件JavaMonitorWaitJavaMonitorEnterThreadParkThreadSleepSocketReadSocketWriteFileWriteFileRead
    • 设计:表格视图,按事件类型分组,显示事件类型、次数、平均、P99、最长、总持续时间
    • 多事件:使用多个事件类型的并集
  • object-statistics:对象统计(占用超过 1%)
    • 事件ObjectCountAfterGCObjectCount
    • 设计:表格视图,按对象类分组,显示类、计数、堆空间、增长量
    • 聚合:使用 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-beginningcell-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:系统信息
    • 事件CPUInformationPhysicalMemoryOSInformationVirtualizationInformation
    • 设计:表单视图,显示物理内存大小、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:本地库加载/卸载失败
    • 事件NativeLibraryLoadNativeLibraryUnload
    • 设计:表格视图,过滤失败操作,显示操作类型、库名称、错误消息
    • 过滤WHERE success = 'false'
  • jvm-flags:JVM 标志
    • 事件IntFlagUnsignedIntFlagBooleanFlagLongFlagUnsignedLongFlagDoubleFlagStringFlag 及其变更事件
    • 设计:表格视图,按名称分组,显示名称、最后值
    • 多事件:使用多个标志事件类型的并集
  • jvm-information:JVM 信息
    • 事件JVMInformation
    • 设计:表单视图,显示 PID、VM 启动时间、名称、版本、VM 参数、程序参数
  • jdk-agents:JDK 代理
    • 事件JavaAgentNativeAgent
    • 设计:表格视图,显示初始化时间、初始化持续时间、名称、选项
    • 格式truncate-beginningcell-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 *
    • 设计:表格视图,按事件类型标签分组,显示事件类型、计数,按名称升序排序

视图设计特点

  1. 分类组织:视图按功能分类(JVM、应用、环境),便于查找和使用
  2. 查询优化:使用 LIMIT 限制结果数量,避免输出过多数据
  3. 格式化选项:使用 FORMAT 控制显示格式,提高可读性
  4. 多事件支持:支持从多个事件类型查询数据(使用并集)
  5. 堆栈跟踪访问:提供多种堆栈帧访问方式,适应不同分析场景
  6. 聚合函数丰富:支持统计、百分位、集合等多种聚合函数
  7. 实时查询:直接从 repository 读取数据,无需转储文件

注意事项

  1. 实时查看JFR.view 直接从 repository 读取数据,无需转储文件
  2. 内存泄漏视图:使用 memory-leaks 视图时,会自动触发 OldObjectSample 事件收集(调用 OldObjectSample.emit(0) 并等待刷新)
  3. 时间范围:默认查看最近 10 分钟的数据,可以通过 maxage 调整
  4. 大小限制:默认最多查看 32MB 的数据,可以通过 maxsize 调整
  5. 查询执行:视图查询通过 QueryExecutor 执行,使用 EventStream 读取事件数据
  6. 性能考虑:复杂查询(如 all-views)可能消耗较多时间和内存
  7. 事件缺失:如果记录中缺少视图所需的事件,会显示错误信息但不会中断执行
  8. 视图名称格式:视图在 view.ini 中定义时使用完整名称(如 [jvm.gc][application.hot-methods]),但在 JFR.view 命令中只需要使用视图名称部分(如 gchot-methods),不需要加类别前缀jvm.application.environment.)。视图名称匹配是大小写不敏感的。

自定义查询与查询语言

JFR.view 命令只能使用预定义的视图,不支持直接执行 SQL 查询。

查询语言的使用场景

  1. 预定义视图JFR.view):使用 view.ini 中预定义的视图,通过视图名称调用
  2. 视图定义view.ini):在配置文件中定义视图,供 JFR.view 使用
  3. 自定义查询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 的作用机制
#

  1. UI 控件定义:为 JMC 和 jfr configure 工具提供配置界面元素
  2. 配置关联:通过 <setting> 元素的 control 属性,将事件配置关联到控件
  3. 自动更新:当控件值改变时,所有关联的 setting 值会自动更新
  4. 条件逻辑:通过 <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 (::&lt;clinit&gt;). Use &lt;init&gt; 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 的工作流程如下:

  1. 解析阶段addControls()):

    • 解析所有 <control> 元素及其子元素(<text><selection><flag><condition>
    • 将控件按名称索引到 controls Map 中
  2. 关联阶段wireSettings()):

    • 遍历所有事件的 setting
    • 如果 setting 有 control 属性,查找对应的控件
    • 建立监听关系:当控件值改变时,自动更新关联的 setting 值
  3. 条件计算阶段wireConditions()):

    • 解析所有 <condition> 元素的表达式
    • 建立依赖关系:当被测试的控件值改变时,重新计算条件值
    • 条件值计算后,自动更新关联的 setting 值
  4. 运行时

    • JVM 不读取 control 部分,只读取最终的 setting 值
    • Control 仅用于配置工具(JMC、jfr configure)提供友好的配置界面

5.2.5. Control 的优势
#

  1. 简化配置:通过高级控件(如"Method Profiling"级别)自动设置多个相关配置
  2. 一致性保证:相关事件的配置通过同一个控件统一管理,确保配置一致
  3. 用户友好:在 JMC 中提供图形化配置界面,无需手动编辑 XML
  4. 条件逻辑:通过 <condition> 实现复杂的配置依赖关系

5.2.6. 注意事项
#

  1. JVM 不读取:Control 元素对 JVM 运行时没有影响,JVM 只读取最终的 setting 值
  2. 唯一性要求:多个 JFC 文件合并时,control 名称不能重复(会抛出异常)
  3. 引用完整性:如果 setting 的 control 属性引用了不存在的控件,会记录警告但不会报错
  4. 手动编辑:如果手动编辑 JFC 文件,需要确保 control 名称和 setting 的 control 属性匹配

5.4. 预定义的 JFC 配置文件
#

JDK 提供了两个预定义的 JFC 配置文件,位于 JAVA_HOME/lib/jfr/ 目录:

  1. default.jfc(默认配置):

    • 标签"Continuous"
    • 描述"Low overhead configuration safe for continuous use in production environments, typically less than 1 % overhead."
    • 特点:低开销配置,适合生产环境持续使用,开销通常小于 1%
    • 适用场景:生产环境持续监控
  2. 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 选项。要列出所有可用的预定义配置,可以通过以下方式:

  1. 直接查看目录JAVA_HOME/lib/jfr/ 目录中通常包含 default.jfcprofile.jfc
  2. 使用 JDK API:通过 Configuration.getConfigurations() 方法获取配置列表(需要编写 Java 代码)
  3. 查看帮助信息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)

交互式模式的工作方式

  1. 启动向导:显示欢迎信息,告知将询问多少个问题
  2. 遍历输入项:对于每个 <control> 中的输入项(<selection><text><flag>),依次询问:
    • <selection>:显示选项列表(1, 2, 3…),用户输入数字选择,或按 ENTER 使用默认值
    • <text>:显示标签和默认值,用户输入文本,或按 ENTER 使用默认值
    • <flag>:显示 Y/N 选择,用户输入 Y 或 N,或按 ENTER 使用默认值
  3. 输入验证:对于时间跨度(timespan)和方法过滤器(method-filter)类型,会进行格式验证
  4. 退出方式:输入 Q 可随时退出向导
  5. 保存文件:最后询问输出文件名(默认 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=highmethod-profiling=high
  • 事件设置格式<事件名称>#<配置项名称>=<配置值>(如 jdk.ThreadSleep#threshold=50msjdk.CPULoad#period=2s
  • 添加新事件:使用 + 前缀,如 +jdk.CustomEvent#enabled=true
  • 时间跨度格式:支持 20ms1s5m 等格式,空格可省略

注意事项

  1. 交互式模式的前提:JFC 文件中必须包含 <control> 元素定义的输入项,否则交互式模式不会询问任何问题
  2. 默认 JFC 文件:如果不指定 --input,默认使用 default.jfc
  3. 空配置:可以使用 --input none 从空配置开始创建
  4. 显示修改:使用 --verbose 参数可以显示所有被修改的设置
  5. 多个 JFC 文件合并的限制
    • jfr configure 命令的限制:不能合并包含相同 control 名称的 JFC 文件。例如,default.jfcprofile.jfc 都定义了 gcmethod-profiling 等 control,尝试使用 jfr configure --input default.jfc,profile.jfc 会报错:Control with 'gc' is declared in multiple files
    • 正确的做法:只使用一个基础 JFC 文件(如 default.jfcprofile.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.xsd Schema
  • 建议使用支持 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 名称(如 gcmethod-profiling 等),jfr configure 命令会抛出异常:Control with 'gc' is declared in multiple files
  • 实际影响:由于 default.jfcprofile.jfc 都定义了相同的 control 名称(如 gcmethod-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 按以下顺序查找:

  1. 预定义配置名称:如果名称是 "default""profile",从 JAVA_HOME/lib/jfr/ 目录加载
  2. JAVA_HOME/lib/jfr/ 目录:如果文件在 JAVA_HOME/lib/jfr/ 目录中,可以直接使用文件名(不需要完整路径)
  3. 完整路径:如果提供了完整路径,直接使用该路径
  4. 相对路径:如果提供了相对路径,相对于当前工作目录

示例

# 以下方式等价(如果 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. 最佳实践
#

  1. 生产环境

    • 使用 default.jfc 或基于它创建的低开销配置
    • 禁用高开销事件(如 ExecutionSampleObjectAllocationSample
    • 提高事件阈值,减少事件数量
    • 禁用不必要的堆栈跟踪
  2. 问题诊断

    • 使用 profile.jfc 或基于它创建的配置
    • 启用相关事件和堆栈跟踪
    • 根据需要调整采样频率和阈值
  3. 配置管理

    • 将自定义 JFC 文件纳入版本控制
    • 为不同场景创建不同的配置文件
    • 使用 jfr configure 命令创建和修改配置,避免手动编辑 XML
  4. 配置验证

    • 使用 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. 注意事项
#

  1. 资源管理RecordingRecordingFile 实现了 Closeable 接口,应该使用 try-with-resources 语句确保资源被正确释放

  2. 记录状态:在调用方法前检查记录状态,某些操作只能在特定状态下执行

  3. 性能开销:虽然 JFR 开销很低,但在高频率路径中创建事件对象仍会有开销,使用 shouldCommit() 可以优化性能

  4. 事件字段类型:事件字段只支持特定类型(基本类型、String、Thread、Class),其他类型会被忽略

  5. 线程安全Recording 类的方法不是线程安全的,如果从多个线程访问,需要同步

  6. 流式处理RecordingStream 适合实时监控场景,但要注意设置 maxAgemaxSize 以避免内存占用过大

7. 通过 JMX 使用 JFR
#

JMX(Java Management Extensions)是 Java 平台提供的管理和监控框架。JFR 通过 FlightRecorderMXBean 接口暴露了完整的 JMX 管理接口,允许通过 JMX 远程或本地管理 JFR 记录。

ObjectNamejdk.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:记录 ID
  • name:记录名称
  • state:记录状态(NEWDELAYEDRUNNINGSTOPPEDCLOSED
  • 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:事件类型 ID
  • name:事件名称(如 "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. 注意事项
#

  1. 远程文件路径:通过远程 JMX 调用 copyTo() 时,文件会写入到目标 JVM 运行的机器上,而不是客户端机器。

  2. 流版本:要从正在运行的记录读取数据,必须设置 streamVersion="1.0"

  3. 资源管理:使用完流后,应该调用 closeStream() 释放资源。

  4. 通知监听器:如果不再需要监听通知,应该移除监听器以避免内存泄漏。

  5. 异常处理:JMX 操作可能抛出 IOExceptionIllegalArgumentExceptionIllegalStateException 等异常,需要适当处理。

  6. 连接管理:使用远程 JMX 时,需要管理 JMXConnector 的生命周期,使用完后应该关闭连接。

  7. 权限要求:远程 JMX 连接可能需要配置认证和 SSL,具体取决于目标 JVM 的配置。

  8. RemoteRecordingStreamRemoteRecordingStream 实现了 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>(必需):视图名称或事件类型名称
    • 预定义视图:gchot-methodsallocation-by-classcontention-by-site
    • 事件类型:jdk.GarbageCollectionjdk.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

注意事项

  1. 视图查询:每个视图都基于一个查询语句,可以通过 --verbose 选项查看
  2. 事件类型:可以直接使用事件类型名称作为视图,会显示该事件类型的所有事件
  3. 性能:对于大型记录文件,某些视图可能需要较长时间处理

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
 ...

注意事项

  1. 文件完整性:如果查看的是 repository 中的最后一个文件,可能会提示文件损坏(因为元数据还没有 flush),这是正常的,默认 1 秒执行一次 flush,多试几次就能看到正确的结果
  2. 排序:事件统计表按事件数量降序排序

8.4. jfr metadata(元数据信息)
#

jfr metadata 命令用于显示事件元数据信息,如事件名称、分类和字段布局。

语法

jfr metadata [--categories <filter>] [--events <filter>] [<file>]

选项

  • --categories <filter>:选择匹配分类名称的事件
  • --events <filter>:选择匹配事件名称的事件
  • <file>(可选):JFR 记录文件路径
    • 如果省略,使用运行 jfr 工具的 JDK 中的元数据

输出内容

  • 事件类型信息:
    • 事件名称、标签、描述
    • 分类名称
    • 字段信息(字段名、类型、标签、描述)
    • 设置描述符(如 enabledthresholdperiod 等)

使用示例

# 显示所有事件元数据(使用 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

注意事项

  1. 元数据来源:如果不指定文件,使用运行 jfr 工具的 JDK 中的元数据;如果指定文件,使用文件中的元数据
  2. 字段布局:元数据包含事件的完整字段布局信息,有助于理解事件结构

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 模式
  • 如果使用多个过滤器,会按指定顺序应用
  • includeexclude 可以组合使用

使用示例

# 只保留套接字相关事件
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

输出信息

命令执行后会显示:

  • 清理后的文件路径
  • 移除的事件统计(事件名称和移除的百分比)

注意事项

  1. 文件大小:清理后的文件通常比原文件小,因为移除了不需要的事件
  2. 数据完整性:清理后的文件仍然是有效的 JFR 文件,可以正常使用其他 jfr 命令分析
  3. 敏感信息:可以用于移除包含敏感信息的事件(如环境变量、系统属性等)

8.6. jfr assemble(组装记录)
#

jfr assemble 命令用于将 repository 中的 chunk 文件组装成完整的 JFR 记录文件。

语法

jfr assemble <repository> <file>

参数

  • <repository>(必需):包含 chunk 文件的 repository 目录路径
  • <file>(必需):要创建的 JFR 记录文件路径(.jfr 扩展名)

工作原理

  1. 扫描 repository 目录中的所有 .jfr chunk 文件
  2. 按文件名排序(保持时间顺序)
  3. 排除未完成的 chunk 文件(.part 文件)
  4. 按时间顺序连接所有 chunk 文件
  5. 生成完整的 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

注意事项

  1. Chunk 文件:只会处理完整的 .jfr 文件,.part 文件会被排除
  2. 时间顺序:Chunk 文件按文件名排序,确保时间顺序正确
  3. 文件完整性:组装后的文件是有效的 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 记录文件路径

工作原理

  1. 读取记录文件,识别所有 chunk
  2. 根据 --max-chunks--max-size 参数将 chunk 分组
  3. 为每个分组创建一个新的 JFR 文件
  4. 文件命名:<原文件名>_1.jfr<原文件名>_2.jfr
  5. 如果 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.jfrrecording_2.jfr、…
  • 如果记录包含 > 100 个 chunk:recording_001.jfrrecording_002.jfr、…

注意事项

  1. 文件完整性:拆分后的每个文件都是有效的 JFR 文件,可以独立分析
  2. Chunk 边界:拆分在 chunk 边界进行,不会破坏 chunk 的完整性
  3. 时间顺序:文件名中的数字保持时间顺序

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. 注意事项
#

  1. 文件路径:文件路径可以是绝对路径或相对路径(相对于当前工作目录)

  2. 文件格式:输入文件必须是有效的 JFR 文件(.jfr 扩展名)

  3. 过滤器语法

    • 支持简单名称:CPULoad
    • 支持限定名称:jdk.CPULoad
    • 支持 glob 模式:jdk.*Thread*
    • 多个值用逗号分隔:CPULoad,GarbageCollection
    • 包含特殊字符时使用引号:"GC,JVM,Java*"
  4. 输出格式

    • jfr print 默认输出到标准输出,可以重定向到文件
    • jfr view 输出到标准输出
    • jfr scrubjfr assemblejfr disassemble 会创建新文件
  5. 性能考虑

    • 对于大型记录文件,某些操作(如 jfr printjfr view)可能需要较长时间
    • 使用过滤器可以减少处理的数据量,提高性能
  6. 文件完整性

    • 如果查看的是 repository 中正在写入的文件,可能会提示文件损坏,这是正常的
    • 等待文件写入完成后再查看,或使用 jfr assemble 组装完整的文件
  7. 帮助信息:使用 jfr help <command> 可以查看每个命令的详细帮助信息

相关文章

使用 JFR 排查 SSL 性能瓶颈

·868 字·2 分钟
深入分析微服务性能问题,包括 CPU 峰值和数据库连接异常。通过 JFR 分析,我们发现根本原因是 Java SecureRandom 在 /dev/random 上阻塞,并提供使用 /dev/urandom 的解决方案。