JVM 性能诊断工具实战——arthas、jstack、jmap 的组合拳使用
JVM 性能诊断工具实战——arthas、jstack、jmap 的组合拳使用
适读人群:需要排查 Java 服务性能问题、线上故障的工程师 | 阅读时长:约18分钟 | 核心价值:掌握 arthas、jstack、jmap 等工具的实战使用,建立线上问题排查的系统方法论
一、凌晨三点的 CPU 飙高排查
凌晨三点,老李的手机响了,监控告警:核心服务 CPU 使用率 95%,接口响应时间从 50ms 飙到 3000ms。他登上服务器,进程还活着,接口还在响,但慢得像死了一样。
他慌了,第一反应是重启。我拦住他说:"别急着重启,先诊断,重启会丢失现场,这次查不清楚下次还会复发。"
接下来 15 分钟,我们用三个工具定位了问题:一个正则表达式出现了灾难性回溯,某个输入字符串触发了指数级匹配时间,导致处理线程全部卡死,CPU 跑满。
没有这套诊断工具,靠看日志可能查几个小时都找不到。这篇文章我把这套工具的完整用法讲清楚,让你遇到线上问题时不慌不忙。
二、Arthas——线上诊断神器
Arthas 是阿里开源的 Java 在线诊断工具,我觉得它是每个 Java 工程师必须掌握的工具,没有之一。
安装和启动:
# 下载
curl -O https://arthas.aliyun.com/arthas-boot.jar
# 启动(会列出当前 JVM 进程,选择目标进程)
java -jar arthas-boot.jar2.1 dashboard——快速看系统状态
[arthas@12345]$ dashboard输出一个实时刷新的仪表盘,显示所有线程的 CPU 使用率、堆内存各区域大小、GC 次数等。一眼就能看到谁在占用 CPU。
2.2 thread——定位 CPU 飙高的元凶
# 展示所有线程
thread
# 展示最忙的 Top 3 线程(CPU 占用最高的)
thread -n 3
# 展示指定线程 ID 的栈
thread 123
# 查找所有阻塞状态的线程
thread --state BLOCKED回到凌晨三点的案例,执行 thread -n 5 之后看到:
"http-nio-8080-exec-7" Id=123 cpuUsage=32% deltaTime=320ms time=45000ms RUNNABLE
at java.util.regex.Pattern$GroupTail.match(Pattern.java:4717)
at java.util.regex.Pattern$BranchConn.match(Pattern.java:4568)
at java.util.regex.Pattern$Branch.match(Pattern.java:4504)
...(递归调用几百层)
at com.example.service.OrderService.validateInput(OrderService.java:87)一眼就看到了正则引擎的递归栈,定位到了 OrderService.java:87 的正则匹配问题。
2.3 watch——不重启就能观察方法入参和返回值
这个命令是 Arthas 里我用得最多的,可以在不修改代码、不重启服务的情况下,实时观察指定方法的调用情况。
# 观察 OrderService.createOrder 方法的入参和返回值
# -x 2 表示展开对象到 2 层
watch com.example.service.OrderService createOrder "{params,returnObj}" -x 2
# 只有当方法抛出异常时才打印
watch com.example.service.OrderService createOrder "{params,throwExp}" -x 2 -e
# 方法耗时超过 100ms 才打印
watch com.example.service.OrderService createOrder "{params,returnObj}" '#cost>100' -x 2
# 观察多个匹配的方法(支持通配符)
watch com.example.service.*Service * "{params,returnObj}" -x 22.4 trace——找到慢在哪一步
# 追踪 OrderService.createOrder 的调用链路和每一步耗时
trace com.example.service.OrderService createOrder
# 只打印总耗时超过 500ms 的调用
trace com.example.service.OrderService createOrder '#cost>500'输出示例:
`---[520ms] com.example.service.OrderService:createOrder()
+---[5ms] com.example.repository.OrderRepository:save()
+---[485ms] com.example.service.InventoryService:deductStock() <- 慢在这里
`---[30ms] com.example.service.NotificationService:notify()直接告诉你哪个子调用慢,精确到毫秒。
2.5 ognl——动态执行表达式
# 查看静态变量的值
ognl '@com.example.config.AppConfig@MAX_RETRY'
# 调用静态方法
ognl '@java.lang.System@getProperty("java.version")'
# 查看 Spring Bean(配合 @SpringContextHolder)
ognl '#bean=@org.springframework.context.ApplicationContextHolder@getContext().getBean("orderService"), #bean.getOrderById(1001L)'三、jstack——分析线程状态
jstack 是 JDK 内置的线程分析工具,输出 JVM 所有线程的栈快照。
# 输出线程栈到文件
jstack -l <pid> > /tmp/thread-dump.txt
# 强制输出(即使进程hang住了)
jstack -F <pid> > /tmp/thread-dump-force.txt分析死锁:jstack 输出末尾会自动识别死锁并高亮显示:
Found one Java-level deadlock:
=============================
"Thread-A":
waiting to lock monitor 0x00007f... (object 0x..., a java.lang.Object),
which is held by "Thread-B"
"Thread-B":
waiting to lock monitor 0x00007f... (object 0x..., a java.lang.Object),
which is held by "Thread-A"分析线程状态分布:
# 统计各状态线程数量(在 Linux 上)
jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c | sort -rn输出:
120 java.lang.Thread.State: WAITING (parking)
35 java.lang.Thread.State: TIMED_WAITING (parking)
18 java.lang.Thread.State: RUNNABLE
5 java.lang.Thread.State: BLOCKED (on object monitor)如果 BLOCKED 线程很多,说明有锁竞争严重的问题;如果 WAITING 线程很多,通常是正常的(等待任务的线程池线程)。
四、jmap——堆内存分析
# 查看堆内存使用情况(快速)
jmap -heap <pid>
# 打印堆对象统计(按对象类型排序,看哪类对象最多)
jmap -histo:live <pid> | head -50
# 生成堆转储文件(Full GC 一次再 dump,保证是存活对象)
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>jmap -histo 输出示例:
num #instances #bytes class name
----------------------------------------------
1: 1234567 123456789 [B (byte[])
2: 567890 45678901 java.lang.String
3: 123456 12345678 com.example.model.OrderDO
4: 89012 8901234 java.util.HashMap$Node如果 OrderDO 实例数量异常多(比如几百万),说明有内存泄漏,需要进一步分析谁持有这些对象的引用。
五、组合拳:CPU 飙高排查完整流程
问题:CPU 飙高,接口响应慢
Step 1: arthas dashboard
→ 确认哪些线程占用 CPU 最高
Step 2: arthas thread -n 5
→ 获取 CPU 最高的线程栈,定位到代码行
Step 3: 如果是锁竞争,arthas thread --state BLOCKED
→ 查看所有阻塞线程,找到竞争的锁
Step 4: 如果要看方法入参,arthas watch
→ 不停机观察触发问题的具体参数
Step 5: arthas trace 慢方法
→ 确认慢在哪一步,是 DB 慢、外部调用慢还是计算慢六、踩坑实录
坑1:jstack 无法 attach 到进程
现象:执行 jstack <pid> 报错 Unable to open socket file。
原因:jstack 执行者的用户与 JVM 进程运行用户不一致(比如 JVM 跑在 tomcat 用户下,jstack 用 root 执行)。
解法:sudo -u tomcat jstack <pid>,用和 JVM 同一个用户执行。
坑2:Arthas attach 之后进程 CPU 更高了
现象:Arthas 连上之后,执行 watch/trace 命令,CPU 飙得更高。
原因:watch 和 trace 依赖字节码增强,每次方法调用都有额外开销。trace 的开销尤其大,因为要记录每一步的时间戳。
解法:线上诊断时,watch/trace 用完立刻停止(q 命令退出),不要长时间开着。高并发接口的 trace 要加 #cost>xxx 条件过滤,减少触发频率。这个坑我也踩过,在一次高峰期排查时 trace 开了太久,把 CPU 从 80% 搞到了 99%。
坑3:jmap -dump 导致服务暂停
现象:执行 jmap -dump 时,服务停止响应了几秒到几十秒。
原因:生成堆转储需要触发一次 Stop-The-World,堆越大停顿时间越长。8G 堆的 dump 可能需要 10~30 秒的 STW。
解法:生产环境需要堆分析时,优先用 Arthas 的在线命令(heapdump 也会 STW,但影响相对小);或者先把流量摘除(Kubernetes 里把 Pod 从 Service 移除),再做 dump。如果业务允许短暂中断,在非高峰期操作。
七、工具选择建议
| 场景 | 推荐工具 |
|---|---|
| CPU 飙高,找热点方法 | arthas thread + trace |
| 方法调用参数/返回值观察 | arthas watch |
| 线程死锁分析 | jstack |
| 内存泄漏初查 | jmap -histo |
| 内存泄漏深度分析 | jmap -dump + MAT |
| JVM 内存区域情况 | jmap -heap 或 arthas memory |
| 整体状态快览 | arthas dashboard |
工具本身不难,难的是建立排查问题的思维模型:先定位是 CPU 问题还是内存问题还是 IO 问题,再选工具,按顺序剥洋葱,不要上来就 dump 堆做最重的分析。
