OOM的常见原因以及排查方法

715 阅读3分钟

在 Java 线上环境中,​OutOfMemoryError(OOM)​ 是常见且严重的问题,通常由以下原因引起:


一、堆内存溢出(Heap Space OOM)​

错误信息java.lang.OutOfMemoryError: Java heap space
原因

  1. 内存泄漏(Memory Leak)​

    • 对象被无意义地长期持有(如静态集合、未关闭的资源、缓存未清理)。

    • 示例:

      static List<Object> list = new ArrayList<>();
      public void addData() {
          while (true) list.add(new Object()); // 对象无法被GC回收
      }
      
  2. 内存配置不合理

    • JVM堆内存(-Xmx)设置过小,无法承载业务数据量。
    • 突增流量导致瞬时对象创建量超过堆容量。
  3. 大对象或频繁创建对象

    • 一次性加载大文件到内存(如未分页查询数据库)。
    • 高频创建临时对象(如日志拼接、大量嵌套循环)。

排查方法

  • 使用 jmap -dump:format=b,file=heap.hprof <pid> 生成堆转储文件,通过 ​Eclipse MAT 或 ​VisualVM 分析对象引用链。
  • 监控 GC 日志(-Xloggc)观察老年代占用是否持续增长。

二、元空间/方法区溢出(Metaspace OOM)​

错误信息java.lang.OutOfMemoryError: Metaspace
原因

  1. 动态生成类过多

    • 大量使用反射(如 Class.forName())、CGLIB 代理(Spring AOP)或 JSP 动态编译。
  2. 未合理配置元空间大小

    • 默认元空间(-XX:MetaspaceSize)较小,未根据业务调整。
  3. 类加载器泄漏

    • 自定义类加载器未正确卸载,导致加载的类无法回收。

示例

// 不断生成新类导致元空间溢出
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)
原因

  1. 递归调用过深

    • 未正确设置递归终止条件。
  2. 线程数过多

    • 每个线程的栈内存(-Xss)默认1MB,大量线程导致总栈内存耗尽。
    • 示例:创建数千个线程(new Thread(() -> { while(true); }).start())。

解决方向

  • 优化代码逻辑,避免无限递归。
  • 减少线程数或调整栈大小(-Xss256k,需权衡栈深度)。

四、直接内存溢出(Direct Memory OOM)​

错误信息java.lang.OutOfMemoryError: Direct buffer memory
原因

  1. NIO操作未释放直接内存

    • 频繁分配 ByteBuffer.allocateDirect() 但未及时回收。
  2. JVM参数限制

    • -XX:MaxDirectMemorySize 设置过小(默认与堆内存一致)。
  3. 第三方库泄漏

    • Netty、Mina等框架未正确释放 PooledByteBuf

排查方法

  • 使用 jcmd <pid> VM.native_memory 查看直接内存占用。
  • 检查代码中是否遗漏 Cleaner 或未调用 Buffer 的 clear() 方法。

五、其他OOM类型

  1. GC Overhead Limit Exceeded

    • JVM花费98%时间做GC但回收不到2%内存(-XX:-UseGCOverheadLimit 可禁用,但需谨慎)。
  2. Unable to Create New Native Thread

    • 操作系统限制线程数(ulimit -u),常见于高并发场景。
  3. Requested array size exceeds VM limit

    • 尝试分配超大数组(如 new int[Integer.MAX_VALUE])。

六、通用排查步骤

  1. 获取错误日志

    • 通过 -XX:+HeapDumpOnOutOfMemoryError 自动生成堆转储文件。
  2. 分析内存快照

    • 使用 ​MAT 查找 Retained Heap 最大的对象。
  3. 监控工具

    • jstat -gcutil <pid> 观察各分区内存使用率。
    • Prometheus + Grafana 实时监控JVM内存。
  4. 代码审查

    • 检查集合类、缓存机制、资源关闭逻辑。

七、预防措施

  • 合理配置JVM参数:根据业务负载调整堆、元空间、栈大小。
  • 代码规范:避免静态集合持有对象、及时关闭资源(数据库连接、文件流)。
  • 压测验证:通过JMeter模拟高并发场景,提前暴露内存问题。
  • 限流降级:使用Sentinel或Hystrix防止突发流量压垮系统。

通过结合日志分析、工具监控和代码优化,可有效定位和解决OOM问题。