面试专题

如果系统出现 OOM,应该如何处理?

从线上止损、现场保留、OOM 类型识别、Heap Dump 与 GC 日志分析,到堆、元空间、直接内存、线程和容器内存定位,系统讲解 Java OOM 的处理方法。

难度:深入更新:2026-07-27

如果系统出现 OOM,应该如何处理?

面试回答

系统发生 OOM 后,我会先止损并保留现场,而不是立即反复重启。首先确认影响范围,通过摘除故障实例、切换流量、限流或关闭非核心功能保护系统;同时保存 OOM 错误、应用日志、GC 日志、Heap Dump、容器事件和监控数据。证据保存完成后,再由进程托管系统重启或扩容恢复服务。

然后根据错误类型确定排查方向。Java heap spaceGC overhead limit exceeded 重点分析堆;Metaspace 重点检查类数量和 ClassLoader;Direct buffer memory 重点检查直接内存和 Netty/NIO Buffer;unable to create native thread 重点检查线程数量、线程栈、系统限制和进程原生内存;容器出现 OOMKilled 时,还要检查进程 RSS、容器内存限制以及堆外内存,因为它不一定会留下 Java OOM。

堆内存问题通常使用 Heap Dump、对象直方图和 GC 日志分析。重点查看占用最大的对象、Retained Size、Dominator Tree 和到 GC Roots 的引用链,并结合老年代在 Full GC 后是否持续增长,区分内存泄漏、正常容量不足和单次超大对象分配。修复后需要用相同流量模型验证内存能否回落、GC 是否恢复正常,同时补齐自动 Dump、GC 日志、内存分区监控和容量告警。

一句话总结:

OOM 处理的主线是先止损、再留证、按内存区域分类定位,最后区分泄漏、容量不足和异常大对象,并通过同等流量验证修复。

一张图看懂

Java OOM 止损、留证、分类定位、修复验证与预防流程

详细讲解

一、OOM 不等于 Java 堆内存泄漏

OutOfMemoryError 表示 JVM 无法满足某次资源申请,但发生问题的资源不一定是 Java 堆,也不一定存在泄漏。

常见类型如下:

错误信息主要排查方向
Java heap space堆容量不足、对象泄漏、瞬时大对象
GC overhead limit exceeded堆长期接近耗尽,GC 频繁但回收效果很差
Metaspace类元数据过多、ClassLoader 泄漏、动态生成类失控
Compressed class space压缩类指针空间不足
Direct buffer memoryNIO、Netty 等直接内存申请过多或释放不及时
unable to create native thread线程过多、线程栈占用过大、系统线程限制或原生内存不足
Requested array size exceeds VM limit代码尝试创建超过 JVM 限制的超大数组
容器 OOMKilled、退出码 137进程总内存超过容器限制,不一定出现 Java OOM

因此,第一步不是直接调大 -Xmx,而是先确认错误类型和耗尽的内存区域。

二、第一阶段:先止损

线上故障首先要控制影响范围:

  1. 从负载均衡中摘除异常实例,避免继续接收新流量;
  2. 有健康副本时切换流量或临时扩容;
  3. 对非核心接口限流、熔断或降级;
  4. 暂停可能持续放大内存的批处理、消费任务或大查询;
  5. 确认现场信息已经保存后,再重启异常实例。

是否立即重启取决于可用性和取证条件。如果服务已经不可用,并且有自动生成的 Heap Dump、GC 日志和监控数据,可以尽快重启恢复;如果没有任何证据且仍能安全操作,应先保留必要现场。

不要让已经发生严重 OOM 的进程长期勉强提供服务。此时进程可能持续 Full GC、请求超时,甚至无法创建用于自救的新线程。

三、第二阶段:保留现场

1. 记录基本信息

至少保存:

  • 完整 OOM 错误和调用栈;
  • 故障时间、实例、版本和流量变化;
  • JVM 启动参数;
  • GC 日志;
  • Heap Dump;
  • 堆、元空间、直接内存、线程数和进程 RSS 监控;
  • 容器事件、退出原因和内存限制;
  • 故障前后的发布、配置和任务变更。

2. 提前配置自动 Heap Dump

生产环境应在启动时配置:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dumps

Dump 目录必须有足够空间和写权限,并有清理策略。Heap Dump 可能包含用户数据、令牌或业务字段,下载和保存时要遵守数据安全要求。

对于由进程托管系统负责自动拉起的无状态服务,可以评估:

-XX:+ExitOnOutOfMemoryError

它让 JVM 在 OOM 后退出,由 Kubernetes、systemd 等外部系统重启。是否使用取决于服务容灾能力,不能替代现场保留和根因修复。

3. 常用诊断命令

jcmd <pid> VM.flags
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram
jcmd <pid> Thread.print
jcmd <pid> GC.heap_dump /data/dumps/app.hprof

原生内存跟踪需要在 JVM 启动时开启:

-XX:NativeMemoryTracking=summary

开启后可以查看:

jcmd <pid> VM.native_memory summary

在线执行对象直方图或 Heap Dump 可能引起停顿和大量磁盘 I/O。操作前要确认磁盘空间、实例影响和可用副本,优先使用 OOM 时自动生成的 Dump。

四、先判断是 JVM OOM,还是容器 OOMKilled

这两类问题经常被混淆。

JVM OOM

JVM 自己发现某个资源无法继续分配,通常会在日志中出现具体的 OutOfMemoryError。如果配置了自动 Dump,可能生成 .hprof 文件。

容器 OOMKilled

当进程总内存超过容器的 cgroup 限制时,操作系统可能直接杀掉进程。Kubernetes 中常见现象是:

Reason: OOMKilled
Exit Code: 137

这时进程可能来不及抛出 Java OOM,也可能没有 Heap Dump。容器计算的是进程总内存,不只是 Java 堆:

进程总内存
≈ Java Heap
+ Metaspace
+ Direct Memory
+ 线程栈
+ Code Cache
+ JVM 原生结构
+ 共享库和其他映射

如果 -Xmx 几乎等于容器内存上限,即使堆没有耗尽,也可能因为堆外空间、线程栈或 JVM 自身开销触发 OOMKilled。

五、Java Heap Space 如何分析

堆 OOM 通常要回答三个问题:

谁占用了内存?
为什么没有被回收?
占用是持续增长,还是一次性突增?

1. 先看 GC 趋势

重点观察:

  • 老年代使用量是否持续增长;
  • Full GC 是否越来越频繁;
  • Full GC 后内存能否明显下降;
  • 对象分配速率是否突然升高;
  • 晋升速率和大对象分配是否异常;
  • GC 停顿是否已经影响业务。

典型判断:

Full GC 后老年代基线持续上升
→ 更像对象泄漏或无上限缓存

Full GC 后能够回落,但高峰期仍然耗尽
→ 更像容量不足或瞬时流量过大

内存突然被单次请求快速打满
→ 检查超大数组、文件、结果集或批次

2. 分析 Heap Dump

常用工具包括 Eclipse MAT、JDK Mission Control 和 VisualVM。

重点关注:

  • Histogram:各类对象的实例数和浅堆大小;
  • Dominator Tree:哪些对象支配了大量内存;
  • Retained Size:删除某对象后理论上能够释放多少内存;
  • Path to GC Roots:对象为什么仍然可达;
  • Leak Suspects:工具给出的泄漏候选,需要结合业务验证。

Shallow Size 只表示对象自身大小;Retained Size 还包括只能通过该对象访问的其他对象。排查泄漏时,Retained Size 通常更有价值。

3. 常见堆泄漏来源

  • 没有容量上限或过期机制的本地缓存;
  • 静态集合持续保存业务对象;
  • ThreadLocal 使用后没有清理;
  • 监听器、回调或订阅关系只注册不注销;
  • 队列无界,生产速度长期高于消费速度;
  • 一次性加载超大文件、结果集或消息批次;
  • ORM 上下文、会话对象长期持有实体;
  • 重试任务、Future 或上下文对象长期积压。

六、GC Overhead Limit Exceeded 是什么

这个错误表示 JVM 花费大量时间执行 GC,但回收出的空间很少。它通常是堆即将耗尽的结果,而不是一种独立内存区域。

排查方法与堆 OOM 基本一致:

  1. 查看 Full GC 频率和回收前后占用;
  2. 分析 Heap Dump 中的主要存活对象;
  3. 检查是否存在内存泄漏或缓存、队列无上限;
  4. 检查堆容量是否与真实负载匹配。

只关闭 GC overhead 检查通常没有解决根因,只会让进程以另一种方式继续恶化。

七、Metaspace OOM 如何分析

Metaspace 保存类元数据。发生 OOM 时重点检查:

  • 已加载类数量是否持续增长;
  • 是否频繁动态生成代理类、脚本类或字节码增强类;
  • 是否存在大量无法卸载的 ClassLoader;
  • 热部署、插件化或脚本引擎是否反复创建类加载器;
  • -XX:MaxMetaspaceSize 是否设置得过小。

可以使用:

jcmd <pid> VM.classloader_stats
jcmd <pid> GC.class_histogram

典型泄漏链路是:

业务线程、ThreadLocal、静态对象或第三方库
→ 持有自定义 ClassLoader
→ ClassLoader 持有其加载的全部 Class
→ 类元数据无法卸载

简单调大 Metaspace 只能延迟故障。类数量持续增长时,应定位是哪个 ClassLoader 和动态类生成逻辑没有释放。

八、Direct Buffer Memory 如何分析

直接内存位于 Java 堆之外,常见使用者包括 NIO、Netty、压缩库和网络框架。

重点检查:

  • DirectByteBuffer 数量和容量;
  • -XX:MaxDirectMemorySize
  • Netty 池化内存和引用计数是否正确释放;
  • 是否缓存了大量直接缓冲区;
  • 是否存在大文件或网络数据的堆外缓冲;
  • 进程 RSS 是否增长,但 Java 堆保持稳定。

如果启用了 NMT,可以通过:

jcmd <pid> VM.native_memory summary

查看 JVM 能追踪到的原生内存分类。需要注意,NMT 不能解释所有第三方原生库分配,仍要结合框架指标、进程 RSS 和操作系统工具分析。

九、Unable to Create Native Thread 如何分析

创建线程不仅需要 Java 对象,还需要原生线程和线程栈。常见原因包括:

  • 线程池配置失控或不断创建新线程;
  • 使用 newCachedThreadPool() 等高上限线程池承接阻塞任务;
  • 每个请求或任务都手动创建线程;
  • 线程阻塞、死锁或下游超时,旧线程迟迟不能释放;
  • -Xss 设置过大;
  • 容器原生内存不足;
  • 操作系统或容器的 PID、用户线程数限制。

排查时可以结合:

jcmd <pid> Thread.print
ps -eLf

重点统计线程总数、线程名称分布和线程状态,确认是哪一个线程池或业务模块持续增长。解决方向通常是限制线程数量、使用有界队列、修复阻塞和超时、释放线程资源,而不是只调整系统线程上限。

十、Requested Array Size Exceeds VM Limit

该错误通常说明代码尝试创建一个超过 JVM 可表示或可管理范围的数组,例如:

  • 根据错误长度计算数组容量;
  • 整数溢出后得到异常容量;
  • 一次性把超大文件读入 byte 数组;
  • 一次性拼接超大字符串;
  • 查询结果或批处理缺少分页。

这类问题的重点是定位申请数组的调用栈并修改数据处理方式,例如分页、流式读取、分块处理或限制单次请求大小。单纯增加堆通常无效。

十一、如何区分泄漏、容量不足和异常大对象

现象更可能的原因
Full GC 后老年代基线仍持续上升对象泄漏、无上限缓存或队列
内存随流量上升,流量下降后能够回落正常业务占用或容量不足
某次请求后内存瞬间大幅增长超大对象、文件、结果集或批次
Java 堆稳定,但进程 RSS 持续增长直接内存、线程栈或原生库
类数量和 Metaspace 持续增长ClassLoader 或动态类泄漏
线程数持续增长并最终创建失败线程泄漏、阻塞或线程池失控

判断不能只靠故障时的一张截图。应把 Heap Dump、GC 日志、时间序列监控、流量和发布记录结合起来。

十二、修复时应该做什么

内存泄漏

  • 删除无效引用;
  • 给缓存和集合增加容量、过期和清理策略;
  • 正确清理 ThreadLocal、监听器和回调;
  • 修复 ClassLoader、Buffer 或原生资源释放;
  • 为队列和重试任务设置上限。

容量不足

  • 根据峰值存活对象和分配速率重新评估堆容量;
  • 优化对象生命周期和数据结构;
  • 降低单次批量大小;
  • 扩容实例并重新分配流量;
  • 调整容器上限时为堆外内存保留空间。

异常大对象或突发流量

  • 限制请求体、文件、分页和批次大小;
  • 使用流式处理代替一次性加载;
  • 对大查询增加保护;
  • 在入口处限流和拒绝异常请求;
  • 避免把外部积压无限转移到 JVM 内存。

调大内存可以作为临时止损手段,但必须建立在容量评估之上。存在泄漏时,内存越大通常只是让故障更晚发生。

十三、如何验证修复有效

修复后不能只确认“暂时没有再 OOM”,还要在接近真实的流量模型下验证:

  • 老年代在 GC 后能够回落并保持稳定;
  • 对象数量不再无界增长;
  • Metaspace、直接内存和线程数保持在合理范围;
  • GC 频率、停顿和吞吐符合目标;
  • 进程 RSS 与容器限制之间有足够余量;
  • 高峰、降峰和长时间运行都没有持续爬升;
  • 限流、降级和自动重启策略能够正确触发。

内存泄漏往往需要时间积累,短时间功能测试不能代替持续稳定性验证。

十四、上线前应该建立哪些预防能力

JVM 证据

  • 开启 OOM 自动 Heap Dump;
  • 保存并轮转 GC 日志;
  • 记录 JVM 参数和应用版本;
  • 在需要分析原生内存的服务上评估开启 NMT。

监控告警

  • Heap 各代使用量及 GC 后基线;
  • GC 次数、停顿和回收量;
  • Metaspace 和已加载类数量;
  • Direct Buffer 使用量;
  • 线程总数和各线程池状态;
  • 进程 RSS、容器 Working Set 和 OOMKilled;
  • 队列长度、缓存数量、请求体和批次大小。

容量与保护

  • 为容器总内存预留堆外空间;
  • 缓存、队列、线程池和批次必须有明确上限;
  • 大请求、大文件和大查询需要入口保护;
  • 关键服务具备摘流、限流、降级和快速重启能力;
  • 定期进行内存压测和故障演练。

十五、常见错误做法

1. 一出现 OOM 就把 Xmx 调大

这可能暂时缓解容量不足,但无法修复泄漏、线程失控、直接内存或 ClassLoader 问题。

2. 重启后不保留任何证据

重启会清空最重要的现场。至少应提前配置自动 Dump、GC 日志和监控。

3. 只看 Heap Dump,不看时间趋势

单份 Dump 能说明“当时谁占内存”,但不一定能证明谁在持续增长。需要结合多个时间点、GC 后基线和业务流量。

4. 把容器内存全部分给 Xmx

JVM 还需要 Metaspace、直接内存、线程栈和原生结构,容器必须保留余量。

5. 在线高峰期随意执行重型诊断

Heap Dump、对象直方图等操作可能产生停顿和磁盘压力,应评估实例状态、可用副本和磁盘空间。

十六、排查检查清单

  • OOM 的完整错误信息是什么;
  • 是 JVM 抛出的 OOM,还是容器 OOMKilled;
  • 故障发生在哪个内存区域;
  • 是否保存了 Heap Dump、GC 日志和监控;
  • Full GC 后老年代能否回落;
  • 最大对象及其 Retained Size 是多少;
  • 对象通过什么引用链连接到 GC Roots;
  • 类数量、ClassLoader、直接内存或线程数是否持续增长;
  • 最近是否有发布、配置、流量或批量任务变化;
  • 问题属于泄漏、容量不足还是异常大对象;
  • 修复后是否在真实负载和长时间运行下验证;
  • 是否补齐自动留证、监控告警和容量保护。

参考资料

DISCUSSION

评论与讨论

留下你的想法