如何以低成本使用Java Flight Recorder (JFR) 捕获生产环境实时性能数据?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1053个文字,预计阅读时间需要5分钟。
JFR系统在生产环境中能够长期稳定运行,关键不在于能不能开得起,而是怎么开才不扰民。默认配置下,性能损耗不明显,interval=1ms和interval=10ms的差异,就体现了服务稳定性的分水岭。
用 jcmd 动态启停,别碰重启
生产环境最怕改 JVM 参数后 reload 或重启。JFR 支持运行时控制,完全绕过这个雷区:
-
jps -l找出目标进程 ID(比如12345) - 启动录制:
jcmd 12345 JFR.start name=prod-6h,settings=profile,filename=/var/log/jfr/prod-$(date +%s).jfr,maxage=6h,maxsize=200M - 临时 dump 当前缓冲数据(不停止录制):
jcmd 12345 JFR.dump name=prod-6h filename=/var/log/jfr/dump-$(date +%s).jfr - 问题复现后停止:
jcmd 12345 JFR.stop name=prod-6h
注意:settings=profile 是生产推荐配置,它禁用低频高开销事件(如类加载细节),只保留 CPU、GC、线程、锁、IO 等核心指标;而 settings=default 会记录更多内容,开销可能升至 3%–5%,慎用。
避免采样频率陷阱:10ms 是甜点,不是底线
CPU 方法采样(jdk.ExecutionSample)是定位热点的主力,但采样太密等于自己制造性能瓶颈:
立即学习“Java免费学习笔记(深入)”;
- 默认
interval=10ms是平衡点:每秒 100 次栈快照,开销可控,精度足够识别 >1% 占比的方法 -
interval=1ms→ 每秒 1000 次采样 → 安全点触发频繁,线程卡顿明显,GC 压力上升 -
interval=100ms→ 每秒仅 10 次 → 很可能漏掉短生命周期热点(比如一次 RPC 中的序列化耗时) - 若需更高精度,优先用
jdk.MethodSample(基于 JIT 编译后方法入口探针),而非暴力缩间隔
动态开启时加 interval=10ms 显式声明,比依赖 profile 默认值更稳妥。
关注三个事件类型,别被 200+ 事件淹没
JFR 内置事件超 200 种,但生产排查只需盯死三类,它们覆盖 90% 性能问题:
-
jdk.GCPhasePause:看pauseDuration字段,单次 >500ms 或总占比 >10% 就要查 GC 配置或对象分配速率 -
jdk.ThreadPark和jdk.JavaMonitorEnter:查parkTime/waitTime,>100ms 且堆栈集中在某把锁(如ConcurrentHashMapresize 或连接池getConnection())就是锁竞争信号 -
jdk.ExecutionSample:按method聚合,看 “Self Time” 占比,>30% 的方法基本就是优化靶心;注意排除Unsafe.park、Object.wait这类阻塞调用
用 jfr print --events "jdk.GCPhasePause,jdk.ThreadPark,jdk.ExecutionSample" --format=json recording.jfr 直接筛出这三类,省去在 JMC 里手动过滤的麻烦。
文件写入路径和生命周期必须由运维接管
很多人忽略这点:JFR 录制文件写到磁盘不是“录完才写”,而是边录边刷环形缓冲区,如果 filename 指向 NFS 或慢盘,I/O 会拖垮整个 JVM:
- 务必写到本地 SSD 分区(如
/var/log/jfr/),禁止写入网络存储或根分区 -
maxsize=200M+maxage=6h是硬约束,防止磁盘打满;建议配合 logrotate 或定时清理脚本 - 不要长期用
duration=0(无限录制),它会让 JFR 持续写入,缓冲区压力不可控;用maxage/maxsize控制滚动才是正解
真正容易被忽略的是:JFR 文件本身是二进制流,没有文件头校验,mv 或 cp 过程中若中断,生成的 .jfr 极大概率损坏——分析前先用 jfr print --events jdk.StartFlightRecording xxx.jfr 快速验证是否可读。
本文共计1053个文字,预计阅读时间需要5分钟。
JFR系统在生产环境中能够长期稳定运行,关键不在于能不能开得起,而是怎么开才不扰民。默认配置下,性能损耗不明显,interval=1ms和interval=10ms的差异,就体现了服务稳定性的分水岭。
用 jcmd 动态启停,别碰重启
生产环境最怕改 JVM 参数后 reload 或重启。JFR 支持运行时控制,完全绕过这个雷区:
-
jps -l找出目标进程 ID(比如12345) - 启动录制:
jcmd 12345 JFR.start name=prod-6h,settings=profile,filename=/var/log/jfr/prod-$(date +%s).jfr,maxage=6h,maxsize=200M - 临时 dump 当前缓冲数据(不停止录制):
jcmd 12345 JFR.dump name=prod-6h filename=/var/log/jfr/dump-$(date +%s).jfr - 问题复现后停止:
jcmd 12345 JFR.stop name=prod-6h
注意:settings=profile 是生产推荐配置,它禁用低频高开销事件(如类加载细节),只保留 CPU、GC、线程、锁、IO 等核心指标;而 settings=default 会记录更多内容,开销可能升至 3%–5%,慎用。
避免采样频率陷阱:10ms 是甜点,不是底线
CPU 方法采样(jdk.ExecutionSample)是定位热点的主力,但采样太密等于自己制造性能瓶颈:
立即学习“Java免费学习笔记(深入)”;
- 默认
interval=10ms是平衡点:每秒 100 次栈快照,开销可控,精度足够识别 >1% 占比的方法 -
interval=1ms→ 每秒 1000 次采样 → 安全点触发频繁,线程卡顿明显,GC 压力上升 -
interval=100ms→ 每秒仅 10 次 → 很可能漏掉短生命周期热点(比如一次 RPC 中的序列化耗时) - 若需更高精度,优先用
jdk.MethodSample(基于 JIT 编译后方法入口探针),而非暴力缩间隔
动态开启时加 interval=10ms 显式声明,比依赖 profile 默认值更稳妥。
关注三个事件类型,别被 200+ 事件淹没
JFR 内置事件超 200 种,但生产排查只需盯死三类,它们覆盖 90% 性能问题:
-
jdk.GCPhasePause:看pauseDuration字段,单次 >500ms 或总占比 >10% 就要查 GC 配置或对象分配速率 -
jdk.ThreadPark和jdk.JavaMonitorEnter:查parkTime/waitTime,>100ms 且堆栈集中在某把锁(如ConcurrentHashMapresize 或连接池getConnection())就是锁竞争信号 -
jdk.ExecutionSample:按method聚合,看 “Self Time” 占比,>30% 的方法基本就是优化靶心;注意排除Unsafe.park、Object.wait这类阻塞调用
用 jfr print --events "jdk.GCPhasePause,jdk.ThreadPark,jdk.ExecutionSample" --format=json recording.jfr 直接筛出这三类,省去在 JMC 里手动过滤的麻烦。
文件写入路径和生命周期必须由运维接管
很多人忽略这点:JFR 录制文件写到磁盘不是“录完才写”,而是边录边刷环形缓冲区,如果 filename 指向 NFS 或慢盘,I/O 会拖垮整个 JVM:
- 务必写到本地 SSD 分区(如
/var/log/jfr/),禁止写入网络存储或根分区 -
maxsize=200M+maxage=6h是硬约束,防止磁盘打满;建议配合 logrotate 或定时清理脚本 - 不要长期用
duration=0(无限录制),它会让 JFR 持续写入,缓冲区压力不可控;用maxage/maxsize控制滚动才是正解
真正容易被忽略的是:JFR 文件本身是二进制流,没有文件头校验,mv 或 cp 过程中若中断,生成的 .jfr 极大概率损坏——分析前先用 jfr print --events jdk.StartFlightRecording xxx.jfr 快速验证是否可读。

