内存泄漏排查
Java 有 GC,但仍然会内存泄漏。Java 内存泄漏通常不是对象无法回收,而是对象仍然被不该存在的引用链持有。
# 1. 什么是 Java 内存泄漏
Java 内存泄漏指对象已经没有业务意义,但仍然可以从 GC Roots 到达,因此无法被 GC 回收。
典型表现:
- 老年代持续增长。
- Full GC 后内存下降不明显。
- 最终 OOM。
- 服务运行时间越长越慢。
# 2. 常见泄漏来源
常见来源:
- 静态集合。
- 本地缓存无淘汰。
- ThreadLocal 未 remove。
- 监听器未注销。
- 连接、Session、Channel 未关闭。
- 类加载器泄漏。
- 大对象被全局引用。
- 队列生产快于消费。
# 3. 观察趋势
先看趋势,而不是立刻 dump。
关注:
- 堆使用量是否持续上升。
- Full GC 后是否能回到稳定水位。
- 老年代是否持续增长。
- 对象数量是否持续增加。
- 泄漏是否和某个接口或任务相关。
如果 Full GC 后仍回不去,泄漏嫌疑较大。
# 4. 生成堆转储
jmap -dump:format=b,file=heap.hprof <pid>
更推荐配置 OOM 自动 dump:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump
如果条件允许,取两个不同时间点的 dump 对比更有价值。
# 5. MAT 分析思路
使用 MAT 时重点看:
- Leak Suspects。
- Dominator Tree。
- Retained Size。
- Path to GC Roots。
- 类实例数量。
不要只看 Shallow Size。一个小对象可能持有一棵巨大的对象树。
# 6. ThreadLocal 泄漏
ThreadLocal 常见泄漏模式:
threadLocal.set(value);
// 忘记 remove
线程池中的线程长期存活,如果不清理 ThreadLocal,值对象可能一直被线程持有。
建议:
try {
threadLocal.set(value);
// business
} finally {
threadLocal.remove();
}
# 7. 缓存泄漏
缓存不是 Map 加静态变量这么简单。
缓存应有:
- 最大容量。
- 过期策略。
- 淘汰策略。
- 指标监控。
- 命中率统计。
生产建议使用成熟缓存库,例如 Caffeine。
# 8. 阶段小结
Java 内存泄漏的本质是“不该活着的对象还能从 GC Roots 到达”。排查时先看趋势,再抓 dump,最后沿引用链找到持有者。修复通常不是加堆,而是切断错误引用或增加生命周期管理。
# 9. 泄漏排查流程图
发现内存持续增长
│
├─ 看 GC 后水位是否回落
│
├─ 回落正常
│ └─ 可能是正常波动或负载增加
│
└─ 不回落
├─ 抓 heap dump
├─ MAT 看 Retained Size
├─ 找 GC Roots 引用链
├─ 对应到代码持有者
└─ 修复生命周期
# 10. 泄漏模式速查
| 模式 | 典型代码味道 | 修复方向 |
|---|---|---|
| 静态集合 | static Map 不清理 | 生命周期管理 |
| ThreadLocal | set 后不 remove | finally remove |
| 缓存 | 无容量、无过期 | Caffeine/淘汰策略 |
| 监听器 | 注册后不注销 | close/unregister |
| 队列 | 无界队列 | 背压、限流、有界队列 |
| 类加载器 | 热部署后旧 loader 可达 | 清理线程、驱动、ThreadLocal |
# 11. Tips 快问快答
Q:内存上涨就是泄漏吗?
A:不一定。要看 Full GC 后水位是否能回落,以及是否和流量/缓存预期一致。
Q:弱引用能防止所有泄漏吗?
A:不能。错误使用弱引用可能隐藏问题,value 或其他链路仍可能强引用对象。
Q:怎么判断缓存是不是泄漏?
A:看是否有明确容量、过期、淘汰和命中率。如果没有边界,就有泄漏风险。
Q:泄漏修复后怎么验证?
A:压测或灰度观察 GC 后水位、对象数量、老年代趋势是否稳定。
上次更新: 2026/06/24, 16:03:01