heap dump和thread dump是jvm性能分析的两类核心诊断文件:前者记录堆内存中对象分布以定位内存泄漏,后者捕获线程执行栈以识别阻塞、死锁及cpu热点,二者协同分析才能准确解决gc频繁、响应迟缓等问题。

JVM性能分析中,内存堆栈跟踪不是单指“堆+栈”的简单叠加,而是两类关键诊断线索的协同使用:堆内存快照(Heap Dump)反映对象分布与泄漏点,线程堆栈跟踪(Thread Dump)揭示执行阻塞与锁竞争。二者结合才能准确定位内存占用高、GC频繁、响应变慢等典型问题。
堆内存跟踪:看“谁占了内存”
堆跟踪的核心是获取和分析 Heap Dump 文件,它记录某一时刻 JVM 堆中所有对象的实例、引用关系及大小。
-
Heap Dump 通常在以下场景主动触发或自动生成:
-
OutOfMemoryError发生时(加-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path) - 手动执行
jmap -dump:format=b,file=dump.hprof <pid></pid> - 通过 JVisualVM、JConsole 或 Arthas 的 dump 功能在线导出
-
-
分析重点包括:
- 大对象(Big Objects):如超大 byte[]、缓存 Map、未关闭的流
- 对象引用链:用 MAT(Eclipse Memory Analyzer)查看 “Dominator Tree” 和 “Leak Suspects”
-
重复类加载:检查
java.lang.Class实例数是否异常增长(可能由热部署、OSGi 或 ClassLoader 泄漏引起) -
字符串常量/内部化问题:大量相同内容的 String 占用堆,但未复用
intern(),或String.substring()在 JDK 7u6 之前保留原始 char[] 引用
示例:若 MAT 报告
char[]占堆 40%,且多数被java.lang.String持有,需排查日志框架(如 Logback)是否记录了超长请求体,或 JSON 解析未做截断。
线程堆栈跟踪:看“谁卡住了执行”
线程堆栈(Thread Dump)是 JVM 在某瞬间所有线程的状态快照,用于分析 CPU 飙升、线程阻塞、死锁等运行态问题。
-
获取方式:
-
jstack -l <pid></pid>(推荐加-l显示详细锁信息) -
kill -3 <pid></pid>(Linux/Unix,输出到 stdout 或 catalina.out) - JVisualVM / JConsole 的“Thread”页点击 “Thread Dump”
-
-
关键状态识别:
-
RUNNABLE:正在 CPU 上执行(注意:可能含 I/O 等待,不等于真忙) -
BLOCKED:等待进入 synchronized 同步块 -
WAITING/TIMED_WAITING:调用了Object.wait()、Thread.sleep()、LockSupport.park()等 -
java.util.concurrent.locks.AbstractOwnableSynchronizer类型锁:说明用了ReentrantLock,需结合jstack -l查持有者
-
-
常见模式:
- 多个线程 BLOCKED 在同一把锁 → 锁竞争瓶颈
- 大量线程处于 WAITING 且堆栈指向
ThreadPoolExecutor.getTask()→ 线程池空闲,非问题 - 线程堆栈深度极深 + 大量
at xxx.xxx.recursiveCall(...)→ 栈溢出风险或递归失控
示例:若发现 200+ 线程卡在
org.apache.http.impl.conn.PoolingHttpClientConnectionManager.closeIdleConnections,可能是连接池配置不合理或下游服务响应超时未处理,导致连接回收线程长期阻塞。
堆与栈联动分析:定位真实根因
单独看堆或栈都容易误判。真实问题常需交叉验证:
- GC 日志(开启
-XX:+PrintGCDetails -Xloggc:gc.log)显示频繁 Full GC → 先查 Heap Dump 是否存在内存泄漏 → 若无明显泄漏,再查 Thread Dump 是否有线程长期持有大对象引用(如静态 Map 缓存未清理) - 应用响应延迟,CPU 不高 → 查 Thread Dump 是否大量线程 WAITING 在数据库连接获取上 → 再结合堆分析
javax.sql.DataSource相关对象,确认连接池是否耗尽或连接泄漏 - 新增功能后内存持续增长 → 对比两个时间点的 Heap Dump(用 MAT 的 “Compare Basket”)→ 找出新增主导类 → 查该类创建路径(OQL 查询
SELECT * FROM com.example.CacheHolder)→ 回溯对应线程堆栈,定位调用源头
不复杂但容易忽略











