OOM:Java 堆溢出 java.lang.OutOfMemoryError: Java heap space
现象:‑Xmx 设置的最大堆内存被占满,GC 反复回收,回收后内存依然不够,抛出堆溢出。
一、定位步骤(排查思路)
-
开启堆 dump,发生 OOM 自动导出堆快照
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/xxx/heap.hprof
程序 OOM 时自动生成 hprof 文件,用 MAT(Memory Analyzer Tool)(Eclipse 开源的内存分析工具,专门分析 hprof 堆转储快照文件,用来排查 Java 内存泄漏、堆 OOM)分析。
- MAT 分析 hprof 做三件事 ① 看大对象、支配树:是谁持有大量对象,是谁的引用链不让对象 GC。 ② 看对象数量:哪个类实例数量异常暴涨。 ③ 找泄漏点:对象本应该被回收,但被 GC Roots 强引用死死抱住。
区分两种完全不同情况:
- 内存泄漏(Memory Leak):对象不再业务使用,但存在强引用,无法 GC,内存只涨不降。
- 内存溢出(Memory Overflow):业务本身就需要这么多对象,内存给小了,没有泄漏。
二、情况 1:内存泄漏(最常见)
对象不用了,还被强引用持有,无法 GC。
常见泄漏场景
- 集合(List/Map)全局 static,只 add 不 remove,全局集合不断累积对象。
- ThreadLocal 没有 remove ();线程池线程复用,ThreadLocalMap 还持有对象引用。
- 监听器、回调注册,注销失败,引用没释放。
- 内部类持有外部类引用;匿名内部类捕获外部对象。
- 缓存没有设置淘汰策略(没有 LRU、过期时间),无限膨胀。
解决:
- 不用的对象手动清除集合元素;
- ThreadLocal 用完必须 remove;
- 缓存增加最大容量、过期淘汰(Caffeine、Guava Cache);
- 及时注销监听器回调;
- 避免长生命周期对象持有短生命周期对象。
三、情况 2:没有泄漏,业务确实需要大量对象(堆内存配置过小)
例子:一次性查询百万数据全部加载进 List,全部放到内存。
MAT 看对象都是业务正常需要的,没有不该存活的对象。
解决思路二选一:
- 调高‑Xmx 堆内存,机器物理内存允许的前提下加大堆;
- 不要一次性全加载到内存,做流式处理 / 分页:分批读取、游标分页,不要把全量数据装入 List;使用迭代器逐条处理。
坑:
select * from table查出几十万行封装进 List,直接打爆堆,属于业务写法问题,不是泄漏。
四、代码层面临时优化手段
- 大对象尽量方法内局部变量,方法执行结束,无其他引用就可以 GC;
- 大集合用完置为
list=null,帮助 GC Roots 解除引用(更多是可读性,JVM 会自动识别); - 避免在循环内疯狂 new 大量对象;循环内尽量复用对象,减少对象创建(看业务)。
五、GC 角度分析
观察 GC 日志:
-XX:+PrintGCDetails
如果看到:YGC 频繁,每次回收后可用内存很少;频繁 Full GC,内存几乎释放不下来 → 大概率内存泄漏。 如果一次性突增大量对象直接打满堆,回收效果尚可,只是瞬时需要内存大 → 业务数据量问题。