什么是 OOM?深度解析Java内存溢出核心概念与原理(上)
作者 :Weisian
发布时间:2026年2月13日

一、开篇:OOM 是程序写给架构师的"遗书"
场景引入 :
你是否曾在线上环境中突然收到这样的告警?
java.lang.OutOfMemoryError: Java heap space
或应用毫无征兆地崩溃,或接口响应越来越慢最终超时,日志中只留下一行:
Process exited with code 137 (OOMKilled)
观点输出:OOM 不是简单的"内存不够",而是 JVM 在资源枯竭时抛出的最严重错误。读懂 OOM,是 Java 高手的必修课。

二、核心概念:OOM、内存溢出、内存泄漏,别再傻傻分不清
在开始之前,必须先厘清三个极易混淆的核心概念:OOM、内存溢出、内存泄漏,这是解决问题的基础。
1. OOM 定义
OOM(OutOfMemoryError)是JVM在无法分配足够的内存空间来满足对象创建或内存申请需求时,抛出的严重运行时异常,直接导致应用进程崩溃或功能不可用。
简单来说:JVM的某块内存区域被耗尽,且无法通过垃圾回收(GC)释放足够空间,后续内存申请无法满足,就会抛出OOM。
📌 重要区分 :OOM 是 Error 而非 Exception ,继承自
VirtualMachineError,通常表示程序无法恢复的严重问题。

2. 核心概念:内存溢出 vs 内存泄漏
很多人会将两者混为一谈,但它们是因果关系(内存泄漏往往导致内存溢出),本质有天壤之别。
| 概念 | 核心定义 | 关键特征 | 是否可通过GC解决 | 生产环境占比 |
|---|---|---|---|---|
| 内存溢出(Memory Overflow) | 内存需求 > 可用内存空间(配置或物理限制) | 直接抛出OOM,内存空间不足以容纳新对象 | 否(即使GC回收,也无法满足内存需求) | 约30% |
| 内存泄漏(Memory Leak) | 无用对象(不再被业务使用)被强引用持有,无法被GC回收,持续占用内存 | 内存使用率持续攀升,无明显下降,最终引发OOM | 否(强引用未释放,GC无法标记为垃圾) | 约70%(生产环境OOM主因) |
📌 通俗比喻:
- 内存溢出:你只有10㎡的房间(可用内存),却要放15㎡的家具(内存需求),直接放不下(无论怎么整理都没用);
- 内存泄漏:你房间里堆满了用不上的垃圾(无用对象),但这些垃圾被钉在了地上(强引用持有),无法清理,慢慢占满整个房间,最终新家具(新对象)无法放入,引发溢出。
✅ 关键结论:解决OOM的核心,是先排查是否存在内存泄漏(70%概率),再优化内存配置与代码(30%概率)。

3. OOM 的共性触发条件
无论哪种OOM类型,触发的核心条件都包含两点:
- 某块内存区域的内存占用持续增长,超出其最大限制;
- 垃圾回收(GC)无法释放足够的可用空间,无法满足后续的内存申请。
三、JVM 运行时数据区与 OOM 的对应关系
核心逻辑:不同内存区域耗尽,抛出不同的 OOM 错误信息。看懂错误信息 = 定位到内存区域。
| JVM 内存区域 | 存储内容 | 对应 OOM 错误信息 | 核心诱因 |
|---|---|---|---|
| 堆(Heap) | 对象实例、数组 | Java heap space |
泄漏/配置不足 |
| 方法区/元空间 | 类元数据、常量池 | Metaspace (JDK8+) / PermGen space |
动态类加载过多 |
| 虚拟机栈 | 栈帧(局部变量、操作数栈) | StackOverflowError(非OOM,但常并列讨论) |
递归过深 |
| 本地方法栈 | 本地方法栈帧 | StackOverflowError |
JNI 递归 |
| 直接内存 | NIO 缓冲区 | Direct buffer memory |
未释放/配置过小 |
| 系统内存 | JVM 进程内存 | Out of swap space / kill -9 |
物理内存+交换空间耗尽 |
| 线程栈 | 线程自身 | Unable to create new native thread |
线程数超限 |
🧠 小结 :看一眼 OutOfMemoryError 后面的关键词,就知道 JVM 的哪块"地皮"被占满了。

四、7 种 OOM 类型速览与原理
1. Java heap space ------ 堆内存溢出
-
日志特征 :
java.lang.OutOfMemoryError: Java heap space -
内存区域:堆
-
触发原理 :
- 泄漏型:静态集合/全局缓存无限增长,GC无法回收。
- 瞬时型 :单个超大对象(如几GB数组)超出
-Xmx。
-
片段示例 :
java// 泄漏型:静态List无限添加 static List<byte[]> CACHE = new ArrayList<>(); CACHE.add(new byte[1*1024*1024]); // 不断添加,永不释放 -
识别特征:堆内存占用持续攀升 / 单次申请超大数组。
2. StackOverflowError ------ 栈内存溢出
-
日志特征 :
java.lang.StackOverflowError -
内存区域:虚拟机栈(线程私有)
-
触发原理:单个线程请求的栈深度 > 虚拟机栈深度(最常见:无限递归)。
-
片段示例 :
javavoid a() { a(); } // 无终止条件,递归调用自身 -
识别特征:异常栈反复出现同一行代码。
3. Metaspace ------ 元空间溢出
-
日志特征 :
java.lang.OutOfMemoryError: Metaspace -
内存区域:元空间(JDK8+)
-
触发原理:大量动态生成代理类、枚举类、热部署类加载器泄漏,类元数据占满元空间。
-
片段示例 :
java// 通过CGLIB/ByteBuddy无限生成子类 Enhancer.create(Business.class, new MethodInterceptor(...)); -
识别特征 :
jstat -class显示加载类数持续暴增。
4. GC overhead limit exceeded ------ GC开销超限
-
日志特征 :
java.lang.OutOfMemoryError: GC overhead limit exceeded -
内存区域:堆(间接)
-
触发原理:JVM 保护机制。98% 的时间在做 GC,但每次回收不到 2% 的堆内存。
-
片段示例 :
java// 短命大对象频繁进入老年代,频繁Full GC但回收极少 while(true) { new byte[2*1024*1024]; } -
识别特征:GC日志中 Full GC 极其频繁,每次回收后内存瞬间又满。
5. Direct buffer memory ------ 直接内存溢出
-
日志特征 :
java.lang.OutOfMemoryError: Direct buffer memory -
内存区域:操作系统本地内存(堆外)
-
触发原理 :
ByteBuffer.allocateDirect()分配了大量堆外内存,未及时释放/显式回收。 -
片段示例 :
javawhile(true) { ByteBuffer.allocateDirect(1*1024*1024); } -
识别特征:堆内存占用很低,但进程RES(常驻内存)极高,应用仍OOM。
6. Unable to create new native thread ------ 线程创建失败
-
日志特征 :
java.lang.OutOfMemoryError: unable to create new native thread -
内存区域:操作系统线程资源
-
触发原理 :进程线程数超出
ulimit -u限制,或线程数 * 栈大小(-Xss)超出虚拟内存。 -
片段示例 :
javawhile(true) { new Thread(() -> Thread.sleep(Long.MAX_VALUE)).start(); } -
识别特征 :
top -H -p pid显示线程数达数千,且无法创建新线程。
7. Out of swap space ------ 交换空间耗尽
-
日志特征 :
java.lang.OutOfMemoryError: Out of swap space? -
内存区域:物理内存 + 交换空间
-
触发原理:操作系统物理内存+Swap 全部耗尽,JVM 向 OS 申请内存失败。
-
片段示例 :
java// 持续持有大对象,耗尽系统内存 List<byte[]> list = new ArrayList<>(); while(true) { list.add(new byte[100*1024*1024]); } -
识别特征 :
free -h显示available极低,swap used满,系统卡死。
五、OOM 类型速查表
| OOM 类型 | 内存区域 | 常见诱因 | 关键参数 |
|---|---|---|---|
Java heap space |
堆内存 | 内存泄漏、缓存过大、配置过小 | -Xmx, -Xms |
GC overhead limit exceeded |
堆内存 | 短命大对象、晋升策略不当 | -XX:GCTimeLimit, -XX:PretenureSizeThreshold |
Metaspace |
元空间 | 动态类生成、热部署、类加载器泄漏 | -XX:MaxMetaspaceSize |
Direct buffer memory |
直接内存 | NIO Buffer 未释放、Netty 使用不当 | -XX:MaxDirectMemorySize |
Unable to create new native thread |
栈内存 | 线程泄漏、线程池配置不当 | -Xss, ulimit -u |
Requested array size exceeds VM limit |
堆内存 | 超大数组分配 | 无(JVM 硬限制) |
Out of swap space |
系统内存 | 内存泄漏蔓延到系统级 | 系统交换分区设置 |

六、内存泄漏:OOM 的主要元凶
1. 什么是内存泄漏?
内存泄漏 指程序无意中持有不再需要的对象引用,导致这些对象无法被 GC 回收,内存使用持续增长直至 OOM。
⚠️ Java 内存泄漏 ≠ C++ 内存泄漏
- C++:忘记调用
delete,内存永久丢失- Java:对象被意外引用,GC 无法回收
2. 内存泄漏的四大特征
- 内存持续增长:即使业务量稳定,内存占用仍线性增长
- Full GC 无法释放:多次 Full GC 后,堆内存占用不下降
- Old Gen 持续增长:老年代使用率只增不减
- 最终触发 OOM:在运行一段时间后必然发生

3. 八大经典内存泄漏场景
场景1:静态集合类泄漏(最常见)
java
public class StaticCollectionLeak {
// 静态集合生命周期 = JVM 生命周期
private static final Map<String, Object> CACHE = new HashMap<>();
public void addToCache(String key, Object value) {
CACHE.put(key, value); // 对象永远无法回收
}
public void removeFromCache(String key) {
CACHE.remove(key); // 必须显式移除
}
}
场景2:单例模式持有大对象
java
public class SingletonLeak {
private static SingletonLeak instance;
private byte[] hugeData; // 10MB 数据
private SingletonLeak() {
hugeData = new byte[1024 * 1024 * 10];
}
public static SingletonLeak getInstance() {
if (instance == null) {
instance = new SingletonLeak();
}
return instance; // 单例永不释放
}
// 即使hugeData不再需要,也无法释放
public void clearData() {
hugeData = null; // 仅释放引用,单例本身还在
}
}
场景3:监听器/回调未注销
java
public class ListenerLeak {
private List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
}
// 缺少removeListener方法!
// public void removeListener(EventListener listener) {
// listeners.remove(listener);
// }
public void fireEvent() {
for (EventListener listener : listeners) {
listener.onEvent();
}
}
}
// 使用示例:每个请求创建一个监听器,但从不移除
public class Client {
public void processRequest() {
ListenerLeak service = new ListenerLeak();
service.addListener(new EventListener() {
@Override
public void onEvent() {
// 业务逻辑
}
});
// 请求结束后,EventListener仍被service持有
}
}
场景4:ThreadLocal 使用不当
java
public class ThreadLocalLeak {
// ThreadLocal 本身是静态的,生命周期长
private static final ThreadLocal<byte[]> threadLocalData =
ThreadLocal.withInitial(() -> new byte[1024 * 1024]); // 每个线程1MB
public void process() {
byte[] data = threadLocalData.get();
// 使用数据...
}
// 线程池场景:线程复用,ThreadLocal 值累积
public void threadPoolLeak() {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
process();
// 线程结束后应清理,但常被遗忘
// threadLocalData.remove();
});
}
}
}
场景5:连接未关闭(数据库、文件、网络)
java
public class ResourceLeak {
public void readFile(String path) {
try {
FileInputStream fis = new FileInputStream(path);
// 读取文件...
// 忘记关闭:fis.close();
} catch (IOException e) {
e.printStackTrace();
}
// FileInputStream 最终会调用 finalize(),
// 但依赖 GC 时机,可能很久才释放
}
}
场景6:内部类持有外部类引用
java
public class OuterClass {
private byte[] largeData = new byte[1024 * 1024 * 100]; // 100MB
public class InnerClass {
public void doSomething() {
// 隐式持有 OuterClass.this 引用
System.out.println("Accessing outer data");
}
}
public InnerClass getInner() {
return new InnerClass();
}
}
// 使用问题:即使不再需要OuterClass,InnerClass仍持有引用
public class LeakClient {
private List<OuterClass.InnerClass> inners = new ArrayList<>();
public void leak() {
OuterClass outer = new OuterClass(); // 占用100MB
inners.add(outer.getInner()); // 保存内部类
// outer 超出作用域,但 inner 仍持有其引用
// outer 的100MB无法释放
}
}
场景7:缓存无过期策略
java
public class CacheLeak {
// Guava Cache 示例:无大小限制
private Cache<String, byte[]> cache = CacheBuilder.newBuilder()
// 缺少以下限制:
// .maximumSize(1000)
// .expireAfterWrite(10, TimeUnit.MINUTES)
.build();
public void put(String key, byte[] data) {
cache.put(key, data); // 可能无限增长
}
}
场景8:JNI 本地内存泄漏
java
public class NativeMemoryLeak {
static {
System.loadLibrary("nativeLib");
}
// 本地方法分配内存
public native long allocateNativeMemory(int size);
// 本地方法释放内存
public native void freeNativeMemory(long ptr);
public void leak() {
long ptr = allocateNativeMemory(1024 * 1024); // 1MB
// 业务逻辑...
// 忘记调用:freeNativeMemory(ptr);
}
}
七、OOM 排查三板斧(原理篇·不做实操)
1. 三板斧步骤
- 看日志:错误信息明确指向内存区域(Heap/Metaspace/Direct...)。
- 看监控:内存趋势图(持续上涨 → 泄漏;瞬间高峰 → 大对象)。
- 看变更:最近是否上线新功能、调整JVM参数、升级中间件。

2. 必备工具清单
| 工具 | 用途 | 适用阶段 |
|---|---|---|
| jps/jcmd | 查看Java进程 | 初步定位 |
| jstat | 监控GC和内存 | 实时监控 |
| jmap | 堆转储、直方图 | 深度分析 |
| jstack | 线程转储 | 关联分析 |
| MAT | 堆转储分析 | 根本原因分析 |
| JProfiler | 实时性能分析 | 开发/测试 |
| Arthas | 在线诊断 | 生产排障 |
| JConsole/VisualVM | 本地监控 | 开发环境 |
| GCViewer | GC日志分析 | 性能调优 |
八、结语(上篇)
OOM 不是玄学,是内存管理的"显性缺陷"。
当你拿到一行 OutOfMemoryError: Java heap space,能立刻说出:
- 这是哪块内存满了?
- 是泄漏还是配置不足?
- 接下来该用
jstat还是jmap?
恭喜你,已经超过了 80% 的开发者。
下一篇文章,我们将手把手带你在本地复现全部 7 种 OOM,并用 MAT、Arthas把它们一个一个"揪出来"修复掉。