在 Java 中,内存溢出(OutOfMemoryError
,简称 OOM)通常是由于程序在运行过程中申请的内存超过了 JVM 的最大可用内存限制。以下是常见的场景及其原理:
1. 堆内存溢出(Heap OOM)
-
原因:对象实例过多,且无法被垃圾回收(GC)。
-
常见场景:
-
内存泄漏(Memory Leak):对象被无意中持有引用,无法回收。例如:
-
静态集合类(如
static Map
)长期引用对象。 -
未正确关闭资源(如数据库连接、文件流)。
-
监听器或回调未注销(如 GUI 组件未移除监听器)。
-
-
大对象或大数组:一次性申请超大对象(如
byte[10_000_000]
)。 -
JVM 堆参数配置过小:
-Xmx
设置不足,无法容纳正常业务数据。
-
-
示例代码:
public class HeapOOM { static class OOMObject {} public static void main(String[] args) { List<OOMObject> list = new ArrayList<>(); while (true) { list.add(new OOMObject()); // 不断创建对象,最终抛出 java.lang.OutOfMemoryError: Java heap space } } }
2. 方法区(元空间)溢出(Metaspace OOM)
-
原因:加载的类信息、常量池等元数据超过 Metaspace 限制。
-
常见场景:
-
动态生成类:频繁使用反射(如
CGLib
、ASM
)生成代理类。 -
大量重复类加载:如热部署框架(Spring Boot DevTools)频繁加载类。
-
未卸载的类加载器:如 Web 应用重启后,旧的类加载器未释放。
-
-
示例配置:
# JVM 参数设置 Metaspace 大小 -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m
3. 栈内存溢出(StackOverflowError)
-
原因:线程请求的栈深度超过 JVM 允许的最大深度(默认 1MB)。
-
常见场景:
-
无限递归调用:未正确设置递归终止条件。
-
栈帧过大:方法内定义超大局部变量(如
Object[10000]
)。
-
-
示例代码:
public class StackOverflow { public static void recursiveCall() { recursiveCall(); // 无限递归,抛出 java.lang.StackOverflowError } public static void main(String[] args) { recursiveCall(); } }
4. 直接内存溢出(Direct Memory OOM)
-
原因:直接内存(堆外内存)使用超过
-XX:MaxDirectMemorySize
限制。 -
常见场景:
-
NIO 操作:频繁使用
ByteBuffer.allocateDirect()
未释放。 -
JNI 调用:本地代码(如 C/C++)分配内存未回收。
-
Netty 等框架:未合理管理堆外内存池。
-
-
示例代码:
public class DirectMemoryOOM { public static void main(String[] args) { while (true) { ByteBuffer.allocateDirect(1024 * 1024); // 持续分配直接内存,抛出 OOM: Direct buffer memory } } }
5. 线程数过多导致 OOM
-
原因:创建大量线程且每个线程占用内存(栈、堆外内存等),超出系统限制。
-
常见场景:
-
线程池配置不当:如
Executors.newCachedThreadPool()
无限制创建线程。 -
异步任务未限制并发数:如 Web 应用处理高并发请求时线程数激增。
-
-
示例错误:
java.lang.OutOfMemoryError: unable to create new native thread
6. 字符串常量池溢出(JDK 6 及之前)
-
原因:JDK 6 中字符串常量池位于方法区(PermGen),大量调用
String.intern()
可能导致溢出。 -
JDK 7+:字符串常量池移至堆内存,此问题缓解。
如何诊断和避免 OOM?
-
合理配置 JVM 参数:
-
堆内存:
-Xms
(初始堆大小)、-Xmx
(最大堆大小)。 -
元空间:
-XX:MetaspaceSize
、-XX:MaxMetaspaceSize
。 -
栈大小:
-Xss
(每个线程栈大小)。
-
-
代码优化:
-
避免内存泄漏:及时释放资源、移除无用引用。
-
限制大对象分配:分批处理数据或使用流式处理。
-
-
工具分析:
-
jmap
+MAT
:分析堆转储文件(Heap Dump)。 -
VisualVM
/JProfiler
:实时监控内存使用。 -
-XX:+HeapDumpOnOutOfMemoryError
:OOM 时自动生成 Heap Dump。
-
总结
内存区域 | 溢出原因 | 典型错误信息 |
---|---|---|
堆内存(Heap) | 对象过多或内存泄漏 | Java heap space |
方法区(Metaspace) | 类元数据过多 | Metaspace |
栈内存(Stack) | 递归过深或局部变量过大 | StackOverflowError |
直接内存(Direct) | 堆外内存分配失控 | Direct buffer memory |
线程数过多 | 线程创建超出系统限制 | unable to create new native thread |
理解这些场景并配合工具分析,能有效预防和解决 Java 内存溢出问题。