Java内存泄漏与溢出本质不同,泄漏是对象无法回收导致堆空间耗尽而引发OOM;排查需分三步:看老年代使用率、GC日志及重启后内存曲线;用jmap+MAT分析堆转储定位泄漏源;结合ThreadLocal、静态集合等高危代码模式验证。

Java内存泄漏和溢出不是同一问题,但常被混为一谈。泄漏是“因”,溢出是“果”——对象本该回收却因引用未断而滞留,久而久之堆空间被占满,最终触发OOM。排查必须分清区域、盯住现象、结合工具,不能只等崩溃才动手。
看现象:三步快速确认是否泄漏
不靠猜,靠指标:
- 老年代使用率长期高于90%,且Full GC后回落幅度极小(比如只降2%~5%)
- GC日志频繁出现
GC overhead limit exceeded,或单次Full GC耗时超过1秒 - 服务重启后,内存占用曲线在几小时内就复现上升趋势,而非缓慢爬升
查堆内存:用jmap + MAT定位“谁占最多、谁拽着不放”
这是最常用也最有效的路径:
- 用
jmap -dump:format=b,file=heap.hprof <pid>抓取堆转储(生产环境建议加-XX:+HeapDumpOnOutOfMemoryError自动触发) - 用MAT打开hprof文件,先看Leak Suspects Report,它会标出前几个可疑对象链
- 重点看Dominator Tree,按Retained Heap排序,找byte[]、HashMap、ArrayList、自定义缓存类等高频泄漏载体
- 对可疑对象右键→Path to GC Roots,选exclude weak/soft references,看清是哪个静态变量、ThreadLocal或单例在强持有
盯线程与上下文:别忽略ThreadLocal和Web容器生命周期
很多泄漏藏在线程复用场景里:
立即学习“Java免费学习笔记(深入)”;
- Web应用中,每个请求线程往
ThreadLocal<byte[]>塞数据但没调remove(),线程池复用后内存越积越多 - Spring Context未正确销毁(如监听器注册后未注销)、Filter/Interceptor中持有Request/Session引用未清理
- 用
jstack <pid>看线程状态,若大量线程卡在getBean或doFilter,再结合MAT查对应线程的本地变量
扫代码与配置:从高频泄漏点反向验证
不用全盘审计,聚焦五类高危模式:
-
静态集合:如
private static Map<String, Object> cache = new HashMap<>();——检查是否有清理逻辑,或改用WeakHashMap、Caffeine - 资源未关闭:InputStream、Connection、Statement等——优先用try-with-resources,避免finally里漏写close
- 监听器/回调未注销:尤其在动态注册场景(如事件总线、GUI组件),确保有对应的unregister方法并被调用
-
DirectBuffer滥用:NIO中
allocateDirect()分配堆外内存,需配合Cleaner或显式调用clean(),否则元空间或直接内存OOM -
JVM参数失配:如
-Xmx设太小却跑大数据任务;或-XX:MaxMetaspaceSize未设,导致动态类加载(如Groovy脚本、热更新)撑爆元空间


















