--- title: "01-内存泄漏" created: 2025-12-02 tags: - Java --- # 内存泄漏 ## 一、什么是内存泄漏(Memory Leak) ### 概念: 内存泄漏是指**程序中某些对象已经不再被使用,但仍然被引用着,GC 无法回收它们,导致内存长时间得不到释放**。 > 换句话说:对象本该死掉,却还“活着”。 ### 举例: ```java 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()`: ```java static ThreadLocal buf = new ThreadLocal<>(); public void handle() { buf.set(new byte[1024 * 1024]); try { // 业务逻辑 } finally { buf.remove(); // 不写这行,线程池场景下必然泄漏 } } ``` 更系统的原理(引用链图、InheritableThreadLocal)见 [[05-并发容器与工具类|并发容器与工具类]]。 ## 补充:泄漏和溢出的关系 两者经常一起出现,关系一句话说清:**泄漏是慢性病,溢出是急性发作**。 | 对比 | 内存泄漏 | 内存溢出(OOM) | | --- | --- | --- | | 本质 | 对象该死没死,被引用拖住 | 内存彻底不够,分配失败 | | 症状 | 缓慢恶化:老年代占用持续走高、Full GC 越来越频繁 | 直接抛 Error,程序崩溃 | | 关系 | 持续泄漏 → 迟早引发堆溢出 | 但溢出不一定是泄漏(也可能是堆配太小、瞬时大对象) | 排查时先看 GC 日志里老年代曲线:**只涨不跌**基本就是泄漏;锯齿状正常波动却偶发 OOM,更可能是堆容量或流量问题。 ## 补充:泄漏排查的标准动作 1. `jps` 找到进程号 → `jmap -histo:live ` 看对象数量排行,异常多的大类先锁定; 2. `jmap -dump:live,format=b,file=heap.hprof ` 拉堆快照; 3. 用 MAT(Eclipse Memory Analyzer)打开,看 **Leak Suspects** 报告和 GC Roots 引用链——MAT 会直接告诉你"谁引用着这堆不该活着的对象"; 4. 对照引用链改代码(静态集合清理、remove、注销监听器……)。 GC 底层为什么回收不掉这些对象(可达性分析、GC Roots 是什么),见 [[03-垃圾回收与GC算法|垃圾回收与GC算法]]。 --- ⬅️ [[00-自动垃圾回收机制|自动垃圾回收机制]] 🏠 [[00-Java|00-Java]] ➡️ [[02-内存溢出|内存溢出]]