Java heap space溢出极可能是内存泄漏,需通过jstat观察Full GC后老年代持续增长、jmap+MAT分析泄露链条、Arthas动态追踪等方法定位“本该释放却未释放”的引用。

Java heap space 溢出(java.lang.OutOfMemoryError: Java heap space)不一定是内存泄漏,但一旦反复出现、Full GC 后老年代仍持续增长,就极可能是真实泄漏。排查关键不是“看哪个对象多”,而是“找到谁长期持有不该持有的引用”,即泄露链条(retention path)。
确认确实是内存泄漏,而非配置不足或瞬时高峰
先排除误判:
- 用
jstat -gc <pid>观察多次 Full GC 后老年代(OU)是否不降反升,且 Metaspace/CodeCache 稳定——这是泄漏的强信号; - 对比应用负载:相同请求量下,堆使用是否随时间单向爬升?若重启后几小时就复现,基本可锁定泄漏;
- 检查 JVM 参数:-Xmx 是否过小?但注意,调大堆只是掩盖问题,不能解决根源。
用 jmap + MAT 定位泄漏根因对象和强引用链
不要只看“最多实例”或“最大保留集”,要抓“无法被 GC 的活对象及其上游持有者”:
- 触发一次稳定复现后,用
jmap -dump:format=b,file=heap.hprof <pid>生成堆快照(确保应用未卡死,必要时加-F强制); - 用 Eclipse MAT 打开,选 Leak Suspects Report ——它会自动识别疑似泄漏的组件(如静态集合、线程局部变量、监听器未注销等);
- 重点看 Paths from GC Roots:右键可疑对象 → “Merge Shortest Paths to GC Roots” → 勾选 “exclude weak/soft/phantom references”;
- 真正泄露链条往往藏在第 3~5 层:比如一个
ArrayList被某个static CacheManager持有,而该 manager 又被一个已失效但未销毁的Spring Bean(如 prototype scope 但被 singleton 错误注入)间接引用。
结合代码上下文验证引用逻辑是否合理
MAT 给出的是“技术路径”,需人工判断“业务合理性”:
立即学习“Java免费学习笔记(深入)”;
- 查到某
ThreadLocal<Map>持有大量对象?检查是否在 Filter/Interceptor 中 set 后未 remove,尤其注意异步线程或线程池复用场景; - 发现某个监听器(Listener)或回调(Callback)实例数暴涨?确认注册后是否有对应 unregister / destroy 逻辑,特别关注动态代理、AOP、事件总线(如 Spring Event、Guava EventBus);
- 看到大量
byte[]或String被org.apache.commons.io.IOUtils类持有?可能是流未关闭 + 静态缓存共用,导致缓冲区堆积; - Spring 应用中常见陷阱:@EventListener 注册在 prototype bean 上、@Scheduled 方法所在类被意外提升为 singleton、Feign Client 的 decode 缓存未清理。
用 Arthas 实时追踪对象创建与引用源头(进阶)
当泄漏周期长、dump 太大或无法复现时,用 Arthas 动态观测更高效:
-
monitor -c 5 com.yourpackage.YourService doSomething查高频方法; -
watch com.yourpackage.CacheUtil put '{params,returnObj}' -x 3观察缓存写入参数与返回; - 最关键:
trace com.yourpackage.DataProcessor buildResult+ 结合ognl查当前线程的 ThreadLocal 内容,定位哪次调用埋下了泄漏种子; - 配合
jvm和dashboard命令,实时比对 Eden/Survivor 转移率、Old Gen 使用趋势,缩小怀疑范围。
不复杂但容易忽略:90% 的真实泄漏都源于“本该释放却没释放”的引用,而不是算法本身。盯住 GC Roots 到对象之间的那几跳,逐层问“这个持有关系,在业务生命周期里是否还应存在”,答案为否的地方,就是泄露点。


















