Java内存泄漏本质是无用对象因强引用链可达GC Roots而无法被回收,如静态集合、未注销监听器、非静态内部类隐式持外类等,导致堆内存持续消耗直至OOM。

因为这些对象虽已无用,但仍有强引用链连向GC Roots,垃圾回收器判定它们“存活”,无法回收——内存就在这类对象的持续堆积中被无声吃掉。
对象没用却不被回收,根源在引用关系
Java GC不看对象是否“业务上还在用”,只看它是否能从GC Roots(如线程栈、静态变量、本地方法栈等)被可达性分析追踪到。只要存在一条强引用路径,哪怕代码里再也不会访问它,它就被视为“活着”。比如:
- 静态Map缓存了用户会话数据,但从未清理过期条目 → 每个会话对象永远被静态引用持有着
- 注册了监听器却忘记反注册 → 监听器对象强持有Activity或Fragment,而Activity又持有大量视图和资源
- 内部类匿名对象隐式持有外部类实例 → 线程未结束,外部类就一直无法释放
堆空间是有限的,而泄漏对象只增不减
JVM启动时通过-Xmx设定了堆最大容量(比如2GB)。一旦发生泄漏,每次新对象分配,GC只能回收那些真正“不可达”的对象;而泄漏对象始终占据着一部分空间,导致:
- 可用堆内存持续缩水,新生代晋升变多,老年代增长加速
- GC频率升高、单次耗时变长,应用响应迟钝
- 最终老年代填满,Full GC也无法腾出足够空间 → 触发java.lang.OutOfMemoryError: Java heap space
不是GC失灵,而是代码“骗”过了GC
GC本身工作正常,问题出在开发者无意中构建了不该存在的引用链。例如:
立即学习“Java免费学习笔记(深入)”;
- 把临时处理结果放进static List,且没做容量控制或淘汰策略
- 使用ThreadLocal存储大对象,但线程复用(如线程池)后未调用remove()
- 数据库连接、文件流、NIO Buffer等资源未关闭,其关联的堆内缓冲区或包装对象也被长期持有着
泄漏效应会随时间放大,尤其对长周期服务
一个Web服务运行7天,每分钟泄漏1MB,7天就累积约10GB——远超-Xmx设置。而开发环境跑几分钟根本暴露不了问题。这种“温水煮青蛙”式的增长,正是线上OOM常在发布后数小时甚至数天才爆发的原因。


















