StackOverflowError 栈溢出的原因与实战解决方案


网罗开发 (小红书、快手、视频号同名)

大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括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:检查递归条件(最根本方法)

递归必须要有:

  1. 明确的出口(终止条件)
  2. 每次递归都要向出口逼近

下面是一个正确的递归写法:

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

  1. 看错误堆栈的最后几行

    通常你会看到相同的方法不断重复出现 100 多行,就是递归位置。

  2. 检查代码中是否有互相调用的方法链

    A 调 B,B 调 A,也是无限递归。

  3. 确认递归调用参数是否正确递减

    常见 Bug:n-- 而不是 n - 1

  4. 调试工具(IDE)中加断点检查递归深度

    IntelliJ 会提示 "method call is too deep"。

总结

StackOverflowError 本质上很简单:你的方法调用太深了,超过 JVM 栈的容量

解决方案也很清晰:

  1. 先检查递归出口(核心)
  2. 能用循环就用循环(稳定)
  3. 必要时加大 JVM 栈大小(应急)

写递归最怕"看起来没问题",但其实没有向终点收敛。

只要你把递归终止条件写好,StackOverflowError 基本不会找上你。

相关推荐
字节拾光录2 小时前
手机号存储避坑指南:从20亿级数据库实践看,为什么VARCHAR才是终极答案
java·数据库·oracle
p***97612 小时前
SpringBoot(7)-Swagger
java·spring boot·后端
j***29482 小时前
springboot集成onlyoffice(部署+开发)
java·spring boot·后端
张较瘦_2 小时前
Springboot | Spring Boot 3 纯 JDBC 实现宠物管理系统增删改查(无 ORM 框架)
spring boot·后端·数据库开发
h***67374 小时前
SpringBoot整合easy-es
spring boot·后端·elasticsearch
叫致寒吧6 小时前
Tomcat详解
java·tomcat
S***267510 小时前
基于SpringBoot和Leaflet的行政区划地图掩膜效果实战
java·spring boot·后端