Java 内存泄漏有哪些典型场景?如何用工具定位?
结论先行:有 GC 不代表不会泄漏——只要“本不该存活的对象”仍被强引用链持有、永远无法回收,就是内存泄漏。典型场景集中在六类:静态/单例集合只增不减、ThreadLocal 用完不清理、连接与流未关闭、监听器/回调未注销、缓存无淘汰策略、线程池任务队列堆积。定位方法统一:观察内存曲线 → 导出堆快照 → 沿 GC Roots 追引用链。
典型泄漏场景表
| 场景 | 泄漏载体 | 修复要点 |
|---|---|---|
| 静态/单例集合 | static Map/List 无限写入 | 明确数据生命周期,改用缓存组件 |
| ThreadLocal 未清理 | Thread → ThreadLocalMap → value | 在 finally 块中调用 remove() |
| 连接/流未关闭 | JDBC、IO、Netty ByteBuf | try-with-resources 或归还连接池 |
| 监听器/回调未注销 | 观察者集合长期持有对象 | add/remove 对称,必要时用弱引用 |
| 缓存无淘汰策略 | Map 只增不减 | Caffeine/Guava 设置上限与过期 |
| 线程池队列堆积 | 无界阻塞队列 | 有界队列 + 拒绝策略 + 监控 |
泄漏示例:ThreadLocal
public class TraceHolder {
static final ThreadLocal<byte[]> CTX = new ThreadLocal<>();
public void handle() {
CTX.set(new byte[1024 * 1024]); // 线程池线程复用、不退出
// 漏掉 finally { CTX.remove(); }
// → 大对象被线程长期持有,线程池越大泄漏越快
}
}
定位工具链
- 用
jstat -gcutil <pid>或监控平台观察 Old 区是否“只升不降”; - 用
jmap -dump:format=b,file=heap.hprof <pid>导出快照,隔一段时间再导一份做对比; - 用 MAT 打开快照,通过 Compare/Histogram 找只增不减的对象,再看 GC Roots 引用链;
- 按嫌疑度依次排查:静态字段、ThreadLocal、监听器集合、连接池、缓存容器。
常见追问 / 记忆点
- 追问:为什么 ThreadLocal 建议 remove 而不是 set(null)?答:remove 真正断开 Entry 引用,且避免下次复用读到残留脏数据。
- 追问:弱引用能彻底解决 ThreadLocal 泄漏吗?答:不能,value 是强引用,只有 remove 或线程消亡才能真正释放。
- 记忆点:泄漏 = “无意的强引用”;定位三步曲:看曲线、导快照、追 GC Roots。