
网罗开发 (小红书、快手、视频号同名)
大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。
图书作者:《ESP32-C3 物联网工程开发实战》
图书作者:《SwiftUI 入门,进阶与实战》
超级个体:COC上海社区主理人
特约讲师:大学讲师,谷歌亚马逊分享嘉宾
科技博主:华为HDE/HDG
我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验 。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。
展菲:您的前沿技术领航员
👋 大家好,我是展菲!
📱 全网搜索"展菲",即可纵览我在各大平台的知识足迹。
📣 公众号"Swift社区",每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。
💬 微信端添加好友"fzhanfei",与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。
📅 最新动态:2025 年 3 月 17 日
快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!
文章目录
-
- 前言
- [什么是 StackOverflowError](#什么是 StackOverflowError)
- [一个最典型的爆栈示例(可运行 Demo)](#一个最典型的爆栈示例(可运行 Demo))
- 爆栈在真实场景中为什么会发生?
-
- [1. 不小心写了错误的递归出口](#1. 不小心写了错误的递归出口)
- [2. 数据结构出现循环引用](#2. 数据结构出现循环引用)
- [3. JSON 序列化 / toString() 调用死循环](#3. JSON 序列化 / toString() 调用死循环)
- [4. Spring AOP 代理链写错,方法自己反复调用自己](#4. Spring AOP 代理链写错,方法自己反复调用自己)
- [如何解决 StackOverflowError](#如何解决 StackOverflowError)
- [解决方案 1:检查递归条件(最根本方法)](#解决方案 1:检查递归条件(最根本方法))
- [解决方案 2:把递归改写为循环(适合大规模递归)](#解决方案 2:把递归改写为循环(适合大规模递归))
- [解决方案 3:增大 JVM 栈空间(临时解决)](#解决方案 3:增大 JVM 栈空间(临时解决))
- [一个合理的真实场景 Demo(带完整解析)](#一个合理的真实场景 Demo(带完整解析))
- [实际项目中如何定位 StackOverflowError](#实际项目中如何定位 StackOverflowError)
- 总结
前言
在日常开发中,特别是写 Java、Kotlin 这种基于 JVM 的语言时,你八成会在某个时刻遇到一个经典错误:StackOverflowError。很多同学第一次看到它会有点懵,但其实它的本质非常简单,通常就是一句话:递归没收住。
今天我们就用轻松的方式聊聊 StackOverflowError 到底怎么产生、怎么定位、怎么解决,并给出可运行的 Demo 让你真正理解"为什么你的程序突然爆栈"。
什么是 StackOverflowError
简单说,线程调用方法时,JVM 会为每一次方法调用开辟一段栈帧,记录参数、局部变量、返回地址等。当方法层层调用方法,或者递归无限深入,就会不断压栈。
当压栈超过 JVM 分给当前线程的栈空间(默认 1M 左右),JVM 就会直接抛一个:
txt
Exception in thread "main" java.lang.StackOverflowError
这就是 StackOverflowError,不是异常,是 Error,意味着你连 try-catch 都救不了它。
在绝大多数场景里,这类错误几乎都是无限递归造成的。
一个最典型的爆栈示例(可运行 Demo)
下面这段代码只做一件事:自己调用自己,而且没有终止条件。
java
public class StackOverflowDemo {
public static void main(String[] args) {
call();
}
public static void call() {
call(); // 递归没有出口 -> 无限调用 -> 栈溢出
}
}
运行这段代码几秒钟后,你就能看到 StackOverflowError。
为什么会溢出?
call() → call() → call() ...
JVM 不停给每次调用开栈帧,但永远没有返回,也没有停止,一直到栈被占满为止。
这就像你不停往背包里塞石头,背包容量不变,最终只能爆开。
爆栈在真实场景中为什么会发生?
虽然上面代码看起来很蠢,但现实里类似场景是非常常见的,例如:
1. 不小心写了错误的递归出口
java
public int sum(int n) {
if (n == 0) return 0;
return n + sum(n--); // 这里应该是 n - 1
}
由于 n-- 是先使用再减,导致传入下一层的仍然是原来的 n,最终会无限递归。
2. 数据结构出现循环引用
例如树或图结构中,节点的 children 循环指向自己。
3. JSON 序列化 / toString() 调用死循环
比如 lombok 的 @Data 自动生成的 toString() 也可能因为循环引用爆栈。
4. Spring AOP 代理链写错,方法自己反复调用自己
这个是后端常见事故。
如何解决 StackOverflowError
解决方案 1:检查递归条件(最根本方法)
递归必须要有:
- 明确的出口(终止条件)
- 每次递归都要向出口逼近
下面是一个正确的递归写法:
java
public int factorial(int n) {
if (n <= 1) return 1; // 出口
return n * factorial(n - 1); // 向出口逼近
}
只要你的递归能保证 逐步收敛,就不会爆栈。
解决方案 2:把递归改写为循环(适合大规模递归)
很多情况下递归能实现的逻辑,循环也能实现,而且不会占用越来越多的栈空间。
递归写法(可能爆栈)
java
public int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
如果 n = 50,这个写法很容易爆栈。
改成循环写法(稳定)
java
public int fibLoop(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int c = a + b;
a = b;
b = c;
}
return b;
}
循环内只使用局部变量,不会无限压栈。
解决方案 3:增大 JVM 栈空间(临时解决)
有时候你确实需要深度递归(比如大规模 DFS),这时可以通过 -Xss 调整线程栈大小:
bash
java -Xss2m StackOverflowDemo
默认 1M,设置 2M、4M 都可以。但注意:
调大栈不是根本解决办法,只是延迟溢出时间。
如果递归本身写得不对,即便给你 1GB 栈也会爆。
一个合理的真实场景 Demo(带完整解析)
假设你在做一个"目录扫描工具",需要递归遍历文件夹:
java
public class FileScanner {
public static void main(String[] args) {
scanDir(new File("/Users/me/project"));
}
public static void scanDir(File dir) {
if (!dir.isDirectory()) return;
System.out.println("扫描目录:" + dir.getAbsolutePath());
File[] files = dir.listFiles();
if (files == null) return;
for (File f : files) {
if (f.isDirectory()) {
scanDir(f); // 深度遍历
}
}
}
}
可能会出问题的地方
如果某些目录存在软链接或循环引用,比如:
txt
A -> B
B -> A
然后你就会:
scanDir(A) → scanDir(B) → scanDir(A) 无限递归。
改进方式:记录访问过的目录
java
private static Set<String> visited = new HashSet<>();
public static void scanDir(File dir) {
if (!dir.isDirectory()) return;
String path = dir.getAbsolutePath();
if (visited.contains(path)) return;
visited.add(path);
System.out.println("扫描目录:" + path);
File[] files = dir.listFiles();
if (files == null) return;
for (File f : files) {
if (f.isDirectory()) {
scanDir(f);
}
}
}
这样就能避免循环引用导致的爆栈。
实际项目中如何定位 StackOverflowError
-
看错误堆栈的最后几行
通常你会看到相同的方法不断重复出现 100 多行,就是递归位置。
-
检查代码中是否有互相调用的方法链
A 调 B,B 调 A,也是无限递归。
-
确认递归调用参数是否正确递减
常见 Bug:
n--而不是n - 1。 -
调试工具(IDE)中加断点检查递归深度
IntelliJ 会提示 "method call is too deep"。
总结
StackOverflowError 本质上很简单:你的方法调用太深了,超过 JVM 栈的容量。
解决方案也很清晰:
- 先检查递归出口(核心)
- 能用循环就用循环(稳定)
- 必要时加大 JVM 栈大小(应急)
写递归最怕"看起来没问题",但其实没有向终点收敛。
只要你把递归终止条件写好,StackOverflowError 基本不会找上你。