有一次凌晨,接口 P99 飙到 30 秒,CPU 却不到 20%。运维说「重启吧」,我拦住了------先 dump 再重启 。十分钟后 jstack 里看到 180 个 http-nio-* 线程堵在 HikariPool.getConnection,根因是连接池耗尽(慢 SQL 占着连接不还),不是网络。
另一次是内存告警:堆使用率 92%,Full GC 每分钟一次,接口时好时坏。这次要的是 Heap Dump ,MAT 里一个大 HashMap 占了 1.2GB,是本地缓存没设上限。
两个场景,两种 Dump。抓错了,白忙活;抓对了,十分钟定位。
一、先搞清:两种 Dump 各是什么
| 维度 | 线程 Dump(Thread Dump) | Heap Dump(堆 Dump) |
|---|---|---|
| 本质 | JVM 某一时刻所有线程的快照 | JVM 堆内存某一时刻的快照 |
| 看什么 | 线程状态、调用栈、锁、死锁 | 对象实例、引用链、内存占用 |
| 采集 | jstack、jcmd Thread.print、Arthas thread |
jmap、Arthas heapdump、-XX:+HeapDumpOnOutOfMemoryError |
| 分析 | grep、FastThread、VisualVM | Eclipse MAT(首选)、VisualVM |
| 文件大小 | 通常 KB~几 MB | 通常几百 MB~数 GB |
| 对业务影响 | 极小,可多次抓 | 较大,Full GC + 写盘,高峰慎用 |
| 适合症状 | 假死、超时、CPU 不高但慢 | OOM、内存泄漏、频繁 Full GC |
一句话:线程 Dump 回答「谁在干什么、卡在哪」;Heap Dump 回答「内存被谁占着、为什么回收不掉」。
二、线程 Dump:能挖出什么
一次 jstack 输出里,重点看这几块:
- 线程状态 :
RUNNABLE(跑或等 I/O)、WAITING/TIMED_WAITING(等锁、等通知)、BLOCKED(抢锁失败) - 调用栈 :栈顶方法往往就是卡点------
socketRead0是等下游,getConnection是连接池干了,park可能在等锁 - 死锁 :文件末尾
Found one Java-level deadlock直接点名 - 线程池 :大量同名
http-nio-*-exec-*、pool-*-thread-*,说明某类任务把池子占满
采集命令
bash
jstack -l <pid> > thread-$(date +%Y%m%d-%H%M%S).dump
# 进程无响应时,加 -F 强制(会 STW 一下)
jstack -F -l <pid> > thread-forced.dump
有 Arthas 时我更常用:
bash
thread -n 10 # CPU 最高的 10 个
thread -b # 找死锁
典型线上场景
| 现象 | 线程 Dump 里常见线索 |
|---|---|
| 接口全超时,CPU 低 | 业务线程 WAITING/TIMED_WAITING,栈顶 getConnection / Feign / socketRead0 |
| 部分接口卡死 | Found one Java-level deadlock 或某把锁被单线程长期持有 |
| CPU 100% | thread -n 找热点栈,常见死循环、正则回溯 |
| MQ 消费堆积 | 消费线程 BLOCKED 在同一把业务锁上 |
三、Heap Dump:能挖出什么
Heap Dump 是堆的快照:每个对象多大、被谁引用。拷到本机后怎么打开、看哪些视图,见第六节。
采集命令
bash
# live 对象(会触发 Full GC,生产高峰慎用)
jmap -dump:live,format=b,file=heap-$(date +%Y%m%d-%H%M%S).hprof <pid>
# 不触发 Full GC 的全量堆(文件更大)
jmap -dump:format=b,file=heap-full.hprof <pid>
更稳妥的做法:启动参数预埋,OOM 时自动落盘:
bash
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump/
-XX:+ExitOnOutOfMemoryError # 可选:OOM 后退出,交给编排重启
Arthas:heapdump /tmp/heap.hprof(--live 会触发 Full GC)。
典型线上场景
| 现象 | Heap Dump 里常见线索 |
|---|---|
OutOfMemoryError: Java heap space |
某集合类实例数异常多,或 byte[] 巨大 |
| 内存缓慢上涨,重启就好 | 静态 ConcurrentHashMap / Guava Cache 无上限 |
| Full GC 频繁但回收不了多少 | Old 区大对象常驻,引用链指向 ThreadLocal 或单例 |
| Metaspace OOM | 不是 Heap Dump 的主场,换 jcmd VM.classloader_stats |
四、线上出事了,先抓哪个?
我按症状选,不两个一起乱抓------Heap Dump 写盘慢,高峰容易雪上加霜。
css
接口慢/超时/假死,内存正常 → 先线程 Dump(连抓 2~3 次,间隔 10s,看栈变不变)
CPU 飙高 → 线程 Dump + top -Hp(或 Arthas profiler)
内存涨/OOM/Full GC 异常 → Heap Dump(低峰抓;已 OOM 且有自动 dump,直接用)
怀疑内存泄漏 → 间隔抓两份 Heap 对比;必要时补一份线程 Dump
标准动作:留现场 → 再止血。
bash
jstack -l <pid> > thread-1.dump
sleep 10 && jstack -l <pid> > thread-2.dump
sleep 10 && jstack -l <pid> > thread-3.dump # 可选第三次
jstat -gcutil <pid> 1000 5 # 判断要不要 heap dump
# 确认需要再抓(磁盘、内存要够)
jmap -dump:live,format=b,file=heap.hprof <pid>
# 最后再重启 / 扩容 / 回滚
同一线程 2~3 次栈顶一样,说明真卡在那;栈在变,可能是正常高负载。
五、能随时执行吗?对线上有啥影响?
两个命令都能在线上跑,安全程度差很多,别一视同仁。
线程 Dump(jstack) |
堆 Dump(jmap / heapdump) |
|
|---|---|---|
| 能随时抓吗 | 基本可以,排查首选 | 能抓,但要挑时机,别高峰随便上 |
| 典型耗时 | 1~3 秒 | 几十秒~几分钟(堆越大越久) |
| 是否触发 Full GC | 否 | -dump:live / --live 会 |
| 对业务影响 | 极小,可连抓 2~3 次 | STW + 写盘 IO,可能短暂卡顿 |
| 其他注意 | jstack -F 会 STW,非必要不用 |
确认磁盘够,1GB 堆≈1GB+ 文件 |
线程 Dump 像拍 X 光------只读快照,不碰堆内存,出问题时可放心先抓。
堆 Dump 像全麻 CT -------dump:live 先 Full GC 再写 .hprof,高峰执行可能雪上加霜。OOM 时由 -XX:+HeapDumpOnOutOfMemoryError 自动落盘,属于「服务已经挂了,留现场」,不算日常随便抓。
我线上的顺序:先 jstack → jstat 看内存 → 确认是内存问题且低峰 → 再 jmap。
六、Dump 下载下来,用什么工具分析
| 文件 | 后缀 | 推荐工具 |
|---|---|---|
| 线程 Dump | .txt、.dump |
grep → FastThread(脱敏)/ VisualVM |
| Heap Dump | .hprof |
Eclipse MAT → VisualVM(小文件概览) |
线程 Dump
| 工具 | 说明 |
|---|---|
| grep + 编辑器 | 零依赖,快速扫死锁、数线程、搜卡点 |
| FastThread | 脱敏后上传,自动归类阻塞线程 |
| VisualVM | 免费,需单独下载(JDK 9+ 不再自带) |
| IBM Thread Analyzer | 锁竞争复杂、要看 monitor 链时 |
bash
grep -A 20 "deadlock" thread.dump
grep "http-nio" thread.dump | wc -l
grep -B 2 -A 30 "getConnection" thread.dump
含 SQL、用户 ID、内部 URL 的 dump,别上传公网。 我习惯本地 grep 定方向,线程多再脱敏扔 FastThread。
Heap Dump
几乎无脑 MAT。 大文件启动前加内存:
bash
./MemoryAnalyzer -vmargs -Xmx4g # 分析 2GB hprof,MAT 至少配 4GB
打开后按顺序看:Leak Suspects Report → Histogram → Dominator Tree → Path to GC Roots。
VisualVM 只适合小文件瞄一眼 Histogram;公司有 license 再用 JProfiler / YourKit。
七、两个 Dump 怎么配合读
单独看都有盲区。我印象最深的一次:@Transactional 里包了 Feign,下游一慢,全站接口超时。线程 Dump 里业务线程栈顶卡在 Feign 读 socket,同时 HikariCP active=10、多线程堵 getConnection------事务占着 DB 连接等 HTTP,不是网络问题。那次只抓线程 Dump 就够了,Heap 没抓。
另两种常见组合:
- 内存涨 + 读变慢 :Heap 里
ConcurrentHashMap$Node实例暴涨;线程 Dump 里get竞争------本地缓存没设上限。 - CPU 100% :线程 Dump 找热点栈;Heap 一般帮不上忙,除非循环里疯狂
new。
八、踩坑提醒
-dump:live 不一定更好。 会 Full GC + STW;1GB 堆可能产出 1GB+ 的 .hprof,磁盘要够。
线程 Dump 过滤噪音。 略过 VM Thread、GC task thread,盯 http-nio-*、ConsumeMessageThread_*。
容器里别搞错 pid。 K8s 里 Java 有时 pid 就是 1;拿不准用 jcmd:
bash
jcmd <pid> Thread.print > thread.dump
jcmd <pid> GC.heap_dump /tmp/heap.hprof
九、几条硬规矩
- 慢、卡、假死、死锁 → 线程 Dump,轻、可反复抓。
- OOM、泄漏、Full GC 救不回来 → Heap Dump,MAT 看引用链。
- 线上先留证再重启,否则下次还是猜。
- 两个 Dump 互补,分轻重------别高峰一上来就 heap。
十、文中术语速查
| 术语 | 一句话解释 |
|---|---|
| pid | 进程 ID。容器里 Java 有时就是 1,拿不准用 jcmd 或 ps 查 |
| STW(Stop-The-World) | GC 或 dump 时暂停所有业务线程,期间请求会卡住 |
| Full GC | 对整个堆(含老年代)做垃圾回收;频繁出现说明内存压力大 |
| OOM | Out Of Memory,堆内存不够,抛 OutOfMemoryError |
| Metaspace | 存类元数据的区域,类加载过多会 Metaspace OOM(和 Heap Dump 不是一回事) |
| 调用栈 | 线程当前执行到哪个方法,栈顶往往是卡点 |
| RUNNABLE | 线程在跑,或在等 I/O(如读 socket) |
| WAITING / TIMED_WAITING | 线程在等锁、等通知,或等连接池/下游返回 |
| BLOCKED | 抢 synchronized 锁失败,堵在门外 |
| GC Roots | 垃圾回收认定的「活对象」起点;删不掉的引用链从这里查 |
| Histogram | MAT 里按类统计对象数量和占用,看谁实例最多 |
| Dominator Tree(支配树) | 找「一个大对象拖住一片内存」的根 |
| Leak Suspects | MAT 自动生成的泄漏嫌疑报告,辅助用,不能盲信 |
| Path to GC Roots | 从对象反查到 GC Roots 的引用链,看为什么回收不掉 |
-dump:live |
只 dump 存活对象,会先 Full GC;文件小,但有 STW 代价 |
| 假死 | 进程还在、端口能连,但线程池/连接池满了,新请求进不来 |