内存泄漏
一、什么是内存泄漏(Memory Leak)
概念:
内存泄漏是指程序中某些对象已经不再被使用,但仍然被引用着,GC 无法回收它们,导致内存长时间得不到释放。
换句话说:对象本该死掉,却还“活着”。
举例:
List cache = new ArrayList<>();
public void add() {
Object obj = new byte[1024 * 1024]; // 1MB
cache.add(obj); // 添加后永远不移除
}
- 每次调用
add()都往cache加对象,却从不清理,哪怕用不到,引用仍存在,GC 无法回收,最终内存被耗尽。
常见内存泄漏场景:
| 场景 | 描述 |
|---|---|
| 静态集合(如 Map、List) | 静态变量生命周期长,不主动清理会导致对象一直被引用 |
| 缓存不清理 | 自实现缓存逻辑,没有定期移除无用对象 |
| 监听器、回调未注销 | 比如事件监听器注册后不移除,导致对象长时间被引用 |
| ThreadLocal 泄漏 | 线程未释放,而 ThreadLocalMap 中的 value 引用未清除 |
| 数据库连接/文件/IO 未关闭 | 导致资源句柄未释放,间接导致内存占用 |
解决策略:
- 及时释放无用引用,例如从集合中移除、置为 null
- 使用 WeakReference(如 WeakHashMap)减少强引用持有
- 正确关闭资源、注销监听器
- 使用内存分析工具(如 VisualVM、MAT、jProfiler)定位泄漏对象和引用链
补充:ThreadLocal 泄漏——最常被问到的场景
表里那一行值得单独展开,因为它的泄漏链条绕了两层引用:
- 每个
Thread对象内部有一张ThreadLocalMap; - Map 的 key 是
ThreadLocal的弱引用,但 value 却是强引用; - 线程池里的线程长期不死,key 被回收后 value 变成"永远到不了的孤儿"——GC 够不着它,因为它还被线程强引用着。
所以用线程池 + ThreadLocal,用完必须 remove():
static ThreadLocal<byte[]> buf = new ThreadLocal<>();
public void handle() {
buf.set(new byte[1024 * 1024]);
try {
// 业务逻辑
} finally {
buf.remove(); // 不写这行,线程池场景下必然泄漏
}
}
更系统的原理(引用链图、InheritableThreadLocal)见 并发容器与工具类。
补充:泄漏和溢出的关系
两者经常一起出现,关系一句话说清:泄漏是慢性病,溢出是急性发作。
| 对比 | 内存泄漏 | 内存溢出(OOM) |
|---|---|---|
| 本质 | 对象该死没死,被引用拖住 | 内存彻底不够,分配失败 |
| 症状 | 缓慢恶化:老年代占用持续走高、Full GC 越来越频繁 | 直接抛 Error,程序崩溃 |
| 关系 | 持续泄漏 → 迟早引发堆溢出 | 但溢出不一定是泄漏(也可能是堆配太小、瞬时大对象) |
排查时先看 GC 日志里老年代曲线:只涨不跌基本就是泄漏;锯齿状正常波动却偶发 OOM,更可能是堆容量或流量问题。
补充:泄漏排查的标准动作
jps找到进程号 →jmap -histo:live <pid>看对象数量排行,异常多的大类先锁定;jmap -dump:live,format=b,file=heap.hprof <pid>拉堆快照;- 用 MAT(Eclipse Memory Analyzer)打开,看 Leak Suspects 报告和 GC Roots 引用链——MAT 会直接告诉你"谁引用着这堆不该活着的对象";
- 对照引用链改代码(静态集合清理、remove、注销监听器……)。
GC 底层为什么回收不掉这些对象(可达性分析、GC Roots 是什么),见 垃圾回收与GC算法。
💬 评论