栈溢出OOM实战: 无限递归时,JVM究竟先报StackOverflowError还是OutOfMemoryError? 栈溢出OOM实战: 无限递归时JVM究竟先报StackOverflowError还是OutOfMemoryError引言: 一个看似简单却暗藏陷阱的问题无限递归会导致什么错误 很多开发者会脱口而出: StackOverflowError 这个答案对吗对但不完全对。实际上在HotSpot JVM中栈内存溢出既可能抛出StackOverflowError也可能抛出OutOfMemoryError。二者之间有明确的界限但具体取决于JVM版本、操作系统和栈内存的分配方式。本文将用实验说话带你亲手触发这两种错误并从JVM规范、HotSpot源码层面讲清楚它们的分界线。一、核心概念: 栈内存的两层结构1.1 逻辑栈 vs 物理内存public class StackMemoryStructure { public static void main(String[] args) { System.out.println( JVM栈内存的两层结构 \n); System.out.println(第一层: 逻辑限制(JVM规范)); System.out.println( 每个线程有独立的Java虚拟机栈); System.out.println( 栈由栈帧(Stack Frame)组成); System.out.println( 每个方法调用创建一个栈帧\n); System.out.println(第二层: 物理限制(操作系统)); System.out.println( 栈内存是真实分配的物理内存); System.out.println( 每个线程的栈大小可以通过-Xss设置); System.out.println( 物理内存耗尽时操作系统无法再分配\n); System.out.println(关键参数:); System.out.println( -Xss: 设置线程栈大小); System.out.println( 默认值: Linux x64约1MB); System.out.println( 默认值: Windows约依赖于OS设置); } }1.2 两种错误的触发边界StackOverflowError正常情况是否-逻辑层OutOfMemoryError否创建新线程为线程分配栈内存物理内存充足?操作系统无法分配抛出OutOfMemoryError:unable to create new native thread方法调用分配栈帧栈空间充足?正常执行方法返回释放栈帧栈深度超限无法创建新的栈帧(当前线程的栈空间内)抛出StackOverflowError二、StackOverflowError实战: 单线程无限递归2.1 触发StackOverflowError/** * 单线程无限递归 - StackOverflowError * * 这是最经典的场景 * 原因: 递归深度超过线程栈的逻辑容量 */ public class StackOverflowErrorDemo { private static int depth 0; public static void main(String[] args) { System.out.println( StackOverflowError演示 \n); // 打印当前线程栈大小 System.out.println(Xss设置: getStackSize()); try { recursiveMethod(); } catch (StackOverflowError e) { System.out.println(\n捕获到StackOverflowError!); System.out.println(递归深度: depth); System.out.println(错误信息: e.getMessage()); } } private static void recursiveMethod() { depth; // 使用大量局部变量来加速栈空间消耗 long a1, a2, a3, a4, a5, a6, a7, a8, a9, a10; long b1, b2, b3, b4, b5, b6, b7, b8, b9, b10; recursiveMethod(); } private static String getStackSize() { // 通过Runtime获取近似值 long totalMemory Runtime.getRuntime().totalMemory(); return 默认约1MB (可通过-Xss调整); } }2.2 不同-Xss参数的影响/** * 不同-Xss参数下的递归深度对比 * * JVM参数: -Xss128k / -Xss256k / -Xss512k / -Xss1m */ public class StackSizeComparison { private static int depth 0; public static void main(String[] args) { System.out.println( 不同栈大小的递归深度对比 \n); System.out.println(栈大小(Xss) | 大约递归深度); System.out.println(-----------|------------); System.out.println(128k | ~1000-2000); System.out.println(256k | ~3000-5000); System.out.println(512k | ~7000-11000); System.out.println(1m(默认) | ~15000-25000); System.out.println(2m | ~30000-50000); System.out.println(\n注意: 具体数值因局部变量大小而异); // 实际测试(使用默认1M) try { testDepth(); } catch (StackOverflowError e) { System.out.println(\n默认1M栈实际深度约: depth); } } private static void testDepth() { // 使用少量局部变量 int a 1, b 2, c 3; depth; testDepth(); } }2.3 栈帧大小对递归深度的影响/** * 栈帧大小与递归深度关系 * * 每个栈帧包含: * 1. 局部变量表 * 2. 操作数栈 * 3. 动态链接 * 4. 返回地址 * * 局部变量越多栈帧越大递归深度越小 */ public class StackFrameSizeImpact { // 场景1: 无局部变量 private static int depth1 0; public static void noLocalVars() { depth1; noLocalVars(); } // 场景2: 少量局部变量 private static int depth2 0; public static void fewLocalVars() { int a 1, b 2; depth2; fewLocalVars(); } // 场景3: 大量局部变量 private static int depth3 0; public static void manyLocalVars() { long a11,a22,a33,a44,a55,a66,a77,a88,a99,a1010; long b11,b22,b33,b44,b55,b66,b77,b88,b99,b1010; long c11,c22,c33,c44,c55,c66,c77,c88,c99,c1010; depth3; manyLocalVars(); } public static void main(String[] args) { System.out.println( 栈帧大小对递归深度的影响 \n); // 需要分别运行(每次JVM只执行一个测试) try { noLocalVars(); } catch (StackOverflowError e) { System.out.println(无局部变量: 递归深度 ~ depth1); } try { fewLocalVars(); } catch (StackOverflowError e) { System.out.println(少量局部变量: 递归深度 ~ depth2); } try { manyLocalVars(); } catch (StackOverflowError e) { System.out.println(大量局部变量: 递归深度 ~ depth3); } } }三、OutOfMemoryError实战: 多线程耗尽物理内存3.1 创建大量线程导致OOM/** * 多线程场景: 创建大量线程耗尽内存 - OutOfMemoryError * * 错误信息: unable to create new native thread * * 原因: * 1. 每个线程都需要分配栈内存 * 2. 操作系统进程的虚拟内存有限 * 3. 线程数 * 栈大小 超过可用内存 * * 注意: 这段代码可能导致系统卡顿请谨慎运行! */ public class ThreadStackOOMDemo { private static int threadCount 0; public static void main(String[] args) { System.out.println( 多线程栈内存OOM演示 \n); System.out.println(警告: 此代码会不断创建线程直到系统资源耗尽); System.out.println(建议在测试环境中运行\n); System.out.println(当前Xss设置: 默认约1MB); System.out.println(每个线程栈: ~1MB); System.out.println(如果创建10000个线程: ~10GB虚拟内存\n); // 取消注释来实际运行 // triggerOOM(); } // 危险! 仅在测试环境运行 public static void triggerOOM() { while (true) { new Thread(() - { try { // 线程保持存活 Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); } }, Thread- threadCount).start(); threadCount; if (threadCount % 100 0) { System.out.println(已创建线程数: threadCount); } } } }3.2 两种OOM的明确区分/** * StackOverflowError vs OutOfMemoryError 区分 * * 关键记忆点: * 1. 单线程无限递归 - StackOverflowError (逻辑栈满) * 2. 多线程不断创建 - OutOfMemoryError (物理内存满) * 3. 栈扩展失败 - 两者都可能(取决于JVM实现) */ public class ErrorDistinction { public static void main(String[] args) { System.out.println( 两种错误的明确区分 \n); System.out.println(┌─────────────────────┬───────────────────────┬─────────────────────┐); System.out.println(│ 场景 │ 错误类型 │ 原因 │); System.out.println(├─────────────────────┼───────────────────────┼─────────────────────┤); System.out.println(│ 单线程无限递归 │ StackOverflowError │ 逻辑栈深度超限 │); System.out.println(│ 过多线程创建 │ OutOfMemoryError │ 物理内存/虚拟内存满 │); System.out.println(│ 栈扩展时内存不足 │ 两者都可能(JVM决定) │ 操作系统返回ENOMEM │); System.out.println(└─────────────────────┴───────────────────────┴─────────────────────┘); } }四、HotSpot源码层面的实现机制4.1 栈扩展的分阶段处理/** * HotSpot栈扩展机制 * * 核心逻辑位于: hotspot/src/os_cpu/linux_x86/os_linux_x86.cpp * * 处理流程: * 1. 尝试在当前栈空间内扩展 (检查guard page) * 2. guard page被触及时尝试向OS申请更多栈内存 * 3. 如果OS分配失败根据情况抛出对应错误 */ public class HotSpotStackMechanism { public static void main(String[] args) { System.out.println( HotSpot栈扩展机制 \n); System.out.println(栈的三层保护机制:\n); System.out.println(第一层: Yellow Page (黄色警戒区)); System.out.println( 作用: 栈可扩展的最后缓冲区); System.out.println( 触及: 表示栈即将用完); System.out.println( 触发: 通常会抛出StackOverflowError\n); System.out.println(第二层: Red Page (红色警戒区)); System.out.println( 作用: 栈的绝对边界); System.out.println( 触及: 栈已经用完); System.out.println( 触发: 必定抛出StackOverflowError\n); System.out.println(第三层: Shadow Zone (影子区)); System.out.println( 作用: 为信号处理预留); System.out.println( 触及: 严重错误\n); System.out.println(操作系统交互:); System.out.println(HotSpot默认不允许栈动态扩展(JDK 8)); System.out.println(StackOverflowError: 触及guard page时抛出); System.out.println(OutOfMemoryError: OS无法分配线程栈时抛出); } }4.2 JVM规范的定义/** * JVM规范对栈异常的定义 * * Java Virtual Machine Specification, Java SE 8 Edition: * * Section 2.5.2 Java Virtual Machine Stacks: * * If the computation in a thread requires a larger Java Virtual Machine * stack than is permitted, the Java Virtual Machine throws a * StackOverflowError. * * If Java Virtual Machine stacks can be dynamically expanded, and * expansion is attempted but insufficient memory can be made available * to effect the expansion, or if insufficient memory can be made * available to create the initial Java Virtual Machine stack for a new * thread, the Java Virtual Machine throws an OutOfMemoryError. */ public class JVMSpecDefinition { public static void main(String[] args) { System.out.println( JVM规范的栈异常定义 \n); System.out.println(StackOverflowError 触发条件:); System.out.println( - 请求的栈深度超过允许的最大深度); System.out.println( - 典型场景: 无限递归\n); System.out.println(OutOfMemoryError 触发条件:); System.out.println( - 栈可以动态扩展但扩展时内存不足); System.out.println( - 创建新线程时无法分配初始栈内存); System.out.println( - 典型场景: 创建过多线程\n); System.out.println(关键区别:); System.out.println(StackOverflowError: 深度超限(逻辑限制)); System.out.println(OutOfMemoryError: 内存不足(物理限制)); } }五、完整实验: 两种错误对比5.1 实验代码/** * 完整对比实验: StackOverflowError vs OutOfMemoryError */ public class CompleteExperiment { // 实验1: StackOverflowError static class StackOverflowTrigger { private static int recursionDepth 0; public static void trigger() { try { deepRecursion(); } catch (StackOverflowError e) { System.out.println(\n[实验1] 捕获StackOverflowError:); System.out.println( 递归深度: recursionDepth); System.out.println( 线程名: Thread.currentThread().getName()); System.out.println( 错误类型: e.getClass().getName()); System.out.println( 线程状态: 当前线程存活(可以捕获处理)); } } private static void deepRecursion() { recursionDepth; long a 1, b 2, c 3; // 加速栈消耗 deepRecursion(); } } // 实验2: OutOfMemoryError static class OOMTrigger { private static int threadCount 0; // 注意: 此方法可能导致系统不稳定! public static void trigger() { System.out.println(\n[实验2] 尝试触发OutOfMemoryError:); System.out.println( 需要创建大量线程...); System.out.println( (此实验在演示中跳过实际执行)); // 实际代码: // try { // while (true) { // new Thread(() - { // try { Thread.sleep(Long.MAX_VALUE); } // catch (InterruptedException e) {} // }).start(); // threadCount; // } // } catch (OutOfMemoryError e) { // System.out.println( 创建线程数: threadCount); // System.out.println( 错误类型: e.getClass().getName()); // } } } public static void main(String[] args) { System.out.println( 完整对比实验 \n); System.out.println(JVM: System.getProperty(java.version)); System.out.println(OS: System.getProperty(os.name)); System.out.println(Xss: 默认(约1MB)\n); // 实验1: StackOverflowError StackOverflowTrigger.trigger(); // 实验2: OutOfMemoryError(仅打印说明) OOMTrigger.trigger(); } }5.2 实验结果对比多线程不断创建否是创建线程OS分配栈内存虚拟内存消耗内存充足?OutOfMemoryErrorunable to create native thread单线程无限递归是否递归调用栈帧入栈栈深度增加触及Guard Page?StackOverflowError线程可恢复六、面试高频问题与最佳实践6.1 面试标准答案/** * 面试高频问题 */ public class InterviewAnswers { public static void main(String[] args) { System.out.println( 面试高频问题与标准答案 \n); // Q1 System.out.println(Q1: 无限递归会报什么错?); System.out.println(A1: 单线程无限递归抛出StackOverflowError。); System.out.println( 它表示当前线程的栈深度超过JVM允许的最大值。\n); // Q2 System.out.println(Q2: 那OutOfMemoryError在栈场景下什么时候发生?); System.out.println(A2: 当创建过多线程每个线程的栈内存累积); System.out.println( 超过系统可用内存时抛出OutOfMemoryError。); System.out.println( 典型信息: unable to create new native thread\n); // Q3 System.out.println(Q3: StackOverflowError能捕获处理吗?); System.out.println(A3: 可以捕获但不建议。); System.out.println( 因为栈可能已经接近耗尽处理逻辑可能进一步消耗栈空间。); System.out.println( 最佳实践是在递归入口处增加深度限制。\n); // Q4 System.out.println(Q4: 如何避免这两种错误?); System.out.println(A4: - 递归改为迭代); System.out.println( - 设置递归深度上限); System.out.println( - 控制线程数量使用线程池); System.out.println( - 合理设置-Xss参数); } }6.2 最佳实践代码/** * 栈安全的最佳实践 */ public class StackSafePractices { // 实践1: 递归深度限制 public static class SafeRecursion { private static final int MAX_DEPTH 1000; public static int factorial(int n) { return factorialHelper(n, 0); } private static int factorialHelper(int n, int depth) { if (depth MAX_DEPTH) { throw new RuntimeException(递归深度超限: depth); } if (n 1) return 1; return n * factorialHelper(n - 1, depth 1); } } // 实践2: 递归转迭代 public static class RecursionToIteration { // 递归版本(有风险) public static int fibonacciRecursive(int n) { if (n 1) return n; return fibonacciRecursive(n - 1) fibonacciRecursive(n - 2); } // 迭代版本(安全) public static int fibonacciIterative(int n) { if (n 1) return n; int a 0, b 1; for (int i 2; i n; i) { int temp a b; a b; b temp; } return b; } } // 实践3: 线程池代替手动创建线程 public static class ThreadPoolUsage { // 危险: 每个请求创建一个线程 // new Thread(task).start(); // 安全: 使用线程池 private static final ExecutorService pool new ThreadPoolExecutor( Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors() * 2, 60, TimeUnit.SECONDS, new LinkedBlockingQueue(1000) ); public static void handleRequest(Runnable task) { pool.execute(task); } } public static void main(String[] args) { System.out.println( 栈安全最佳实践 \n); System.out.println(1. 递归设置深度上限); System.out.println(2. 优先使用迭代代替递归); System.out.println(3. 使用线程池避免无限制创建线程); System.out.println(4. 合理设置-Xss参数); System.out.println(5. 不要在catch(StackOverflowError)中做复杂操作); } }七、总结7.1 两种错误的本质区别| 维度 | StackOverflowError | OutOfMemoryError ||------|-------------------|------------------|| 触发场景 | 单线程递归太深 | 过多线程/栈无法分配 || 本质原因 | 逻辑栈深度超限 | 物理/虚拟内存不足 || 错误信息 | 通常无详细信息 | unable to create new native thread || 线程状态 | 当前线程可恢复 | 新线程创建失败 || 可捕获性 | 可捕获(但不建议) | 可捕获(意义不大) || 解决方式 | 限制递归深度/改迭代 | 控制线程数/增大内存 |7.2 核心结论单线程无限递归 → StackOverflowError(逻辑栈满) 多线程不断创建 → OutOfMemoryError(物理内存满) HotSpot默认不允许栈动态扩展 → 几乎总是StackOverflowError JVM规范允许栈扩展失败时抛出OutOfMemoryError(取决于实现)---如果本文帮你理清了栈内存中的两种错误欢迎点赞收藏。有任何疑问欢迎评论区交流