跳到内容

元数据卡

  • 前置知识:方法的定义与调用、参数与返回值
  • 预计时间:15 分钟
  • 完成标志:能画出简单调用栈,理解压栈/弹栈,知道 StackOverflowError 的原因

回顾

你学会了定义方法、传参数、返回值,甚至用重载让同一个名字做不同的事。但有一个深层次的问题你还没问过:

当你调用一个方法的时候,电脑到底做了什么?

不是"跑到另一个地方执行",不是"跳过去"。是一个精确的、可预测的结构——理解它,你才算真正学会了编程。

第六幕:藏在背后的高塔

你每次调用方法,JVM 在当前线程的调用栈上压入一个栈帧

栈帧 = 一个方法调用的"快照",包含:

  • 局部变量表(你方法里声明的所有变量——形参也算)
  • 操作数栈(做运算时临时放值的地方)
  • 方法返回地址(执行完后回到哪一行)

打个比方:你走进老陈的铁匠铺,把剑坯递过去(压栈)。老陈开始打铁(执行方法)。打完了把剑还给你(返回值)。你走出铁匠铺(出栈),回到你原来的位置。

如果你在 forge 里又调用了 checkMaterial——那老陈的活没干完,他得先停下来,让你去找材料检验员。检验员检验完了,回来说"钢的,没问题",老陈再继续打。

这就形成了一座高塔

text
main 调用 forge("钢", 5)

 ├─ 压入栈帧:forge("钢", 5)
 │  执行到一半,发现需要检查材料强度

 ├─ 压入栈帧:checkStrength("钢")
 │  执行完毕,返回 "硬度8"
 │  弹出栈帧 ← 检查员走了

 ├─ 继续执行 forge,拿到结果
 │  执行完毕,返回 "精良钢剑"
 │  弹出栈帧 ← 铁匠也走了

 └─ main 继续

用代码看到它:

java
public class StackDemo {
    public static void main(String[] args) {
        System.out.println("1. main 开始");
        int result = add(3, 4);
        System.out.println("4. result = " + result);
        System.out.println("5. main 结束");
    }

    static int add(int a, int b) {
        System.out.println("2. add 开始: a=" + a + " b=" + b);
        int sum = a + b;
        System.out.println("3. add 结束 sum=" + sum);
        return sum;
    }
}
python
def add(a, b):
    print("2. add 开始: a=" + str(a) + " b=" + str(b))
    sum = a + b
    print("3. add 结束 sum=" + str(sum))
    return sum

def main():
    print("1. main 开始")
    result = add(3, 4)
    print("4. result = " + str(result))
    print("5. main 结束")

if __name__ == "__main__":
    main()
cpp
#include <iostream>

using namespace std;

int add(int a, int b) {
    cout << "2. add 开始: a=" << a << " b=" << b << endl;
    int sum = a + b;
    cout << "3. add 结束 sum=" << sum << endl;
    return sum;
}

int main() {
    cout << "1. main 开始" << endl;
    int result = add(3, 4);
    cout << "4. result = " << result << endl;
    cout << "5. main 结束" << endl;
    return 0;
}

语言:Java 21 如何运行javac StackDemo.java && java StackDemo预期输出

1. main 开始
2. add 开始: a=3 b=4
3. add 结束 sum=7
4. result = 7
5. main 结束

输出顺序反映的是调用栈的压入弹出顺序。如果嵌套更深:

java
public class StackDepth {
    public static void main(String[] args) {
        System.out.println("A: main");
        first();
        System.out.println("F: main");
    }

    static void first() {
        System.out.println("B: first");
        second();
        System.out.println("E: first");
    }

    static void second() {
        System.out.println("C: second");
        third();
        System.out.println("D: second");
    }

    static void third() {
        System.out.println("D: third");  // 注意:这里的 D 和上面的 D 是巧合
    }
}
python
def third():
    print("D: third")

def second():
    print("C: second")
    third()
    print("D: second")

def first():
    print("B: first")
    second()
    print("E: first")

def main():
    print("A: main")
    first()
    print("F: main")

if __name__ == "__main__":
    main()
cpp
#include <iostream>

using namespace std;

void third() {
    cout << "D: third" << endl;
}

void second() {
    cout << "C: second" << endl;
    third();
    cout << "D: second" << endl;
}

void first() {
    cout << "B: first" << endl;
    second();
    cout << "E: first" << endl;
}

int main() {
    cout << "A: main" << endl;
    first();
    cout << "F: main" << endl;
    return 0;
}

语言:Java 21 如何运行javac StackDepth.java && java StackDepth预期输出

A: main
B: first
C: second
D: third
D: second  ← 从这里开始往回弹
E: first
F: main

看到了吗?打印的顺序是 A→B→C→D→(最深的先结束)→D→E→F。栈是后进先出——最后调用的方法,最先执行完并弹出。

让我们画一下栈的变化过程:

时间点 1:main() 开始
┌──────────┐
│ main()   │  ← 栈底
└──────────┘

时间点 2:main → first()
┌──────────┐
│ first()  │  ← 栈顶(最新)
├──────────┤
│ main()   │
└──────────┘

时间点 3:main → first → second()
┌──────────┐
│ second() │  ← 栈顶
├──────────┤
│ first()  │
├──────────┤
│ main()   │
└──────────┘

时间点 4:main → first → second → third()
┌──────────┐
│ third()  │  ← 栈顶(最深)
├──────────┤
│ second() │
├──────────┤
│ first()  │
├──────────┤
│ main()   │
└──────────┘

时间点 5:third() 结束,弹出
┌──────────┐
│ second() │  ← 栈顶
├──────────┤
│ first()  │
├──────────┤
│ main()   │
└──────────┘

时间点 6:second() 结束,弹出
┌──────────┐
│ first()  │  ← 栈顶
├──────────┤
│ main()   │
└──────────┘

时间点 7:first() 结束,弹出
┌──────────┐
│ main()   │  ← 只剩 main
└──────────┘

陷阱一:无限递归——栈击穿了

本章只做预告:递归的完整原理与深入分析将在后续章节展开。 这里只展示其症状——希望你能意识到"没有终止条件的递归会耗尽调用栈"。

java
public class Boom {
    public static void main(String[] args) {
        dive();  // 崩溃
    }

    static void dive() {
        System.out.println("下潜…");
        dive();  // 又调自己
    }
}
python
def dive():
    print("下潜…")
    dive()  # 又调自己

def main():
    dive()

if __name__ == "__main__":
    main()
cpp
#include <iostream>

using namespace std;

void dive() {
    cout << "下潜…" << endl;
    dive();  // 又调自己
}

int main() {
    dive();
    return 0;
}

运行后你会看到一长串 "下潜…",然后程序崩溃,报错:

Exception in thread "main" java.lang.StackOverflowError

调用栈是有限的。 每个栈帧占一点内存(约几十到几百字节)。当你无限压栈——每压一帧,离边界近一步——最终栈内存耗尽,JVM 扔出一句 StackOverflowError

一个正常 Java 程序的调用栈深度是:

  • 默认大约 1000-2000 层(取决于 JVM 参数和栈帧大小)
  • 可以用 -Xss 调整:java -Xss2m Boom

实战教训:写递归一定要有终止条件

为什么你要理解调用栈?

调试崩溃时最常见的几个场景,都和调用栈有关:

  1. 看异常栈:当你看到 Exception in thread "main" java.lang.NullPointerException at MyClass.methodA(MyClass.java:10) —— 它告诉你的就是一个栈帧序列。

  2. 嵌套调用太深:也许不是无限递归,只是一个很深的函数链——每个函数内部又调了三四个函数——调用栈会迅速增长。

  3. 理解变量隔离:调用栈也解释了为什么每个方法里的变量是独立的——它们住在各自独立的栈帧里。两个方法可以声明同名的变量,各不相干。

旅人笔记

  • 每次方法调用,JVM 压入一个栈帧(包含局部变量、返回值地址等)
  • 方法执行完毕,栈帧弹出,控制权回到调用者
  • 调用栈是后进先出(LIFO)的——最后调用的,最先结束
  • 栈的空间是有限的;无限压栈会导致 StackOverflowError
  • 每个方法的变量在自己的栈帧里,同名也没关系——栈帧之间是隔离的

下一步

你已经理解了方法的核心原理——拆解、参数、返回值、重载,以及背后那个精密的调用栈。是时候用这些知识做点实际的东西了。

下一节 练习与挑战,老陈给你准备了几个小任务。

Built with VitePress | Software Systems Atlas