2901 字
约 9 分钟
17
S05-X25 · JVM 商用分析工具:JProfiler、JRockit 与 JMC
接口平时 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. 自测
- 同一个接口,采样结果里热点方法和插桩结果不一致,先怀疑哪一种工具的副作用?
- 远程
-agentpath里漏了nowait,服务启动会表现成什么样? - 离线模式拿到的快照里没有任何可疑时段,最可能是哪一步没做?
- JFR 的
settings=profile和settings=default差别在哪,开销差在哪? - 调用树上某方法"总耗时高、自身耗时低",该往哪一层钻?
- 老系统的
.jra记录用最新版 JMC 打不开,正确的处理顺序是什么?
S05-X25 · JVM 商用分析工具:JProfiler、JRockit 与 JMC
https://www.clxhxhhr.top/posts/6005/ 评论
0 条
还没有评论,先写一条吧。