Java 内存泄漏有哪些典型场景?如何用工具定位?

结论先行:有 GC 不代表不会泄漏——只要“本不该存活的对象”仍被强引用链持有、永远无法回收,就是内存泄漏。典型场景集中在六类:静态/单例集合只增不减、ThreadLocal 用完不清理、连接与流未关闭、监听器/回调未注销、缓存无淘汰策略、线程池任务队列堆积。定位方法统一:观察内存曲线 → 导出堆快照 → 沿 GC Roots 追引用链。

典型泄漏场景表

场景泄漏载体修复要点
静态/单例集合static Map/List 无限写入明确数据生命周期,改用缓存组件
ThreadLocal 未清理Thread → ThreadLocalMap → value在 finally 块中调用 remove()
连接/流未关闭JDBC、IO、Netty ByteBuftry-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(); }
        // → 大对象被线程长期持有,线程池越大泄漏越快
    }
}

定位工具链

  1. jstat -gcutil <pid> 或监控平台观察 Old 区是否“只升不降”;
  2. jmap -dump:format=b,file=heap.hprof <pid> 导出快照,隔一段时间再导一份做对比;
  3. 用 MAT 打开快照,通过 Compare/Histogram 找只增不减的对象,再看 GC Roots 引用链;
  4. 按嫌疑度依次排查:静态字段、ThreadLocal、监听器集合、连接池、缓存容器。

常见追问 / 记忆点

  • 追问:为什么 ThreadLocal 建议 remove 而不是 set(null)?答:remove 真正断开 Entry 引用,且避免下次复用读到残留脏数据。
  • 追问:弱引用能彻底解决 ThreadLocal 泄漏吗?答:不能,value 是强引用,只有 remove 或线程消亡才能真正释放。
  • 记忆点:泄漏 = “无意的强引用”;定位三步曲:看曲线、导快照、追 GC Roots。
笔记加载中…