OOM 引起原因以及如何排查?

OOM 引起原因以及如何排查?

面试考察点

原理理解:面试官不仅仅是想知道 OOM 是什么,更是想看你是否清楚 OOM 会发生在 JVM 的哪些区域,以及每个区域 OOM 的触发条件。这块能答清楚,说明你对 JVM 内存模型有真理解。

排查能力:这是重中之重。面试官想知道你遇到 OOM 之后有没有一套系统的排查方法论,而不是上来就瞎猜。会不会看日志、会不会用 jmap、会不会分析堆转储文件,这些才是区分 "背过八股文" 和 "真正处理过线上问题" 的分水岭。

预防意识:回答中能不能主动提到预防措施(比如加监控、配参数、代码 Review),体现了你是不是一个有生产意识的工程师。

核心答案

OOM 根据发生的内存区域不同,主要有以下几种类型:

OOM 类型

错误信息

根本原因

高发场景

堆溢出

Java heap space

堆内存不足,对象太多回收不了

内存泄漏、大对象、集合只增不减

栈溢出

StackOverflowError

栈深度超限

递归调用过深、方法循环调用

方法区溢出

Metaspace / PermGen space

类加载过多

动态代理、CGLIB、JSP 热部署

直接内存溢出

Direct buffer memory

堆外内存不足

NIO 的 DirectByteBuffer 使用不当

GC 开销超限

GC overhead limit exceeded

GC 回收效率太低

堆几乎满了,98% 以上时间在 GC

排查 OOM 的核心思路:保留现场 → 定位区域 → 抓取快照 → 分析根因 → 修复验证。

深度解析

一、OOM 的常见原因

1. 堆溢出——最常见

堆溢出占了线上 OOM 的 80% 以上,主要有两种情况:

内存泄漏:对象已经不用了,但 GC 无法回收。比如 static 集合一直往里塞数据但从来不清理,或者内部类持有外部类引用导致外部类无法回收。

内存溢出:确实需要这么多内存,但堆给的不够。比如一次性加载了一个 500MB 的文件到内存。

// 典型内存泄漏:静态集合只增不减

public class OomDemo {

// static 集合生命周期跟类一样长,GC 永远回收不了里面的对象

private static final List CACHE = new ArrayList<>();

public void addToCache(Object obj) {

CACHE.add(obj); // 一直往里加,从不移除 → 最终堆爆掉

}

}

// 典型一次性加载大对象

public void loadBigFile() throws IOException {

// 一次性把整个文件读进内存,文件一大就 OOM

byte[] data = Files.readAllBytes(Paths.get("huge_file.dat"));

}

上面两段代码展示了堆溢出的两种典型场景。第一段是内存泄漏,static 集合持有对象引用导致 GC 无法回收;第二段是内存溢出,虽然对象用完就可以回收,但瞬时内存需求超过了堆大小。

2. 栈溢出

栈溢出相对好定位,看异常堆栈就能一眼看到递归调用链。

// 经典递归无终止条件 → StackOverflowError

public int fibonacci(int n) {

return fibonacci(n - 1) + fibonacci(n - 2); // 没有 n <= 1 的终止条件

}

3. 方法区(元空间)溢出

这块很多人容易忽略。Spring、MyBatis 这类大量使用动态代理的框架,运行时会生成大量类,如果元空间没限制好大小,就可能溢出。

// CGLIB 动态代理疯狂生成类 → Metaspace OOM

public class MetaspaceOomDemo {

public static void main(String[] args) {

while (true) {

Enhancer enhancer = new Enhancer();

enhancer.setSuperclass(MetaspaceOomDemo.class);

enhancer.setUseCache(false);

enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) ->

proxy.invokeSuper(obj, args1));

enhancer.create(); // 每次创建都生成一个新类

}

}

}

4. 直接内存溢出

NIO 使用 DirectByteBuffer 分配堆外内存,不受 -Xmx 限制,但受 -XX:MaxDirectMemorySize 和物理内存限制。这块溢出时错误信息可能不太明确,排查起来相对棘手。

二、OOM 排查完整流程

这才是面试官最想听的部分,也是拉开差距的地方。

上图展示了 OOM 排查的完整 5 步流程,下面展开每一步的关键操作。

第一步:保留现场

这是最关键也最容易被忽视的一步。等 OOM 发生了再去想怎么抓现场,就来不及了。必须在 JVM 启动参数里提前配好:

# 必配参数:OOM 时自动 dump 堆内存

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/data/logs/heapdump.hprof

# 建议同时配 GC 日志

-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags

我就吃过这个亏,有次线上 OOM,没配自动 dump,只能干瞪眼重启,重启后又好了,根因找不到。所以这个参数 每个线上应用都必须配上,没商量。

第二步:确认 OOM 类型

看日志里的错误信息,快速判断是哪个区域出了问题,缩小排查范围。具体的类型对应关系上面流程图里已经列了。

第三步:命令行工具快速诊断

# 1. 找到 Java 进程

jps -l

# 2. 查看 GC 情况,观察各代内存变化

jstat -gcutil 1000 10 # 每秒输出一次,共输出 10 次

# 3. 查看堆中对象统计(按占用空间排序)

jmap -histo | head -20

# 4. 手动生成堆转储文件

jmap -dump:format=b,file=heap.hprof

# 5. 查看线程堆栈(排查是否有死锁或异常线程)

jstack

如果生产环境允许在线诊断,Arthas 是更好的选择,不用重启应用就能排查:

# Arthas 一键诊断

dashboard # 实时查看线程、内存、GC 概览

heapdump # 生成堆转储

thread -n 3 # 查看 CPU 占用最高的 3 个线程

memory # 查看各内存区域使用情况

Arthas 这玩意儿确实好用,我们团队现在线上排查基本都靠它,比 jmap、jstack 那一套方便太多。

第四步:分析堆转储文件

拿到 .hprof 文件后,用 MAT(Memory Analyzer Tool) 分析,这是排查内存泄漏的核心武器。

MAT 会自动生成一份 Leak Suspects Report(泄漏嫌疑报告),告诉你哪些对象占用了大量内存且无法被回收。重点关注:

Dominator Tree(支配树):按内存占用从大到小排列,一眼看到哪个对象最 "吃内存"

Leak Suspects(泄漏嫌疑):MAT 自动分析的内存泄漏嫌疑点

GC Roots 引用链:从嫌疑对象追溯到 GC Root,找到是谁 "拽着" 这个对象不放

上图展示了 MAT 分析的核心思路——沿着 GC Roots 引用链往下追踪,找到哪个 "锚点" 导致大量对象无法被回收。通常你会发现一个 static 集合或者一个生命周期很长的对象,里面塞满了本该被回收的业务数据。

第五步:定位根因并修复

常见的修复手段:

问题类型

修复方式

集合只增不减

用完及时 remove / clear,或用 WeakHashMap

大对象一次性加载

改为流式处理,分批读取

线程池无界队列

改为有界队列,配合适的拒绝策略

元空间溢出

调大 -XX:MaxMetaspaceSize,检查是否有类泄漏

直接内存溢出

检查 DirectByteBuffer 是否及时释放

堆本身不够大

调大 -Xmx,但先确认不是泄漏

三、预防 OOM 的最佳实践

排查是事后补救,预防才是正道。分享几个我们团队在生产环境积累的经验:

# 1. JVM 参数标配模板

-Xms2g -Xmx2g # 堆大小固定,避免动态扩缩容

-XX:+HeapDumpOnOutOfMemoryError # OOM 自动 dump

-XX:HeapDumpPath=/data/logs/heapdump.hprof

-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m # 元空间限制

-XX:+UseG1GC # G1 收集器对大堆更友好

代码层面:集合类设初始容量避免频繁扩容;大文件用流处理;ThreadLocal 用完必须 remove;静态集合定期清理或用弱引用。

监控层面:接入 Prometheus + Grafana 监控 JVM 内存、GC 频率;配告警阈值,比如堆使用率超过 85% 就告警。

测试层面:上线前用 JMeter 压测,观察内存走势是否持续上升不回落。

面试高频追问

追问:内存泄漏和内存溢出有什么区别?

内存泄漏是对象无法被 GC 回收(有引用链连着 GC Root),导致可用内存越来越少,最终可能引发溢出。内存溢出是确实需要这么多内存但给的不够。一句话:泄漏是 "不该留的没清掉",溢出是 "确实装不下了"。

追问:jmap 在生产环境使用有什么风险?

jmap -histo 在 JDK 8 及之前会触发 Full GC(加 live 参数),可能导致应用暂停。jmap -dump 生成堆转储时会暂停应用(STW),堆越大暂停越久。生产环境建议优先用 Arthas,或者用 -XX:+HeapDumpOnOutOfMemoryError 提前配好自动 dump。

追问:WeakHashMap 和 HashMap 的区别?为什么 WeakHashMap 能防内存泄漏?

WeakHashMap 的 key 是弱引用,当 key 对象没有强引用指向时,GC 可以直接回收该 key,对应的 entry 也会在下次操作时被清除。适合做缓存场景,防止 key 对象一直被引用导致泄漏。

常见面试变体

"线上 OOM 了怎么排查?说一下你的排查思路"

"如何排查 Java 应用的内存泄漏?"

"jmap、jstack、jstat 分别有什么用?"

"Arthas 用过吗?说说常用的命令"

记忆口诀

排查五步:保现场(配 dump) → 看类型(错误信息) → 用命令(jmap/jstat) → 分析 dump(MAT) → 追根因(GC Roots 链)

OOM 分类:堆(最常见)、栈(递归)、元空间(动态代理)、直接内存(NIO)

总结

OOM 排查的核心就三件事:提前配好自动 dump 保留现场,用命令行工具快速定位问题区域,用 MAT 分析堆转储找到泄漏根因。面试时从 "原因分类" 到 "排查流程" 再到 "预防措施" 三层递进地回答,基本能覆盖面试官的所有追问。最后别忘了提一嘴 Arthas,这个加分项很多面试官都认可。

相关推荐

黑色配什么颜色好看?:三个黑色最佳配色方案
塗鴉藝術
365bet在线网址

塗鴉藝術

08-08 720
青藏高原所构建冰川失稳灾害清单