元数据卡
- 前置知识:方法的定义与调用、参数与返回值
- 预计时间:15 分钟
- 完成标志:能画出简单调用栈,理解压栈/弹栈,知道 StackOverflowError 的原因
回顾
你学会了定义方法、传参数、返回值,甚至用重载让同一个名字做不同的事。但有一个深层次的问题你还没问过:
当你调用一个方法的时候,电脑到底做了什么?
不是"跑到另一个地方执行",不是"跳过去"。是一个精确的、可预测的结构——理解它,你才算真正学会了编程。
第六幕:藏在背后的高塔
你每次调用方法,JVM 在当前线程的调用栈上压入一个栈帧。
栈帧 = 一个方法调用的"快照",包含:
- 局部变量表(你方法里声明的所有变量——形参也算)
- 操作数栈(做运算时临时放值的地方)
- 方法返回地址(执行完后回到哪一行)
打个比方:你走进老陈的铁匠铺,把剑坯递过去(压栈)。老陈开始打铁(执行方法)。打完了把剑还给你(返回值)。你走出铁匠铺(出栈),回到你原来的位置。
如果你在 forge 里又调用了 checkMaterial——那老陈的活没干完,他得先停下来,让你去找材料检验员。检验员检验完了,回来说"钢的,没问题",老陈再继续打。
这就形成了一座高塔:
main 调用 forge("钢", 5)
│
├─ 压入栈帧:forge("钢", 5)
│ 执行到一半,发现需要检查材料强度
│
├─ 压入栈帧:checkStrength("钢")
│ 执行完毕,返回 "硬度8"
│ 弹出栈帧 ← 检查员走了
│
├─ 继续执行 forge,拿到结果
│ 执行完毕,返回 "精良钢剑"
│ 弹出栈帧 ← 铁匠也走了
│
└─ main 继续用代码看到它:
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;
}
}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()#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 结束输出顺序反映的是调用栈的压入弹出顺序。如果嵌套更深:
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 是巧合
}
}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()#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
└──────────┘陷阱一:无限递归——栈击穿了
本章只做预告:递归的完整原理与深入分析将在后续章节展开。 这里只展示其症状——希望你能意识到"没有终止条件的递归会耗尽调用栈"。
public class Boom {
public static void main(String[] args) {
dive(); // 崩溃
}
static void dive() {
System.out.println("下潜…");
dive(); // 又调自己
}
}def dive():
print("下潜…")
dive() # 又调自己
def main():
dive()
if __name__ == "__main__":
main()#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
实战教训:写递归一定要有终止条件。
为什么你要理解调用栈?
调试崩溃时最常见的几个场景,都和调用栈有关:
看异常栈:当你看到
Exception in thread "main" java.lang.NullPointerException at MyClass.methodA(MyClass.java:10)—— 它告诉你的就是一个栈帧序列。嵌套调用太深:也许不是无限递归,只是一个很深的函数链——每个函数内部又调了三四个函数——调用栈会迅速增长。
理解变量隔离:调用栈也解释了为什么每个方法里的变量是独立的——它们住在各自独立的栈帧里。两个方法可以声明同名的变量,各不相干。
旅人笔记
- 每次方法调用,JVM 压入一个栈帧(包含局部变量、返回值地址等)
- 方法执行完毕,栈帧弹出,控制权回到调用者
- 调用栈是后进先出(LIFO)的——最后调用的,最先结束
- 栈的空间是有限的;无限压栈会导致
StackOverflowError - 每个方法的变量在自己的栈帧里,同名也没关系——栈帧之间是隔离的
→ 下一步
你已经理解了方法的核心原理——拆解、参数、返回值、重载,以及背后那个精密的调用栈。是时候用这些知识做点实际的东西了。
下一节 练习与挑战,老陈给你准备了几个小任务。