栈溢出是线程调用栈被撑爆,主因是调用过深或单次占栈过多;递归无终止条件会无限压栈直至超出默认约1MB栈上限,触发StackOverflowError,表现为同一方法在堆栈中反复出现。

栈溢出就是线程调用栈被撑爆了,不是内存不够,而是“调用太深”或“单次占栈太多”——它和堆溢出(OutOfMemoryError)是两回事,不能靠加堆内存解决。
为什么递归会直接触发 StackOverflowError
每次方法调用都会在栈上压入一个帧(frame),存参数、局部变量、返回地址。递归没终止条件,或终止条件永远不满足,就会无限压栈,直到超出线程栈上限(默认通常 1MB 左右)。
- 常见错误现象:
Exception in thread "main" java.lang.StackOverflowError,堆栈跟踪里全是同一个方法反复出现 - 典型场景:阶乘、斐波那契、树遍历等写成纯递归但没控制深度;或误把
if写成while导致逻辑死递归 - 容易踩的坑:以为“只是数据大”,其实问题在调用结构;比如
fibonacci(100)不是算得慢,是根本跑不起来 - 实操建议:
– 加if (n 这类明确出口,且确保每次递归都向出口靠近
– 深度超 1000 的递归,优先考虑改用迭代或尾递归(Java 8+ 不支持尾调用优化,别信“加@TailRec就行”这种误导)
-Xss 参数能救急,但不该是首选
它调整的是每个线程的栈空间大小,比如 java -Xss2m MyApp 把栈从默认约 1MB 提到 2MB。但这只是“扩大容器”,不解决“往里塞太多”的本质问题。
- 参数差异:
-Xss512k太小,深度递归或 native 调用(如SocketInputStream.read0)可能直接失败;-Xss4m以上对多数应用偏奢侈,尤其高并发时线程数多,总栈内存开销剧增 - 性能影响:栈变大 → 单线程更耐造,但线程总数可能被迫减少(OS 级限制或物理内存耗尽),反而降低吞吐
- 容易踩的坑:在容器环境(如 Kubernetes)里盲目加大
-Xss,导致 Pod 因 RSS 超限被 OOMKilled;或者只改了开发机参数,上线后因环境差异又崩 - 实操建议:
– 先确认是不是真需要大栈:用jstack看崩溃前最后几十帧是否真密集嵌套
– 线上慎用,优先查代码;若必须调,建议从1024k→1536k小步试,避免翻倍
不只是递归:局部变量和 native 调用也会压垮栈
很多人只盯着递归,却忽略一个方法里声明几十个大数组、或频繁调用 JNI 方法(如加密、图像处理),同样会快速吃光栈空间。
立即学习“Java免费学习笔记(深入)”;
- 常见错误现象:非递归方法突然报
StackOverflowError;或只在特定 OS(如 Linux 64位)上出错,Windows 正常 - 使用场景:
ByteBuffer.allocateDirect()虽然分配在堆外,但某些 native 实现会在栈上预分配缓冲区(例如 64KB);大量String.substring()在老 JDK 中也可能隐式引用大字符数组 - 容易踩的坑:以为“没递归就安全”,结果在循环里不断 new 对象 + 调 native 方法,每轮都压栈;或在 Lambda 里闭包了大对象,间接增加栈帧体积
- 实操建议:
– 方法内避免声明超大局部数组(如byte[] buf = new byte[1024 * 1024];),改用堆上分配 + 显式复用
– 查 JNI 调用文档,确认其栈需求;必要时在 native 层做栈检查或切分任务
– 用jstack -l查锁信息时顺带看栈深度,比只看异常堆栈更早发现问题
真正难排查的栈溢出,往往藏在框架回调、代理类生成、甚至日志格式化里——它们让调用链变得不可见。与其事后调 -Xss,不如写完递归立刻画三笔调用图,确认最坏深度是否可控。









