2901 字
约 9 分钟
17
S05-X25 · JVM 商用分析工具:JProfiler、JRockit 与 JMC
2024-12-31
2026-11-09

接口平时 40ms、大促时 900ms,jstack 连抓三次都停在业务代码里,jcmd GC.heap_info 也没看到老年代异常。JDK 自带工具能回答"谁在等",回答不了"谁被调了一万次、每次花了多久"。带计数和调用关系的证据只有分析器能给,而分析器按实现原理分成两类——选错一类,排查就会变成一次人为事故。

按"原理 → 能力面 → 接入方式 → 生产边界"过一遍:JProfiler、JRockit(历史)、JMC + JFR,以及它们与 async-profiler、MAT 的分工。


1. 采样与插桩:先分清这两类

维度 采样型 插桩型
原理 周期性向 JVM 要线程栈快照,按栈帧累计 改写字节码,在方法进出处记时间戳与计数
覆盖 只统计"采到时正在执行"的方法,短方法会漏 每次调用都记,无遗漏
开销量级 典型量级 1%–3% 典型量级数倍,按工具与场景核对
对被测程序的影响 时钟偏差小,JIT 基本不受干扰 破坏内联、触发 deoptimize、可能放大锁竞争
会不会改变结论 影响小 会——把"插桩后变慢"当成"原本就慢"
用在哪 线上、长时间开 离线复现、需精确次数

判断规则一条:采样回答"哪里热",插桩回答"被调了多少次"。线上要前者,后者留给能接受性能损失的环境。


2. JProfiler 的能力面

视图 数据来源 能回答的问题 前提
CPU 调用树 采样或插桩 谁占 CPU、谁调用了我 采样无需改代码
分配热点 分配点记录 哪个类、哪个调用点分配最多 额外吃内存
对象存活/引用链 堆遍历 谁抓着对象不放 会触发一次 GC
线程与锁 状态采样 + 监视器事件 锁竞争、死锁、等待链 监视器事件需插桩
JDBC 探针 驱动代理或字节码增强 哪条 SQL 慢、有无 N+1 需匹配驱动版本
HTTP 探针 servlet filter 或类增强 哪个 URL 慢 容器需支持异步

2.1 本地 attach:不改参数、不重启

# GUI:Session -> New Session -> 选择进程
# 旧版本随工具提供命令行包装(按你手上的版本核对是否存在)
$ jpenable --pid 5123 --gui

本地 attach 走 Attach API,要求同用户、同位数,且目标 JVM 未被 -XX:+DisableAttachMechanism 关掉。

2.2 远程 agent:需要重启

java -agentpath:/opt/jprofiler/bin/linux-x64/libjprofilerti.so=port=8849,nowait \
     -jar app.jar
参数 作用 不写会怎样
port agent 监听端口,GUI 反向连接 用默认端口,易撞车
nowait agent 不等 GUI,程序继续启动 阻塞等到 GUI 连上来
id 会话标识,用于离线快照命名 快照只靠时间戳区分
config 指定配置文件 抓什么不由你定
offline 进入离线模式 只能在线分析

2.3 离线模式:把现场存下来再看

$ java -agentpath:/opt/jprofiler/bin/linux-x64/libjprofilerti.so=offline,id=187,config=/opt/jprofiler/config.xml \
       -jar app.jar
$ ls -l /opt/jprofiler/bin/linux-x64/*.jps   # 快照,用 GUI 打开

取舍很明确:config.xml 决定抓什么,没配的东西事后补不回来。触发条件(CPU 超阈值才开始记、或固定时长)必须提前配好,否则拿到的是"什么都没发生的快照"。


3. JRockit 的历史位置

时间 事件 对今天的影响
2002 前后 BEA 推出 JRockit,面向服务端 其 JRA 记录思路演化为 JFR
2008 Oracle 收购 BEA JRockit 与 HotSpot 并行维护过一段
JDK 7 起 Mission Control 与飞行记录移植进 HotSpot JMC 出现在 JDK 7 更新版中
JDK 8 之后 JRockit 不再作为独立发行版演进 只剩老系统还在跑

它已经不是选项目。真遇到时:

# jrcmd 是 jcmd 的前身,命令名完全不同(按 JRockit 文档核对)
$ jrcmd 5123 print_threads
$ jrcmd 5123 heap_dump /tmp/jr.hprof
$ jrcmd 5123 jra_recordings
$ jrcmd 5123 jra_startrecording filename=/tmp/base.jra
  • 记录格式:早期 .jra/.jfr 要用对应年代的 JMC 打开,新版查看器未必兼容,先备份原始文件。
  • 堆转储:用 -XX:+HeapDumpOnCtrlBreak 或 jrcmd heap_dump 触发,产出的 hprof 与 HotSpot 不完全等价(线程本地分配区、nursery 概念不同),MAT 打开可能要转换。
  • GC 日志:-Xverbose:gc 输出,与 -Xlog:gc* 是两套格式,GCViewer 不认识。

4. JMC 与 JFR:今天真正可用的那个

概念 说明
JFR(Flight Recorder) JVM 内置事件记录引擎,把 GC、线程、锁、IO、异常、方法采样写进环形缓冲
JMC(Mission Control) 读 .jfr 的 GUI,附带 JMX 控制台与记录管理
关系 JFR 是数据源,JMC 只是查看器;也能用 jfr 命令行读
# 方式一:启动参数(JDK 11+ 已开放;JDK 8 需先解锁商业特性开关)
java -XX:StartFlightRecording=duration=120s,filename=/tmp/app.jfr,settings=profile -jar app.jar

# 方式二:运行中动态开启,不需要重启
$ jcmd 5123 JFR.start name=diag settings=profile duration=60s filename=/tmp/diag.jfr
$ jcmd 5123 JFR.check
$ jcmd 5123 JFR.dump name=diag filename=/tmp/diag-partial.jfr
$ jcmd 5123 JFR.stop name=diag
# 命令行读文件,不依赖 GUI
$ jfr summary /tmp/diag.jfr
$ jfr print --events jdk.ExecutionSample --stack-depth 64 /tmp/diag.jfr | head -n 40
$ jfr print --events jdk.JavaMonitorEnter /tmp/diag.jfr

低开销来自机制而非调参:事件写进线程本地缓冲、异步刷盘、复用预分配对象不产生额外垃圾;方法采样是周期采样不是逐次计数。整体开销典型量级在 1% 上下,settings=profile 记录事件更多所以更高——具体按 JDK 版本与负载核对。


5. 四类工具的能力对照

工具 采样粒度 看分配 看锁竞争 开销量级 成本
JProfiler 可采样可插桩,方法级 能(热点+存活) 能(监视器事件) 采样低、插桩高 商业授权,按席位
JMC + JFR 采样,毫秒级周期 能(需开分配事件) 能(JavaMonitorEnter) 低 JDK 11+ 免费
async-profiler 采样,可到毫秒内,能进 native 与内核 能(-e alloc) 能(-e lock) 低 开源
MAT / VisualVM 无采样,堆快照 能(直方图) 只能看线程栈 STW 开源

6. 商用与开源的分工

需求 首选 理由
线上临时抓 CPU 热点 async-profiler 单文件 agent、attach 即用、火焰图直出
方法级调用次数 JProfiler 插桩 采样给不了精确计数
谁在锁上排队 JFR 监视器事件 + JProfiler 锁视图 事件带持锁栈
SQL 慢与 N+1 JProfiler JDBC 探针 给出业务调用点而非只看驱动
常态化留证 JFR 常开 + 归档 开销低、可回看历史

7. 生产环境的三条边界

边界 要重启吗 前置条件 授权面
JFR 动态开启 不需要 JDK 11+(8 需解锁商业特性)、Attach 可用 无需商业授权
JProfiler 本地 attach 不需要 同用户、同位数、Attach 未禁用 GUI 与 agent 版本一致
JProfiler 远程 agent 需要 -agentpath 进启动脚本、端口放行 占许可席位
离线模式 需要 config.xml 提前准备、目录可写 占席位

授权合规的技术面只有三件事:许可文件放哪、浮动许可端口可达性、离网机器怎么绑定。上线前先确认。


8. 火焰图与调用树的读法差异

维度 调用树 火焰图
结构 树,按调用路径展开 合并相同栈帧,同层按字典序
横轴 不含时间含义,按占比排版 宽度=采样占比,不是时间轴
纵轴 调用层级 栈深度,底部是根
同一方法多次出现 每条调用路径各一行 合并成一个框
能否看"谁调用了我" 能,反向展开 不能,只能自下而上
适合 定位具体代码路径 一眼看占比、对外汇报
# async-profiler 直出各类火焰图(页面内可交互检索)
$ ./profiler.sh -d 30 -e cpu -f /tmp/cpu.html 5123
$ ./profiler.sh -d 30 -e alloc -f /tmp/alloc.html 5123
$ ./profiler.sh -d 30 -e lock -f /tmp/lock.html 5123

两个固定陷阱:"框宽"包含子调用,平顶宽框才是自身热点;递归会被合并成自己调自己的框。


9. 排错:七种典型现场

现象 原因 处理
attach 列表里没有目标进程 不同用户、namespace 隔离 docker exec 进容器执行
-agentpath 启动即失败 路径不存在、位数不符、版本不匹配 核对 uname -m 与 JVM 版本
agent 起了但 GUI 连不上 只监听回环、防火墙、启动顺序 ss -lntp 看监听地址,走 SSH 隧道
插桩后接口全线变慢 插桩开销叠加锁竞争,触发大量类重转换 立即停记录,改采样
采样耗时之和远小于实际耗时 周期太粗,或大量时间在 native/内核 缩短周期,换 async-profiler
离线快照没有出事的时段 config.xml 只配了启动即记录,无触发条件 改阈值触发或长期环形记录
.jfr 打不开 文件格式与工具版本代差不匹配 用同代 jfr 命令或对应版本 JMC

10. 边界:这些情况本就该换工具

场景 首选工具 理由
只看 GC 停顿 -Xlog:gc* + GCViewer / JFR GC 事件 停顿归因要每次回收的数据
native 与内核态热点 async-profiler、perf JVM 采样器看不进 native 栈
容器/托管 JVM 平台 APM 或 OpenTelemetry agent 可能挂不进去
长期趋势与告警 Prometheus + Micrometer 分析器只看当下
确认生效参数 jcmd VM.flags 不必开分析器

一句话:要精确次数用插桩,要线上低损耗用采样,要事后复盘用 JFR 常开。


11. 自测

  1. 同一个接口,采样结果里热点方法和插桩结果不一致,先怀疑哪一种工具的副作用?
  2. 远程 -agentpath 里漏了 nowait,服务启动会表现成什么样?
  3. 离线模式拿到的快照里没有任何可疑时段,最可能是哪一步没做?
  4. JFR 的 settings=profile 和 settings=default 差别在哪,开销差在哪?
  5. 调用树上某方法"总耗时高、自身耗时低",该往哪一层钻?
  6. 老系统的 .jra 记录用最新版 JMC 打不开,正确的处理顺序是什么?
S05-X25 · JVM 商用分析工具:JProfiler、JRockit 与 JMC
https://www.clxhxhhr.top/posts/6005/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。