线程 Dump 还是 Heap Dump?线上故障时先抓哪个

线程 Dump 还是 Heap Dump?线上故障时先抓哪个

Scroll Down

有一次凌晨,接口 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 堆内存某一时刻的快照
看什么 线程状态、调用栈、锁、死锁 对象实例、引用链、内存占用
采集 jstackjcmd 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 输出里,重点看这几块:

  1. 线程状态RUNNABLE(跑或等 I/O)、WAITING/TIMED_WAITING(等锁、等通知)、BLOCKED(抢锁失败)
  2. 调用栈:栈顶方法往往就是卡点——socketRead0 是等下游,getConnection 是连接池干了,park 可能在等锁
  3. 死锁:文件末尾 Found one Java-level deadlock 直接点名
  4. 线程池:大量同名 http-nio-*-exec-*pool-*-thread-*,说明某类任务把池子占满

采集命令

jstack -l <pid> > thread-$(date +%Y%m%d-%H%M%S).dump

# 进程无响应时,加 -F 强制(会 STW 一下)
jstack -F -l <pid> > thread-forced.dump

有 Arthas 时我更常用:

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 是堆的快照:每个对象多大、被谁引用。拷到本机后怎么打开、看哪些视图,见第六节

采集命令

# 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 时自动落盘:

-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 写盘慢,高峰容易雪上加霜。

接口慢/超时/假死,内存正常  → 先线程 Dump(连抓 2~3 次,间隔 10s,看栈变不变)
CPU 飙高                    → 线程 Dump + top -Hp(或 Arthas profiler)
内存涨/OOM/Full GC 异常     → Heap Dump(低峰抓;已 OOM 且有自动 dump,直接用)
怀疑内存泄漏                → 间隔抓两份 Heap 对比;必要时补一份线程 Dump

标准动作:留现场 → 再止血。

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 自动落盘,属于「服务已经挂了,留现场」,不算日常随便抓。

我线上的顺序:jstackjstat 看内存 → 确认是内存问题且低峰 → 再 jmap


六、Dump 下载下来,用什么工具分析

文件 后缀 推荐工具
线程 Dump .txt.dump grep → FastThread(脱敏)/ VisualVM
Heap Dump .hprof Eclipse MAT → VisualVM(小文件概览)

线程 Dump

工具 说明
grep + 编辑器 零依赖,快速扫死锁、数线程、搜卡点
FastThread 脱敏后上传,自动归类阻塞线程
VisualVM 免费,需单独下载(JDK 9+ 不再自带)
IBM Thread Analyzer 锁竞争复杂、要看 monitor 链时
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。 大文件启动前加内存:

./MemoryAnalyzer -vmargs -Xmx4g    # 分析 2GB hprof,MAT 至少配 4GB

打开后按顺序看:Leak Suspects ReportHistogramDominator TreePath 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 ThreadGC task thread,盯 http-nio-*ConsumeMessageThread_*

容器里别搞错 pid。 K8s 里 Java 有时 pid 就是 1;拿不准用 jcmd

jcmd <pid> Thread.print > thread.dump
jcmd <pid> GC.heap_dump /tmp/heap.hprof

九、几条硬规矩

  1. 慢、卡、假死、死锁 → 线程 Dump,轻、可反复抓。
  2. OOM、泄漏、Full GC 救不回来 → Heap Dump,MAT 看引用链。
  3. 线上先留证再重启,否则下次还是猜。
  4. 两个 Dump 互补,分轻重——别高峰一上来就 heap。

十、文中术语速查

术语 一句话解释
pid 进程 ID。容器里 Java 有时就是 1,拿不准用 jcmdps
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 代价
假死 进程还在、端口能连,但线程池/连接池满了,新请求进不来

参考内容