线上java内存泄漏需证据链闭环:先确认真泄漏(区分错误类型、gc行为、时间规律),再抓堆/gc/线程三类快照,用mat通过leak suspects和gc roots定位根因,最后聚焦静态集合、threadlocal、监听器、内部类四类高频场景精准修复。

线上 Java 内存泄漏不能靠猜,得靠证据链闭环:从现象确认真泄漏 → 抓取三类现场快照 → 用 MAT 定位根因 → 精准修复代码。关键不是“有没有泄漏”,而是“谁在长期持有不该持有的对象”。
先确认是不是真泄漏
别一见 OOM 就 dump。快速看三件事:
-
错误类型:日志里是
Java heap space(堆泄漏)、Metaspace(类加载爆炸)、Direct buffer memory(NIO 堆外泄漏),还是unable to create new native thread(线程数超限)?不同错误排查路径完全不同 -
GC 行为:执行
jstat -gcutil <pid> 2000</pid>每两秒刷新一次。如果老年代使用率持续 ≥95%、Full GC 频繁但回收量极少,说明对象“钉住”不放 - 时间规律:内存是否随定时任务(如每小时缓存刷新)、特定接口(如导出大 Excel)、或部署后缓慢上涨(每天+200MB)?这直接指向问题代码段
立刻抓取三类关键现场数据
服务卡顿或告警时,第一反应不是重启,而是保留证据:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
-
堆快照:运行
jmap -dump:live,format=b,file=heap.hprof <pid></pid>。加live参数只导出存活对象,避免干扰;若进程已僵死,提前配好-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/自动捕获 -
GC 日志:启动时启用
-Xlog:gc*:file=gc.log:time(JDK 11+)或-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log(旧版)。从中能看清每次 GC 回收了多少、哪些区域没动、是否出现 promotion failure -
线程快照:执行
jstack <pid> > thread.log</pid>。重点检查是否有大量 WAITING 线程堆积(如锁竞争)、或 RUNNABLE 线程长时间执行可疑逻辑(如遍历超大集合、未设 timeout 的远程调用)
用 MAT 快速定位泄漏源头
把 heap.hprof 加载进 Eclipse MAT,核心就两步:
-
打开 Leak Suspects Report:它会自动标出最可疑的几个对象及内存占比。比如显示 “
java.util.ArrayList占用 87% 堆”,立刻点进去看它的 Retained Heap 和 References - 查 GC Roots 引用链:右键可疑对象 → “Path to GC Roots” → 选 “exclude weak/soft references”。重点看是否被静态变量、ThreadLocal、未关闭的监听器或常驻线程池持有
聚焦高频泄漏场景,直击代码根因
90% 的堆泄漏集中在这几类:
-
静态集合无限增长:如
private static final Map<string object> cache = new HashMap();</string>只存不删。解决方案:换WeakHashMap、用 Caffeine/Guava Cache 设置容量和过期策略,或提供显式清理方法 -
ThreadLocal 未清理:尤其在线程池复用场景下,
ThreadLocal.set()后忘记remove(),导致 value 被线程强引用长期滞留。必须在 finally 块中 remove - 监听器/回调未注销:GUI 或事件驱动框架中注册监听器后,对象销毁时未反注册,造成监听器及其上下文对象无法回收
- 内部类持外部类引用:非静态内部类默认持有外部类实例引用。若内部类对象生命周期长于外部类(如传给线程池),会导致外部类“意外”无法释放。改用静态内部类 + 显式弱引用
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










