JVMNotes

第 22 章:堆 dump 分析

zjc 于 2026-01-22 发布

这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 堆 dump 是某个时刻 Java 堆的快照。它像一次完整的“合影”,能看到对象类型、数量、字段、引用链和类加载器。它不适合观察趋势,但适合回答:堆里到底是什么、谁引用了它、为什么不能回收。

22.1 dump 类型

类型 内容 用途
heap dump 对象和引用 内存泄漏、对象膨胀
core dump 进程完整崩溃快照 Native 崩溃
JFR 持续事件流 性能分析

堆 dump 通常使用 HPROF 格式,文件扩展名常为 .hprof

注意:堆 dump 本身会占用磁盘空间,大小通常接近活跃对象和堆结构元数据规模。

22.2 获取方式

OOM 自动生成:

java -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/data/dump \
  -jar app.jar

手动生成:

jmap -dump:live,format=b,file=/data/dump/app.hprof <pid>

推荐使用 jcmd

jcmd <pid> GC.heap_dump /data/dump/app.hprof

jmap -dump:live 会先触发一次 Full GC,只保留可达对象。这样文件更小,但会丢失不可达对象和真实的瞬时内存状态。

22.3 采集注意点

手动 dump 会引发 Stop-The-World,堆越大停顿越久。

生产采集前应确认:

  1. 已摘除流量或允许短时不可用;
  2. 磁盘剩余空间足够;
  3. dump 路径挂载在足够大的卷;
  4. 文件不会写入容器可写层;
  5. 有权限读取进程;
  6. 记录采集时间、实例、版本和当时流量;
  7. 敏感数据需要脱敏和访问控制。

更稳妥的策略是 OOM 自动 dump,并配合滚动保留策略避免磁盘被填满。

22.4 MAT 基本用法

Eclipse Memory Analyzer(MAT)是常用堆分析工具。

打开 dump 后,常用视图:

视图 用途
Overview 堆大小、对象总数、大类
Dominator Tree 谁独占最多内存
Histogram 类维度对象数量和浅堆
Path to GC Roots 为什么对象未回收
Leak Suspects 自动可疑点
Thread Overview 线程栈和局部变量

关键指标:

指标 含义
Shallow Heap 对象自身大小
Retained Heap 对象被回收后可释放总量
Objects 实例数量
Class Loader 类加载来源

排查内存问题优先看 Retained Heap,而不是只看实例数。

22.5 分析流程

1. 确认 dump 采集时间和症状
2. 看 Overview,确认堆规模
3. Histogram 找数量异常的类
4. Dominator Tree 找 Retained Heap 最大对象
5. 查看对象字段内容
6. Path to GC Roots 找引用者
7. 映射到业务对象或缓存 key
8. 结合源码确认生命周期
9. 修复后压测验证

不要只依赖 MAT 的 Leak Suspects 自动报告。自动报告只能提示可疑点,最终要回到引用链和源码。

22.6 常见泄漏形态

形态 特征 方向
静态集合增长 Map/List Retained 大 检查清理逻辑
ThreadLocal 未清理 线程池线程持有 Entry finally 中 remove
监听器未注销 回调对象长期持有 生命周期绑定
缓存无界 业务 key 持续增加 容量、过期、淘汰
ClassLoader 泄漏 类和实例大量重复加载 检查动态类加载
大数组暂存 byte[] Retained 大 拆分或释放
连接对象未关闭 连接和缓冲持有 try-with-resources

22.7 ThreadLocal 泄漏示例

错误代码:

private static final ThreadLocal<List<byte[]>> CONTEXT =
    ThreadLocal.withInitial(ArrayList::new);

public void handle(int size) {
    CONTEXT.get().add(new byte[size]);
}

在线程池中,线程会长期存活。如果业务对象放入 ThreadLocal 后不移除,对象可能一直挂在线程的 ThreadLocalMap 上。

正确处理:

try {
    CONTEXT.get().add(new byte[size]);
    process();
} finally {
    CONTEXT.remove();
}

更基本的问题是:ThreadLocal 是否应该存大对象?如果只是传参,优先使用方法参数或明确的请求上下文对象。

22.8 ClassLoader 泄漏

热部署、脚本引擎、动态代理、OSGi、反射生成类都可能创建新的 ClassLoader。如果老 ClassLoader 加载的类仍被全局对象引用,就无法卸载。

MAT 中的特征:

  1. 同名类出现大量 ClassLoader;
  2. Class 对象数量异常;
  3. Metaspace 增长;
  4. 某个应用 ClassLoader Retained Heap 很大。

排查方向:

1. Path to GC Roots 查看谁引用 ClassLoader
2. 检查静态字段、线程、JMX、日志器、缓存
3. 检查第三方库注册回调
4. 重新部署是否真正释放旧类
5. 限制不必要的动态类生成

22.9 大对象分析

如果 byte[]char[]String 或集合占用量大,需要继续问三个问题:

  1. 它是什么业务数据;
  2. 谁引用它;
  3. 生命周期应该多长。

常见来源:

类型 来源
byte[] 文件、网络、序列化、加解密
String 大 SQL、大 JSON、日志拼接
ArrayList 一次性加载全量数据
HashMap 无界缓存
StringBuilder 循环拼接

优化方向:

  1. 分页或流式处理;
  2. 限制单次读取大小;
  3. 使用有界缓存;
  4. 复用缓冲区;
  5. 压缩冷数据;
  6. 生命周期结束后显式清空引用。

22.10 修复与验证

修复不是只改一行代码,还要验证:

1. 压测复现原问题
2. 对比修复前后堆增长曲线
3. 观察 Full GC 后 Old 占用
4. 检查实例数量是否稳定
5. 确认延迟和吞吐没有劣化
6. 保留监控告警

一个合格结论应写成:

现象:Old 区 6 小时从 1.2g 增长到 3.8g
证据:MAT 显示 xxxCache Retained 1.9g,key 为请求参数
根因:本地缓存没有容量和过期策略
修复:改为 Caffeine,maximumSize=100000,expireAfterWrite=10m
验证:8 小时压测后 Old 回收后稳定在 1.3g

本章小结

堆 dump 适合分析某个时刻堆内对象和引用关系,是定位内存泄漏和大对象问题的重要证据。生产中应优先配置 OOM 自动 dump,手动 dump 要评估停顿和磁盘空间。MAT 分析要抓住 Dominator Tree、Histogram、Retained Heap 和 Path to GC Roots,最终把引用链映射回业务生命周期。

思考题

  1. Shallow Heap 和 Retained Heap 有什么区别?
  2. jmap -dump:live 为什么会改变堆状态?
  3. ThreadLocal 泄漏为什么在线程池场景更明显?
  4. Metaspace 持续增长时应检查什么?
  5. 如何写出可复现、可验证的内存泄漏分析报告?