
堆内存 OOM 是所有 OOM 错误中最常见的一种,几乎每个 Java 开发者都或多或少遇到过。理解堆内存 OOM 的产生原因和解决方法,是 Java 工程师的必备技能。
Java 堆是 JVM 管理的内存中最大的一块区域,主要用于存储对象实例。当我们创建对象时,JVM 会在堆中为其分配内存。如果堆中没有足够的空间分配给新创建的对象,并且垃圾收集器也无法回收出足够的内存时,就会抛出java.lang.OutOfMemoryError: Java heap space异常。
堆内存的大小可以通过 JVM 参数-Xms(初始堆大小)和-Xmx(最大堆大小)来设置。默认情况下,JVM 会根据物理内存自动计算这些值,但在生产环境中,我们通常需要手动配置以满足应用需求。
堆内存结构可以用以下架构图表示:

堆内存 OOM 的产生流程如下:

堆内存 OOM 通常发生在以下场景:
下面的代码将模拟堆内存 OOM 的场景:
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
/**
* 堆内存OOM演示
* 运行时需要设置JVM参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof
*
* @author ken
*/
@Slf4j
public class HeapOomDemo {
/**
* 用于存储大量对象的列表
*/
private static final List<byte[]> DATA_LIST = new ArrayList<>();
/**
* 模拟堆内存溢出
* 不断向列表中添加大字节数组,直到堆内存耗尽
*/
public static void simulateHeapOom() {
int count = 0;
try {
// 每次添加1MB大小的字节数组
while (true) {
byte[] data = new byte[1024 * 1024];
DATA_LIST.add(data);
count++;
log.info("已添加{}个1MB数组,当前列表大小: {}MB", count, count);
}
} catch (OutOfMemoryError e) {
log.error("发生堆内存OOM,共添加了{}个1MB数组", count, e);
throw e;
}
}
public static void main(String[] args) {
simulateHeapOom();
}
}
运行上述代码时,需要设置 JVM 参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof。这些参数的含义是:
-Xms20m:初始堆大小为 20MB-Xmx20m:最大堆大小为 20MB-XX:+HeapDumpOnOutOfMemoryError:发生 OOM 时自动生成堆转储文件-XX:HeapDumpPath=./heapdump.hprof:堆转储文件的保存路径运行后,程序会不断创建 1MB 大小的字节数组并添加到列表中,直到堆内存耗尽,此时会抛出java.lang.OutOfMemoryError: Java heap space异常,并在指定路径生成堆转储文件。
排查堆内存 OOM 常用的工具包括:
-XX:+HeapDumpOnOutOfMemoryError在 OOM 时自动生成jmap -dump:format=b,file=heapdump.hprof <pid>手动生成(<pid>为进程 ID)针对堆内存 OOM,常见的解决方案包括:
-Xms和-Xmx参数)栈内存 OOM(无法创建新的本地线程)是另一种常见的 OOM 错误,尤其在高并发场景下容易出现。与堆内存 OOM 不同,它的产生原因和解决方案都有其特殊性。
在 Java 中,每个线程都有自己的虚拟机栈(JVM Stack),用于存储方法调用的栈帧。虚拟机栈的大小可以通过-Xss参数设置,默认值因 JVM 版本和操作系统而异(通常为 1MB)。
当创建一个新线程时,JVM 需要向操作系统申请内存来创建线程的栈空间。如果操作系统无法为新线程分配足够的内存,就会抛出java.lang.OutOfMemoryError: unable to create new native thread异常。
导致这种情况的原因主要有两个:
线程创建与内存分配的关系可以用以下流程图表示:

栈内存 OOM(无法创建新线程)通常发生在以下场景:
下面的代码将模拟无法创建新线程的 OOM 场景:
import lombok.extern.slf4j.Slf4j;
/**
* 无法创建新线程的OOM演示
* 运行时可以设置JVM参数: -Xss1m (增大栈内存,减少可创建的线程总数)
*
* @author ken
*/
@Slf4j
public class ThreadCreationOomDemo {
/**
* 记录已创建的线程数量
*/
private static int threadCount = 0;
/**
* 模拟不断创建新线程,直到无法创建
*/
public static void simulateThreadCreationOom() {
try {
while (true) {
threadCount++;
// 创建并启动新线程
new Thread(() -> {
try {
// 让线程一直运行,不终止
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程被中断", e);
}
}).start();
if (threadCount % 100 == 0) {
log.info("已创建{}个线程", threadCount);
}
}
} catch (OutOfMemoryError e) {
log.error("发生无法创建新线程的OOM,共创建了{}个线程", threadCount, e);
throw e;
}
}
public static void main(String[] args) {
simulateThreadCreationOom();
}
}
运行上述代码时,可以通过-Xss参数调整栈内存大小。栈内存设置越大(如-Xss1m),每个线程占用的内存就越多,系统可创建的线程总数就越少,从而更快地触发 OOM。
程序会不断创建新线程,每个线程都会进入无限休眠状态(不释放资源),直到达到系统线程数限制或内存不足,此时会抛出java.lang.OutOfMemoryError: unable to create new native thread异常。
排查无法创建新线程的问题常用的工具包括:
ps -T -p <pid>)top -H -p <pid>查看特定进程的线程)ulimit -a)ps -T -p <pid>查看进程的线程总数ulimit -u查看)jstack <pid>生成线程栈信息free -m)lsof -p <pid> | wc -l)针对无法创建新线程的 OOM,常见的解决方案包括:
优化线程池配置:
// 合理的线程池配置示例
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
20, // 最大线程数
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 较大的队列
new ThreadPoolExecutor.CallerRunsPolicy() // 当队列满时,让提交任务的线程执行任务
);
减少单个线程的栈内存:
-Xss参数适当减小栈内存(如-Xss256k)解决线程泄漏问题:
调整系统资源限制:
ulimit -u <number>)ulimit -n <number>)采用异步非阻塞架构:
栈内存溢出(StackOverflowError)虽然名称中没有 "OutOfMemoryError",但它也是一种常见的内存相关错误,容易与栈内存 OOM 混淆。实际上,它们的产生原因和表现形式都有很大差异。
Java 虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被调用时,都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
当一个方法被调用时,对应的栈帧会被压入虚拟机栈;当方法执行完成时,栈帧会被弹出并销毁。如果方法调用层级过深,导致虚拟机栈中堆积的栈帧过多,超过了虚拟机栈的最大容量,就会抛出java.lang.StackOverflowError。
栈帧的创建与销毁流程如下:

栈内存溢出与栈内存 OOM 的主要区别:
栈内存溢出通常发生在以下场景:
-Xss参数值太小)下面的代码将模拟栈内存溢出的场景:
import lombok.extern.slf4j.Slf4j;
/**
* 栈内存溢出(StackOverflowError)演示
* 运行时可以设置JVM参数: -Xss128k (减小栈内存,更快触发溢出)
*
* @author ken
*/
@Slf4j
public class StackOverflowDemo {
/**
* 记录递归调用次数
*/
private static int recursionCount = 0;
/**
* 递归方法,没有终止条件,会导致栈溢出
*/
public static void recursiveMethod() {
recursionCount++;
// 每1000次调用打印一次日志
if (recursionCount % 1000 == 0) {
log.info("递归调用次数: {}", recursionCount);
}
// 递归调用自身,没有终止条件
recursiveMethod();
}
/**
* 演示栈溢出
*/
public static void simulateStackOverflow() {
try {
recursiveMethod();
} catch (StackOverflowError e) {
log.error("发生栈内存溢出,递归调用次数: {}", recursionCount, e);
throw e;
}
}
public static void main(String[] args) {
simulateStackOverflow();
}
}
运行上述代码时,可以通过-Xss参数调整栈内存大小。栈内存设置越小(如-Xss128k),递归调用次数越少就会触发溢出;栈内存设置越大(如-Xss1m),可以支持的递归调用次数就越多。
程序会不断递归调用recursiveMethod()方法,每次调用都会在虚拟机栈中创建一个新的栈帧,直到栈深度超过限制,此时会抛出java.lang.StackOverflowError。
排查栈内存溢出常用的工具包括:
针对栈内存溢出,常见的解决方案包括:
修复递归逻辑:
// 修复后的递归方法,包含正确的终止条件
public static int factorial(int n) {
// 终止条件
if (n <= 1) {
return 1;
}
// 递归调用
return n * factorial(n - 1);
}
增加栈内存大小:
-Xss参数适当增大栈内存(如-Xss512k或-Xss1m)优化方法调用链:
// 迭代实现替代递归
public static int factorialIterative(int n) {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
优化方法内部实现:
直接内存(Direct Memory)是 Java NIO 引入的一种内存分配方式,它不属于 JVM 堆内存,而是直接从操作系统分配的内存。直接内存 OOM 在使用 NIO 的应用中比较常见,且排查起来相对困难。
直接内存的分配不会受到 Java 堆大小的限制,但会受到系统总内存大小的限制。在 Java 中,通过java.nio.ByteBuffer的allocateDirect(int capacity)方法可以分配直接内存。
直接内存与 Java 堆内存的关系如下:

直接内存的回收机制比较特殊:
sun.misc.Cleaner(JDK 9 + 中为java.lang.ref.Cleaner)机制实现直接内存 OOM 的产生原因:
-XX:MaxDirectMemorySize参数限制了直接内存大小,且实际使用超过了这个限制直接内存 OOM 通常发生在以下场景:
-XX:MaxDirectMemorySize参数,或设置过小,而应用需要大量直接内存下面的代码将模拟直接内存 OOM 的场景:
import lombok.extern.slf4j.Slf4j;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* 直接内存OOM演示
* 运行时需要设置JVM参数: -Xms20m -Xmx20m -XX:MaxDirectMemorySize=20m
*
* @author ken
*/
@Slf4j
public class DirectMemoryOomDemo {
/**
* 存储直接ByteBuffer的列表
*/
private static final List<ByteBuffer> BUFFER_LIST = new ArrayList<>();
/**
* 模拟直接内存OOM
* 不断分配直接内存,直到无法分配
*/
public static void simulateDirectMemoryOom() {
int count = 0;
try {
// 每次分配1MB的直接内存
while (true) {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
BUFFER_LIST.add(buffer);
count++;
log.info("已分配{}个1MB直接内存缓冲区", count);
}
} catch (OutOfMemoryError e) {
log.error("发生直接内存OOM,共分配了{}个1MB直接内存缓冲区", count, e);
throw e;
}
}
public static void main(String[] args) {
simulateDirectMemoryOom();
}
}
运行上述代码时,需要设置 JVM 参数:-Xms20m -Xmx20m -XX:MaxDirectMemorySize=20m。这些参数的含义是:
-Xms20m -Xmx20m:将堆内存限制为 20MB,避免堆内存 OOM 干扰-XX:MaxDirectMemorySize=20m:将直接内存限制为 20MB程序会不断分配 1MB 大小的直接内存缓冲区并添加到列表中,直到超过直接内存限制,此时会抛出java.lang.OutOfMemoryError: Direct buffer memory异常。
排查直接内存 OOM 常用的工具包括:
jcmd <pid> VM.native_memory)vmdirectmemory命令查看直接内存使用情况jcmd <pid> VM.native_memory summary查看直接内存分配情况-XX:MaxDirectMemorySize设置的值jmap -histo:live <pid>查看直接 ByteBuffer 对象的数量和大小针对直接内存 OOM,常见的解决方案包括:
调整直接内存大小:
-XX:MaxDirectMemorySize参数适当增大直接内存限制-XX:MaxDirectMemorySize=1G(设置直接内存最大为 1GB)优化直接内存使用:
// 直接ByteBuffer对象池示例
public class DirectBufferPool {
private final Queue<ByteBuffer> pool;
private final int bufferSize;
private final int maxPoolSize;
public DirectBufferPool(int bufferSize, int maxPoolSize) {
this.bufferSize = bufferSize;
this.maxPoolSize = maxPoolSize;
this.pool = new ArrayDeque<>(maxPoolSize);
}
// 获取缓冲区
public ByteBuffer acquire() {
ByteBuffer buffer = pool.poll();
return buffer != null ? buffer : ByteBuffer.allocateDirect(bufferSize);
}
// 释放缓冲区到池
public void release(ByteBuffer buffer) {
if (buffer.capacity() != bufferSize) {
return; // 不回收非标准大小的缓冲区
}
buffer.clear();
if (pool.size() < maxPoolSize) {
pool.offer(buffer);
}
}
}
主动释放直接内存:
import sun.misc.Cleaner;
import java.nio.ByteBuffer;
import java.lang.reflect.Field;
public class DirectMemoryUtils {
/**
* 主动释放直接内存
*/
@SuppressWarnings("unchecked")
public static void cleanDirectBuffer(ByteBuffer buffer) {
if (buffer == null || !buffer.isDirect()) {
return;
}
try {
// 通过反射获取Cleaner对象
Field cleanerField = buffer.getClass().getDeclaredField("cleaner");
cleanerField.setAccessible(true);
Cleaner cleaner = (Cleaner) cleanerField.get(buffer);
if (cleaner != null) {
cleaner.clean();
}
} catch (Exception e) {
log.error("释放直接内存失败", e);
}
}
}
Cleaner.clean()主动释放解决直接内存泄漏:
GC OOM(GC overhead limit exceeded)是一种特殊的内存错误,它不是因为内存绝对不足,而是因为垃圾收集花费了过多的时间但回收效果不佳,JVM 为了避免系统不可用而主动抛出的异常。
JVM 会监控垃圾收集的时间和效率,如果满足以下条件,就会抛出java.lang.OutOfMemoryError: GC overhead limit exceeded异常:
这种机制的设计初衷是:当 GC 效率极低时,系统已经接近不可用状态(大部分 CPU 时间都用于 GC,而不是执行实际业务),此时不如抛出 OOM 异常让应用终止,以便于快速恢复。
GC OOM 的触发流程如下:

GC OOM 通常表明应用存在严重的内存问题,可能是内存泄漏,也可能是内存不足。
GC OOM 通常发生在以下场景:
下面的代码将模拟 GC OOM 的场景:
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* GC OOM演示
* 运行时需要设置JVM参数: -Xms5m -Xmx5m -XX:+PrintGCDetails
*
* @author ken
*/
@Slf4j
public class GcOverheadOomDemo {
/**
* 存储部分生命周期较长的对象
*/
private static final List<Object> LONG_LIFE_OBJECTS = new ArrayList<>();
/**
* 模拟GC OOM
* 创建大量对象,其中一部分被长期引用,一部分被短期引用
* 导致GC频繁执行但回收效果不佳
*/
public static void simulateGcOverheadOom() {
int count = 0;
try {
while (true) {
// 添加一些长期存在的对象(占用部分内存)
if (count % 10 == 0) {
LONG_LIFE_OBJECTS.add(new byte[1024 * 10]); // 10KB
}
// 创建大量短期对象(很快会成为垃圾)
byte[] shortLifeObject = new byte[1024 * 5]; // 5KB
count++;
if (count % 100 == 0) {
log.info("已创建{}个对象,长期对象数量: {}", count, LONG_LIFE_OBJECTS.size());
}
}
} catch (OutOfMemoryError e) {
log.error("发生GC OOM,已创建{}个对象,长期对象数量: {}", count, LONG_LIFE_OBJECTS.size(), e);
throw e;
}
}
public static void main(String[] args) {
simulateGcOverheadOom();
}
}
运行上述代码时,需要设置 JVM 参数:-Xms5m -Xmx5m -XX:+PrintGCDetails。这些参数的含义是:
-Xms5m -Xmx5m:将堆内存限制为 5MB,制造内存紧张的环境-XX:+PrintGCDetails:打印 GC 详细日志,便于观察 GC 情况程序会不断创建对象,其中一部分对象被添加到LONG_LIFE_OBJECTS列表中长期持有,另一部分则是短期对象。随着时间推移,长期对象会占用大部分内存,导致 GC 频繁执行但只能回收少量内存(短期对象),最终触发java.lang.OutOfMemoryError: GC overhead limit exceeded异常。
排查 GC OOM 常用的工具包括:
jstat -gc <pid> 1000每秒输出一次 GC 信息)针对 GC OOM,常见的解决方案包括:
解决内存泄漏问题:
调整堆内存大小:
-Xms和-Xmx参数)优化对象创建和使用:
调整 GC 参数:
-XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:MaxGCPauseMillis=200
-XX:NewRatio、-XX:SurvivorRatio)禁用 GC overhead limit 检查(不推荐):
-XX:-UseGCOverheadLimit参数禁用 GC overhead limit 检查元空间(Metaspace)是 JDK 8 及以上版本中用于存储类元数据的区域,它替代了 JDK 7 及之前的永久代(PermGen)。元空间 OOM 在使用大量动态生成类的应用中比较常见。
元空间用于存储以下信息:
与永久代不同,元空间并不在 JVM 堆内存中,而是使用本地内存(Native Memory)。元空间的大小默认不受限制,只受限于系统可用内存,但也可以通过 JVM 参数进行限制。
元空间的结构可以用以下架构图表示:

元空间 OOM 的产生原因:
-XX:MaxMetaspaceSize参数限制了元空间大小,且实际使用超过了这个限制元空间 OOM 通常发生在以下场景:
-XX:MaxMetaspaceSize参数值太小)下面的代码将模拟元空间 OOM 的场景:
import lombok.extern.slf4j.Slf4j;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 元空间OOM演示
* 需要添加cglib依赖
* 运行时需要设置JVM参数: -XX:MaxMetaspaceSize=20m
*
* @author ken
*/
@Slf4j
public class MetaspaceOomDemo {
/**
* 用于动态代理的目标类
*/
static class TargetClass {}
/**
* 方法拦截器,用于cglib动态代理
*/
static class TargetInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
}
/**
* 模拟元空间OOM
* 使用cglib动态生成大量类,直到元空间耗尽
*/
public static void simulateMetaspaceOom() {
int count = 0;
try {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TargetClass.class);
enhancer.setCallback(new TargetInterceptor());
// 不断生成TargetClass的子类
while (true) {
Class<?> proxyClass = enhancer.createClass();
count++;
if (count % 100 == 0) {
log.info("已生成{}个动态类", count);
}
// 为了避免堆内存OOM,不保存生成的类实例
// 只让类元数据保存在元空间中
}
} catch (OutOfMemoryError e) {
log.error("发生元空间OOM,共生成{}个动态类", count, e);
throw e;
}
}
public static void main(String[] args) {
simulateMetaspaceOom();
}
}
运行上述代码需要添加 CGLIB 依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
运行时需要设置 JVM 参数:-XX:MaxMetaspaceSize=20m,该参数将元空间大小限制为 20MB。
程序会使用 CGLIB 不断动态生成TargetClass的子类,每个类的元数据都会存储在元空间中,直到元空间耗尽,此时会抛出java.lang.OutOfMemoryError: Metaspace异常。
排查元空间 OOM 常用的工具包括:
jstat -class <pid> 1000)jmap -clstats <pid>)classloader命令查看类加载器和加载的类数量jstat -class <pid>查看类加载数量和元空间大小-XX:MaxMetaspaceSize设置的值jmap -clstats <pid>查看类加载器统计信息针对元空间 OOM,常见的解决方案包括:
调整元空间大小:
-XX:MaxMetaspaceSize参数适当增大元空间限制-XX:MaxMetaspaceSize=256m(设置元空间最大为 256MB)-XX:MetaspaceSize=128m优化动态类生成:
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import net.sf.cglib.proxy.Enhancer;
import java.util.concurrent.TimeUnit;
public class ProxyClassCache {
// 缓存代理类,过期时间10分钟
private static final Cache<Class<?>, Class<?>> PROXY_CLASS_CACHE = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
/**
* 获取或创建代理类
*/
public static Class<?> getProxyClass(Class<?> targetClass, MethodInterceptor interceptor) {
try {
// 从缓存获取
Class<?> proxyClass = PROXY_CLASS_CACHE.getIfPresent(targetClass);
if (proxyClass == null) {
// 缓存中没有,创建新的代理类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetClass);
enhancer.setCallback(interceptor);
proxyClass = enhancer.createClass();
// 放入缓存
PROXY_CLASS_CACHE.put(targetClass, proxyClass);
}
return proxyClass;
} catch (Exception e) {
log.error("获取代理类失败", e);
throw new RuntimeException(e);
}
}
}
解决类加载器泄漏:
减少类数量:
面对各种 OOM 问题,我们需要一套系统的排查方法和工具使用技巧,以便快速定位和解决问题。

工具 | 用途 | 常用命令 |
|---|---|---|
jps | 查看 Java 进程 ID | jps -l |
jstat | 监控 JVM 统计信息 | jstat -gc <pid> 1000 |
jmap | 生成堆转储和查看内存信息 | jmap -dump:format=b,file=heap.hprof <pid>jmap -histo:live <pid> |
jstack | 查看线程栈信息 | jstack <pid> |
jconsole | 可视化监控工具 | jconsole |
jvisualvm | 功能强大的可视化分析工具 | jvisualvm |
jcmd | 多功能诊断命令 | jcmd <pid> VM.native_memoryjcmd <pid> GC.heap_info |
MAT | 堆转储分析工具 | 图形界面操作 |
Arthas | 阿里巴巴开源诊断工具 | arthas-boot.jardashboard, thread, heapdump 等命令 |
合理配置 JVM 参数:
-Xms2g -Xmx2g -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof实施内存监控:
代码层面预防:
测试层面预防:
OOM 问题是 Java 开发中不可避免的挑战,但通过深入理解各种 OOM 的底层原理,掌握系统的排查方法和工具使用技巧,我们可以有效地定位和解决这些问题。需要强调的是,解决 OOM 问题不能只停留在表面,简单地通过增大内存来 "掩盖" 问题,而应该深入分析根本原因,从代码和架构层面进行优化,才能真正避免问题再次发生。最后,预防胜于治疗。通过合理的 JVM 配置、完善的监控告警和良好的编码实践,我们可以在很大程度上避免 OOM 问题的发生,确保应用的稳定运行。
希望本文能帮助您更好地理解和应对 OOM 问题,让您的 Java 应用更加健壮和可靠。