Java中的StackOverflowError与OutOfMemoryError

StackOverflowError是栈空间耗尽所致,主因是无限递归或栈帧过大;OutOfMemoryError是堆/元空间/直接内存不足所致,需据错误信息细分定位;二者发生时机、排查方式及JVM参数均不同。

StackOverflowError 通常是因为递归太深或栈帧过大

Java 虚拟机为每个线程分配固定大小的栈空间(默认一般 1MB 左右),StackOverflowError 就是这个空间被耗尽时抛出的。最常见原因是无限递归,比如 factorial(n) 忘了写 base case,或者递归深度天然很大(如遍历深度 10000 的树)却没改栈大小。

  • 不是所有递归都会触发——JVM 会做尾调用优化(但 Java 8+ 官方不支持尾递归优化,javac 和 HotSpot 都不处理)
  • 局部变量过多、方法参数太长、匿名类/lambda 捕获大量外部变量,也会让单个栈帧变大,加速栈溢出
  • 可通过 -Xss512k 减小栈大小来快速复现问题,或用 -Xss2m 加大来临时缓解(但治标不治本)

OutOfMemoryError 的常见子类型和定位方式

OutOfMemoryError 是堆内存(或元空间、直接内存等)不足时的统称,必须看完整错误信息才能判断具体原因。常见形式有:

  • java.lang.OutOfMemoryError: Java heap space:堆对象太多且 GC 清不掉,典型如缓存未设上限、集合不断 add 不清理、大对象长期持有引用
  • java.lang.OutOfMemoryError: Metaspace:类加载太多(尤其动态生成类场景,如 Spring Boot + CGLIB + 频繁 reload)、或 -XX:MaxMetaspaceSize 设得太小
  • java.lang.OutOfMemoryError: Direct buffer memory:用了 ByteBuffer.allocateDirect() 却没调 cleanerfree(),或 -XX:MaxDirectMemorySize 限制过严

建议启动时加 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof,OOM 后直接分析 dump 文件,比猜快得多。

两者关键区别:栈 vs 堆,错误时机与排查路径完全不同

StackOverflowError 发生在方法调用过程中,JVM 还能正常运行,甚至可以 catch(虽然不推荐);而多数 OutOfMemoryError(尤其是堆溢出)发生时,GC 已经频繁失败,系统响应迟滞,catch 住也很难安全恢复。

  • StackOverflowError 的 stack trace 通常很长且高度重复(比如几百行都是 at com.example.Calc.compute(Calc.java:23)
  • OutOfMemoryError: Java heap space 的 stack trace 往往很短,最后一行常是 newArrayList.add 这类分配点,真正根源在上游
  • JVM 参数干预方向相反:调栈大小用 -Xss,调堆用 -Xmx,调元空间用 -XX:MaxMetaspaceSize

一个容易被忽略的交叉点:递归创建大量对象也可能触发 OOM 而非 SOE

如果递归函数里每层都新建大对象(比如每次 ne

w byte[1024*1024]),可能还没等栈满,堆就先爆了。这时候看到的是 OutOfMemoryError: Java heap space,但根源仍是递归失控。

public void badRecursion(int n) {
    if (n <= 0) return;
    byte[] data = new byte[1024 * 1024]; // 每层占 1MB 堆
    badRecursion(n - 1);
}

这种混合场景下,光看错误类型会误判。得结合线程 dump + heap dump + 代码逻辑一起看——栈深度、对象数量、GC 日志三者缺一不可。