问题

OutOfMemory和StackOverflow的区别是什么?

答案

核心概念

OutOfMemoryErrorStackOverflowError都是Java中的Error(而非Exception),表示严重的不可恢复错误。两者的本质区别在于发生的内存区域和原因不同

区别对比

维度 OutOfMemoryError (OOM) StackOverflowError (SOF)
继承关系 VirtualMachineError VirtualMachineError
发生区域 堆、元空间、直接内存等 虚拟机栈/本地方法栈
根本原因 内存空间不足 栈深度超过限制
是否可扩展 可以(但达到上限) 可以(但达到上限)
典型场景 内存泄漏、对象过多 递归调用层数过深
解决方向 增加内存、修复泄漏 优化递归、增加栈大小

OutOfMemoryError详解

1. 堆内存OOM

发生场景: Java堆无法分配新对象

/**
 * VM Args: -Xmx20m -Xms20m
 */
public class HeapOOM {
    static class OOMObject {}

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while (true) {
            list.add(new OOMObject());  // 持续创建对象
        }
    }
}

// 输出:
// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

原因分析:

  • 创建了大量对象且持有引用
  • 对象无法被GC回收
  • 堆内存不足以分配新对象

解决方案:

# 1. 增加堆内存
-Xmx4g

# 2. 分析堆转储文件
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof

2. 元空间OOM (JDK 8+)

发生场景: 加载了过多的类

/**
 * VM Args: -XX:MaxMetaspaceSize=20m
 */
public class MetaspaceOOM {
    public static void main(String[] args) {
        int count = 0;
        try {
            while (true) {
                // 使用CGLib动态生成类
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(Object.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object obj, Method method,
                                          Object[] args, MethodProxy proxy) throws Throwable {
                        return proxy.invokeSuper(obj, args);
                    }
                });
                enhancer.create();
                count++;
            }
        } catch (Throwable e) {
            System.out.println("Generated " + count + " classes");
            e.printStackTrace();
        }
    }
}

// 输出:
// java.lang.OutOfMemoryError: Metaspace

原因分析:

  • 动态生成大量类(CGLib、ASM、动态代理等)
  • 类加载器泄漏导致类无法卸载
  • 元空间配置过小

解决方案:

-XX:MaxMetaspaceSize=512m
-XX:+TraceClassLoading    # 跟踪类加载
-XX:+TraceClassUnloading  # 跟踪类卸载

3. 直接内存OOM

发生场景: DirectByteBuffer分配超过限制

/**
 * VM Args: -Xmx20m -XX:MaxDirectMemorySize=10m
 */
public class DirectMemoryOOM {
    public static void main(String[] args) throws Exception {
        final int _1MB = 1024 * 1024;
        int count = 0;
        try {
            while (true) {
                ByteBuffer.allocateDirect(_1MB);
                count++;
            }
        } catch (Throwable e) {
            System.out.println("Allocated " + count + " MB");
            e.printStackTrace();
        }
    }
}

// 输出:
// java.lang.OutOfMemoryError: Direct buffer memory

解决方案:

-XX:MaxDirectMemorySize=1g

4. 无法创建新的本地线程

发生场景: 系统无法创建新线程

/**
 * VM Args: -Xss2m (减少每个线程栈大小,可以创建更多线程)
 */
public class ThreadOOM {
    public static void main(String[] args) {
        int count = 0;
        try {
            while (true) {
                new Thread(() -> {
                    try {
                        Thread.sleep(100000);
                    } catch (InterruptedException e) {}
                }).start();
                count++;
            }
        } catch (Throwable e) {
            System.out.println("Created " + count + " threads");
            e.printStackTrace();
        }
    }
}

// 输出:
// java.lang.OutOfMemoryError: unable to create new native thread

原因分析:

  • 系统线程数达到上限
  • 内存不足以分配新线程的栈空间
  • 操作系统限制(ulimit -u)

解决方案:

# 1. 减少每个线程栈大小
-Xss256k

# 2. 使用线程池控制线程数
ExecutorService executor = Executors.newFixedThreadPool(100);

# 3. 调整系统限制(Linux)
ulimit -u 4096

StackOverflowError详解

1. 典型场景: 递归调用过深

/**
 * VM Args: -Xss128k (减小栈大小,更容易复现)
 */
public class StackOverflowExample {
    private int depth = 0;

    public void recursion() {
        depth++;
        recursion();  // 无限递归
    }

    public static void main(String[] args) {
        StackOverflowExample example = new StackOverflowExample();
        try {
            example.recursion();
        } catch (Throwable e) {
            System.out.println("Stack depth: " + example.depth);
            e.printStackTrace();
        }
    }
}

// 输出:
// Stack depth: 1000 (具体数值取决于-Xss设置)
// java.lang.StackOverflowError

2. 原因分析

栈帧过多:

  • 每次方法调用都会创建栈帧
  • 栈帧包含局部变量表、操作数栈、返回地址等
  • 递归深度超过栈空间限制

栈帧过大:

public void largeLocalVariables() {
    int[] array1 = new int[100000];  // 局部变量过大
    int[] array2 = new int[100000];
    int[] array3 = new int[100000];
    // 单个栈帧占用空间过大,也可能导致SOF
}

3. 常见错误代码

错误的递归终止条件:

// 错误: 条件永远为true
public int factorial(int n) {
    if (n > 0) {  // 应该是 n == 1 或 n <= 1
        return n * factorial(n - 1);
    }
    return 1;
}

循环引用导致无限递归:

class Node {
    String data;
    Node next;

    @Override
    public String toString() {
        return data + " -> " + next;  // 如果存在循环引用,toString会无限递归
    }
}

// 使用:
Node n1 = new Node();
Node n2 = new Node();
n1.next = n2;
n2.next = n1;  // 循环引用
System.out.println(n1);  // StackOverflowError

4. 解决方案

方案1: 优化算法,改递归为迭代:

// 递归版本(可能SOF)
public int factorialRecursive(int n) {
    if (n <= 1) return 1;
    return n * factorialRecursive(n - 1);
}

// 迭代版本(不会SOF)
public int factorialIterative(int n) {
    int result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

方案2: 使用尾递归优化(Java不支持尾递归优化,但可以手动改写):

// 普通递归
public int sum(int n) {
    if (n == 1) return 1;
    return n + sum(n - 1);  // 不是尾递归
}

// 尾递归形式(Java仍会SOF,但逻辑上更优)
public int sumTail(int n, int accumulator) {
    if (n == 1) return accumulator + 1;
    return sumTail(n - 1, accumulator + n);  // 尾递归
}

方案3: 增加栈大小:

-Xss2m  # 将栈大小从默认1MB增加到2MB

方案4: 使用栈数据结构模拟递归:

// 使用栈模拟递归
public void traverseTree(TreeNode root) {
    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);

    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        System.out.println(node.val);

        if (node.right != null) stack.push(node.right);
        if (node.left != null) stack.push(node.left);
    }
}

如何区分和诊断

1. 异常信息判断

try {
    // 可能抛出OOM或SOF的代码
} catch (OutOfMemoryError e) {
    System.err.println("发生内存溢出: " + e.getMessage());
    // Java heap space / Metaspace / Direct buffer memory / unable to create new native thread
} catch (StackOverflowError e) {
    System.err.println("发生栈溢出");
    e.printStackTrace();  // 查看调用栈,找到递归调用链
}

2. 分析堆转储

# OOM时自动生成堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof

# 使用MAT或jhat分析
jhat heapdump.hprof
# 浏览器访问 http://localhost:7000

3. 查看GC日志

# 启用GC日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/tmp/gc.log

# OOM前通常有频繁的Full GC
[Full GC 1024K->1024K(2048K), 1.2345678 secs]  # GC后内存几乎不下降

实战案例

案例1: 定位堆OOM

// 使用MAT(Memory Analyzer Tool)分析
// 1. Leak Suspects Report: 自动找出疑似泄漏点
// 2. Dominator Tree: 查看占用内存最多的对象
// 3. Path to GC Roots: 查看对象为何无法被GC

// 常见原因:
// - ThreadLocal未清理
// - 静态集合持有对象引用
// - 缓存无限增长
// - 监听器未注销

案例2: 定位StackOverflow

// 分析异常栈:
java.lang.StackOverflowError
    at com.example.MyClass.method(MyClass.java:10)
    at com.example.MyClass.method(MyClass.java:10)  // 重复调用
    at com.example.MyClass.method(MyClass.java:10)
    ...

// 查看MyClass.java第10行,找到递归调用

面试总结

OutOfMemoryError:

  • 发生在堆、元空间、直接内存等区域
  • 根本原因是内存不足
  • 解决: 增加内存、修复内存泄漏、优化对象创建
  • 典型场景: 内存泄漏、创建大量对象、类加载过多

StackOverflowError:

  • 发生在虚拟机栈/本地方法栈
  • 根本原因是栈深度超限
  • 解决: 优化递归算法、改用迭代、增加栈大小
  • 典型场景: 无限递归、递归深度过大

关键区别: OOM是”空间不够了”,SOF是”调用太深了”。