ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

彻底吃透 Java OOM 异常:从原理、场景、排查到解决方案全攻略

2026/8/16 4:08:24 拓冰建站 浏览量
彻底吃透 Java OOM 异常:从原理、场景、排查到解决方案全攻略

在 Java 后端开发里,OOM(OutOfMemoryError)绝对是线上最让人 “头皮发麻” 的问题之一。它不像普通异常那样好定位,往往服务跑着跑着突然崩掉,日志寥寥几句,让人无从下手。

这篇文章就把 OOM 讲得通透、详细、能直接用于实战,从是什么、为什么、有哪些类型、怎么排查、怎么根治,一次性讲全。


一、什么是 OOM?

OOM 全称OutOfMemoryError,意思是:JVM 无法为新对象分配内存,且垃圾回收也无法腾出足够空间,最终抛出的致命错误。

重点理解三句话:

  1. OOM 是Error,不是 Exception,一旦出现,应用基本不可用。
  2. 不是 “内存不够”,而是内存被占满、且回收不了
  3. 90% 的 OOM 不是 JVM 内存太小,而是代码问题

二、OOM 出现的根本原因

一句话总结:程序不断创建对象 → 对象一直被引用 → GC 无法回收 → 内存被撑爆 → 新对象无处安放 → OOM

常见根源:

  • 内存泄漏:对象不用了,但一直被持有,GC 无法回收
  • 一次性加载过多数据:不分页查全表、大文件全量读入内存
  • 资源未关闭:连接、流、线程池滥用
  • 静态集合无限添加对象
  • JVM 参数不合理:堆、元空间设置过小
  • 动态生成大量类:导致元空间溢出

三、JVM 内存区域与 OOM 的关系

Java 虚拟机把内存分成不同区域,每个区域都可能 OOM

  1. Heap 堆:存放对象实例 →最常见 OOM
  2. Metaspace 元空间:存放类信息、方法、常量、代理类
  3. 虚拟机栈:方法调用栈 → 栈溢出
  4. 本地方法栈:Native 方法使用
  5. 直接内存:NIO 使用的堆外内存
  6. 程序计数器:唯一不会 OOM 的区域

下面我们逐个讲最常出现的 6 种 OOM


四、6 种最经典 OOM 场景详解

1. 堆内存溢出(Java heap space)

错误信息:

java.lang.OutOfMemoryError: Java heap space

原因:堆内存被占满,不断创建对象,且都被强引用,无法 GC。

典型代码:

List<Object> list = new ArrayList<>(); while (true) { list.add(new Object()); }

真实业务场景:

  • 不分页查询数据库全表数据
  • 大文件一次性全部加载到内存
  • 死循环创建对象
  • 静态集合无限添加数据

这是线上最常见的 OOM


2. 元空间溢出(Metaspace)

错误信息:

java.lang.OutOfMemoryError: Metaspace

原因:加载的类太多,元空间放不下。

常见场景:

  • 大量动态代理(CGLIB、MyBatis、Spring 代理)
  • 反射频繁生成类
  • 热部署过多
  • 自定义类加载器未正确释放

JVM 参数:

-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m

3. 直接内存溢出(Direct buffer memory)

错误信息:

java.lang.OutOfMemoryError: Direct buffer memory

原因:NIO 使用ByteBuffer.allocateDirect()申请的堆外内存耗尽。

常见场景:

  • Netty、MINA 等 NIO 框架
  • 大文件上传下载、流媒体处理

4. 栈溢出(StackOverflowError)

错误信息:

java.lang.StackOverflowError

原因:方法调用层级太深,栈帧数量超过栈最大深度。

典型代码:

public void loop() { loop(); // 无限递归 }

业务场景:

  • 递归没有出口
  • 循环调用 A→B→A→B

5. 无法创建本地线程

错误信息:

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

原因:每个线程都占用栈内存,线程数超出系统限制

场景:

  • 代码里随便new Thread()
  • 线程池参数不合理,maximumPoolSize 过大
  • 高并发下无节制创建线程

6. GC 开销超限

错误信息:

java.lang.OutOfMemoryError: GC overhead limit exceeded

原因:JVM 98% 的时间在做 GC,却只回收不到 2% 内存,进入 “累死状态”。

本质:内存快空了,又不断产生垃圾


五、内存泄漏 vs 内存溢出

这是面试 + 实战必考点。

1. 内存溢出(OOM)

  • 内存真的不够用了
  • 一次性加载太多对象
  • 加内存可以临时缓解

2. 内存泄漏(Memory Leak)

  • 对象不用了,但一直被引用,GC 无法回收
  • 内存慢慢被吃掉
  • 最终一定会导致 OOM
  • 加内存没用,必须改代码

最常见泄漏场景:

  • static List/Map 不断添加对象
  • ThreadLocal 没remove()
  • 数据库连接、IO 流未关闭
  • 内部类持有外部类引用
  • 缓存不加过期、淘汰机制

六、线上 OOM 标准排查流程

第一步:必须开启 OOM 自动 dump(重中之重)

在 JVM 启动参数里加:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/heap.hprof

OOM 时会自动导出内存快照,这是定位问题的关键


第二步:分析 dump 文件

工具:

  • Eclipse MAT(最强大)
  • JProfiler
  • Arthas(阿里开源,线上神器)

重点看 4 点:

  1. 哪个类的对象最多
  2. 谁在引用这些对象
  3. 是不是业务对象(是 → 代码问题)
  4. GC Roots 链在哪里

第三步:配合命令行工具定位

jstat -gc 进程ID 1000 10 # 看GC情况 jmap -heap 进程ID # 看堆配置 jmap -histo 进程ID # 看对象数量 arthas-boot.jar # 阿里Arthas,一键排查

判断内存泄漏最简单方式:每次 Full GC 后,内存水位不下降 → 100% 内存泄漏


七、OOM 通用解决方案

1. 代码层面(根治)

  • 数据库查询必须分页
  • 大文件使用流式读取,不一次性加载
  • 资源用完必须关闭(连接、流、ThreadLocal)
  • 慎用 static 集合
  • 线程池标准化,不无限创建线程
  • 缓存加过期、淘汰、降级策略

2. JVM 参数优化

-Xms2g -Xmx2g # 堆初始值和最大值 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+HeapDumpOnOutOfMemoryError

3. 架构层面

  • 消息队列削峰
  • 分布式计算,压力分散
  • 大文件上传走分片
  • 热点数据放缓存,不全部放内存

八、总结(一句话记住 OOM)

  • OOM =内存满了,新对象没地方分配
  • Java heap space最常见,大多是没分页、内存泄漏
  • Metaspace溢出 = 类太多
  • Direct buffer溢出 = NIO/Netty 问题
  • StackOverflow= 递归 / 调用太深
  • 无法创建线程= 线程太多
  • 排查核心:dump 文件 + MAT/Arthas
  • 90% OOM 都是代码问题,不是 JVM 内存太小