JVMNotes

第 27 章:OOM 排查

zjc 于 2026-01-27 发布

这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 OOM(OutOfMemoryError)表示 JVM 无法满足某个内存区域的分配或分配相关请求。它不只是“堆不够”,不同 OOM 类型的排查方向完全不同。

27.1 常见类型

OOM 含义 方向
Java heap space Java 堆不足 对象过多、泄漏、堆配置
GC overhead limit exceeded GC 占用时间过高且回收效果差 堆接近耗尽
Metaspace 类元数据空间不足 动态类生成、ClassLoader
Unable to create new native thread 无法创建 Native 线程 线程数、栈大小、OS 限制
Direct buffer memory 直接内存不足 NIO、Netty、堆外池
Requested array size exceeds VM limit 数组超过 JVM 限制 拆分数据
Out of swap space 或 OS 级 OOM 交换空间或系统内存不足 容器、进程、系统

必须先读完整异常信息,再决定排查方向。

27.2 准备证据

生产 OOM 后不要急着重启。应先保留:

  1. OOM 完整异常栈;
  2. GC 日志;
  3. 堆 dump;
  4. JVM 参数;
  5. 容器 memory limit;
  6. 进程 RSS;
  7. 线程数;
  8. 元空间和直接内存指标;
  9. 当时流量和业务日志。

推荐启动配置:

java -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/data/dump \
  -XX:+ExitOnOutOfMemoryError \
  -Xlog:gc*:file=/log/gc.log:time,uptime,level,tags:filecount=10,filesize=50m \
  -jar app.jar

27.3 Java heap space

完整异常:

java.lang.OutOfMemoryError: Java heap space

排查流程:

1. 看是否伴随突发流量或大请求
2. 分析 GC 日志确认回收后占用
3. 打开堆 dump
4. 找 Retained Heap 最大对象
5. Path to GC Roots 定位持有者
6. 结合源码确认生命周期
7. 修复后压测验证

常见原因:

原因 特征
无界缓存 HashMap/List 持续增长
大查询 一次加载百万行
大文件 全量读入内存
批量导出 累积所有结果
ThreadLocal 泄漏 线程池线程持有
队列积压 生产快于消费
堆配置不足 Live 数据确实需要更多

如果堆 dump 显示大量不可达对象,可能是 OOM 时分配过快,GC 来不及完成,而不是传统泄漏。

27.4 GC overhead limit exceeded

该 OOM 表示 JVM 花了大量时间回收,但释放空间很少。通常说明堆里大量对象仍可达,或堆余量太小。

示例判断:

观察期 GC 时间占比 > 98%
回收后堆占用 > 2%

不同版本行为和参数可能调整,不能只靠字面比例记忆。重点仍是看 GC 日志中回收后占用和停顿分布。

处理:

  1. 堆 dump 分析存活对象;
  2. 修复杂质对象或泄漏;
  3. 增大堆只是临时缓解;
  4. 降低分配速率;
  5. 检查缓存上限。

27.5 Metaspace

异常:

java.lang.OutOfMemoryError: Metaspace

常见原因:

  1. 动态代理生成大量类;
  2. Groovy、JSP、脚本引擎重复编译;
  3. 反射调用生成类;
  4. 热部署导致旧 ClassLoader 无法卸载;
  5. 类加载器泄漏。

排查:

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

堆 dump 中查看:

  1. 类数量;
  2. ClassLoader 数量;
  3. 同名类被多少 Loader 加载;
  4. Class 对象 Retained Heap;
  5. 引用链指向谁。

处理方向:

  1. 限制动态类生成;
  2. 复用脚本编译结果;
  3. 检查热部署释放;
  4. 清理全局注册回调;
  5. 合理设置 MaxMetaspaceSize

27.6 无法创建线程

异常:

java.lang.OutOfMemoryError: unable to create new native thread

它不是堆内存不足,而是进程无法再创建 Native 线程。

常见原因:

  1. 线程数达到进程或系统限制;
  2. 线程栈设置过大;
  3. 代码频繁 new Thread;
  4. 容器 PID namespace 或 pids cgroup 限制;
  5. 系统内存不足;
  6. 线程泄漏。

查看:

jcmd <pid> Thread.print > thread.txt
ps -M -p <pid> | wc -l
ulimit -u
cat /proc/<pid>/status | grep Threads

处理:

  1. 改用线程池并设置上限;
  2. 降低 -Xss
  3. 排查未关闭的线程来源; 4| 提高容器 pids 限制,前提是有明确容量评估;
  4. 使用异步 IO 或虚拟线程时重新评估模型。

27.7 直接内存

异常:

java.lang.OutOfMemoryError: Direct buffer memory

常见来源:

组件 典型使用
NIO DirectByteBuffer
Netty PooledDirectByteBuf
RPC 框架 堆外缓冲
缓存 堆外缓存
压缩/序列化 Native 缓冲

排查:

  1. 检查 -XX:MaxDirectMemorySize
  2. 查看直接内存指标;
  3. 检查 Netty arena 指标;
  4. 确认释放路径;
  5. 是否泄漏 DirectByteBuffer;
  6. NMT 是否启用。

启用 Native Memory Tracking:

java -XX:NativeMemoryTracking=summary -jar app.jar
jcmd <pid> VM.native_memory summary

NMT 有轻微开销,生产启用前应评估。

27.8 容器 OOM Kill

JVM 未抛 OOM 但进程消失,可能是被容器运行时或操作系统杀掉。

证据:

Kubernetes Event: OOMKilled, exit code 137
dmesg: Out of memory: Killed process <pid>

原因:

  1. -Xmx 加堆外超过容器 limit;
  2. 线程栈过多;
  3. 直接内存泄漏;
  4. Native 库泄漏;
  5. sidecar 或同 Pod 其他进程消耗内存;
  6. limit 设置过低。

排查:

1. 确认 OOMKilled
2. 查看 Pod memory limit
3. 查看 JVM 最大堆
4. 统计线程数和栈大小
5. 查看直接内存和 Native 内存
6. 调整堆占比或修复泄漏

常见公式:

容器内存 > 堆 + Metaspace + 线程栈 + 直接内存 + CodeCache + Native + 余量

27.9 OOM 应急流程

发现告警
  -> 确认是否仍可访问
  -> 摘除流量
  -> 保存 GC 日志和 dump
  -> 记录监控和事件
  -> 如果无法诊断,手动生成 dump
  -> 重启或替换实例
  -> 修复根因
  -> 灰度验证

如果配置了 ExitOnOutOfMemoryError,进程会快速退出,由编排系统拉起。这样可以避免故障实例继续接收请求,但前提是 dump 已写入本地卷。

本章小结

OOM 排查第一步是识别类型:堆、元空间、线程、直接内存、数组限制还是系统级 OOM。不同类型对应不同工具和参数。堆 OOM 用 GC 日志和堆 dump;Metaspace 看类加载器;线程 OOM 看线程数和系统限制;直接内存看堆外使用;OOMKilled 则要看容器总内存。生产环境必须提前配置 dump 和日志,否则重启后证据消失。

思考题

  1. Java heap spaceDirect buffer memory 的排查差异是什么?
  2. 为什么堆 dump 可能看到大量不可达对象?
  3. Metaspace OOM 常见来源有哪些?
  4. 容器 exit code 137 一定代表 Java OOM 吗?
  5. 如何设计 OOM 应急预案?