这是《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,堆越大停顿越久。
生产采集前应确认:
- 已摘除流量或允许短时不可用;
- 磁盘剩余空间足够;
- dump 路径挂载在足够大的卷;
- 文件不会写入容器可写层;
- 有权限读取进程;
- 记录采集时间、实例、版本和当时流量;
- 敏感数据需要脱敏和访问控制。
更稳妥的策略是 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 中的特征:
- 同名类出现大量 ClassLoader;
- Class 对象数量异常;
- Metaspace 增长;
- 某个应用 ClassLoader Retained Heap 很大。
排查方向:
1. Path to GC Roots 查看谁引用 ClassLoader
2. 检查静态字段、线程、JMX、日志器、缓存
3. 检查第三方库注册回调
4. 重新部署是否真正释放旧类
5. 限制不必要的动态类生成
22.9 大对象分析
如果 byte[]、char[]、String 或集合占用量大,需要继续问三个问题:
- 它是什么业务数据;
- 谁引用它;
- 生命周期应该多长。
常见来源:
| 类型 | 来源 |
|---|---|
| byte[] | 文件、网络、序列化、加解密 |
| String | 大 SQL、大 JSON、日志拼接 |
| ArrayList | 一次性加载全量数据 |
| HashMap | 无界缓存 |
| StringBuilder | 循环拼接 |
优化方向:
- 分页或流式处理;
- 限制单次读取大小;
- 使用有界缓存;
- 复用缓冲区;
- 压缩冷数据;
- 生命周期结束后显式清空引用。
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,最终把引用链映射回业务生命周期。
思考题
- Shallow Heap 和 Retained Heap 有什么区别?
jmap -dump:live为什么会改变堆状态?- ThreadLocal 泄漏为什么在线程池场景更明显?
- Metaspace 持续增长时应检查什么?
- 如何写出可复现、可验证的内存泄漏分析报告?