StackOverflowError最常见原因是递归未设终止条件或收敛太慢,如factorial(n)遗漏n≤1的基准情形,导致无限递归。

递归调用没设终止条件或收敛太慢
StackOverflowError 最常见原因不是栈太小,而是递归根本停不下来。比如写了个 factorial(n) 却忘了 n 的返回分支,或者用递归遍历树时没判空节点,结果无限调用下去。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 检查每条递归路径是否**必然抵达边界条件**,尤其注意边界是否可被实际到达(比如
n为负数时会不会跳过终止判断) - 在递归函数入口加日志或断点,观察前几次调用的参数变化趋势——如果
n从 10 → 9 → 8 → … → 1 → 0 → -1 → -2,说明终止条件漏了非正数分支 - 能用迭代替代的递归尽量换掉,比如深度优先遍历树,用
Stack<TreeNode>显式维护比递归更可控
-Xss 参数调大只是临时止痛,不是解药
启动 JVM 时加 -Xss2m 确实能让栈空间从默认的 1MB(64位Linux常见值)翻倍,但只是把崩溃点往后推了一点。原来递归 1000 层爆栈,调到 2MB 可能撑到 2000 层,但问题还在。
实操建议:
立即学习“Java免费学习笔记(深入)”;
-
-Xss值不宜盲目调高:每个线程都独占这份栈空间,线程多时会显著增加内存占用,甚至触发OutOfMemoryError: unable to create new native thread - 不同平台默认值差异大:Windows 上
-Xss默认常是 320k,Linux 64位常见 1MB,macOS 可能是 512k——别凭经验硬套 - 只在确认递归深度合理(比如已知最多 500 层)、且无法改代码时才用,例如老项目里嵌在三方库里的递归逻辑你没法动
隐式递归:Lambda、代理、AOP 容易踩坑
不是只有 foo() { foo(); } 才算递归。Spring AOP 的环绕通知、JDK 动态代理、甚至某些 Lambda 捕获自身引用的写法,都会导致“看不见的递归”。典型现象是抛错堆栈里反复出现同一方法名,但源码里找不到显式调用。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 查堆栈日志时注意重复出现的类/方法,特别是带
$Proxy、CGLIB、$$Lambda$字样的类名 - Spring 中 @Transactional 方法自调用(
this.method())不会走代理,但如果用了 AspectJ 编译时织入,就可能绕过这个限制变成真递归 - Lambda 写成
Function<Integer, Integer> fib = n -> n 是典型的闭包递归,等价于手动递归,一样爆栈
排查时别只盯着堆栈最上面几行
报错信息里 java.lang.StackOverflowError 后面跟着的几十行重复堆栈,其实只是“最后一击”的现场。真正的问题往往藏在靠下的某次调用里——比如第 800 行是 UserService.findUser(),第 801 行是 UserMapper.selectById(),再往下突然跳回 UserService.findUser(),说明这里触发了循环依赖或错误代理。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 用
jstack <pid>抓线程快照,比看应用日志更全,能看到完整调用链和线程状态 - IDE 调试时开启 “Show All Threads”,暂停后逐帧往上翻,重点看第一次出现“异常模式”(比如相同方法连续出现三次以上)的位置
- 加 JVM 参数
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps辅助判断:如果 GC 频繁发生且紧挨着 StackOverflowError,可能是栈溢出导致对象分配失败,引发连锁反应
栈大小和递归逻辑是两回事,调 -Xss 解不了逻辑病;而删掉一个 if 判断,可能比加 10MB 栈内存更管用。真实环境里,八成的 StackOverflowError 都卡在某个没被 review 到的边界 case 上。








