在 Java 线上环境中,OutOfMemoryError(OOM) 是常见且严重的问题,通常由以下原因引起:
一、堆内存溢出(Heap Space OOM)
错误信息:java.lang.OutOfMemoryError: Java heap space
原因:
-
内存泄漏(Memory Leak)
-
对象被无意义地长期持有(如静态集合、未关闭的资源、缓存未清理)。
-
示例:
static List<Object> list = new ArrayList<>(); public void addData() { while (true) list.add(new Object()); // 对象无法被GC回收 }
-
-
内存配置不合理
- JVM堆内存(
-Xmx)设置过小,无法承载业务数据量。 - 突增流量导致瞬时对象创建量超过堆容量。
- JVM堆内存(
-
大对象或频繁创建对象
- 一次性加载大文件到内存(如未分页查询数据库)。
- 高频创建临时对象(如日志拼接、大量嵌套循环)。
排查方法:
- 使用
jmap -dump:format=b,file=heap.hprof <pid>生成堆转储文件,通过 Eclipse MAT 或 VisualVM 分析对象引用链。 - 监控 GC 日志(
-Xloggc)观察老年代占用是否持续增长。
二、元空间/方法区溢出(Metaspace OOM)
错误信息:java.lang.OutOfMemoryError: Metaspace
原因:
-
动态生成类过多
- 大量使用反射(如
Class.forName())、CGLIB 代理(Spring AOP)或 JSP 动态编译。
- 大量使用反射(如
-
未合理配置元空间大小
- 默认元空间(
-XX:MetaspaceSize)较小,未根据业务调整。
- 默认元空间(
-
类加载器泄漏
- 自定义类加载器未正确卸载,导致加载的类无法回收。
示例:
// 不断生成新类导致元空间溢出
public class MetaspaceOOM {
static class OOMObject {}
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.create(); // 生成动态代理类
}
}
}
解决方向:
- 增加元空间大小:
-XX:MaxMetaspaceSize=512m - 使用
-XX:+TraceClassLoading跟踪类加载情况。
三、栈溢出(Stack Overflow)
错误信息:java.lang.StackOverflowError(严格来说属于Error,但常伴随OOM)
原因:
-
递归调用过深
- 未正确设置递归终止条件。
-
线程数过多
- 每个线程的栈内存(
-Xss)默认1MB,大量线程导致总栈内存耗尽。 - 示例:创建数千个线程(
new Thread(() -> { while(true); }).start())。
- 每个线程的栈内存(
解决方向:
- 优化代码逻辑,避免无限递归。
- 减少线程数或调整栈大小(
-Xss256k,需权衡栈深度)。
四、直接内存溢出(Direct Memory OOM)
错误信息:java.lang.OutOfMemoryError: Direct buffer memory
原因:
-
NIO操作未释放直接内存
- 频繁分配
ByteBuffer.allocateDirect()但未及时回收。
- 频繁分配
-
JVM参数限制
-XX:MaxDirectMemorySize设置过小(默认与堆内存一致)。
-
第三方库泄漏
- Netty、Mina等框架未正确释放
PooledByteBuf。
- Netty、Mina等框架未正确释放
排查方法:
- 使用
jcmd <pid> VM.native_memory查看直接内存占用。 - 检查代码中是否遗漏
Cleaner或未调用Buffer的clear()方法。
五、其他OOM类型
-
GC Overhead Limit Exceeded
- JVM花费98%时间做GC但回收不到2%内存(
-XX:-UseGCOverheadLimit可禁用,但需谨慎)。
- JVM花费98%时间做GC但回收不到2%内存(
-
Unable to Create New Native Thread
- 操作系统限制线程数(
ulimit -u),常见于高并发场景。
- 操作系统限制线程数(
-
Requested array size exceeds VM limit
- 尝试分配超大数组(如
new int[Integer.MAX_VALUE])。
- 尝试分配超大数组(如
六、通用排查步骤
-
获取错误日志
- 通过
-XX:+HeapDumpOnOutOfMemoryError自动生成堆转储文件。
- 通过
-
分析内存快照
- 使用 MAT 查找
Retained Heap最大的对象。
- 使用 MAT 查找
-
监控工具
jstat -gcutil <pid>观察各分区内存使用率。- Prometheus + Grafana 实时监控JVM内存。
-
代码审查
- 检查集合类、缓存机制、资源关闭逻辑。
七、预防措施
- 合理配置JVM参数:根据业务负载调整堆、元空间、栈大小。
- 代码规范:避免静态集合持有对象、及时关闭资源(数据库连接、文件流)。
- 压测验证:通过JMeter模拟高并发场景,提前暴露内存问题。
- 限流降级:使用Sentinel或Hystrix防止突发流量压垮系统。
通过结合日志分析、工具监控和代码优化,可有效定位和解决OOM问题。