5.5 调用栈、栈帧与栈轨迹
前置知识:方法参数与返回值
预计时间:35 分钟
完成标志:能从栈轨迹反向还原调用链,并理解运行时优化边界
“这就是调用栈的模型:后进入的方法先返回。”
每个线程有自己的 JVM 栈
当线程调用方法时,JVM 为该调用创建帧。规范层面的帧包含局部变量区、操作数栈,以及对当前类运行时常量池等信息的引用;它还参与返回和异常传播。
线程的 JVM 栈(概念图)
┌──────────────┐
│ calculateTax │ ← 当前执行
├──────────────┤
│ checkout │
├──────────────┤
│ main │
└──────────────┘“栈帧就是局部变量 + 返回地址”适合第一步,但不完整;“每帧几十到几百字节”也不是可移植保证。帧布局、栈大小和优化由 JVM 实现、方法字节码和运行参数共同决定。
用日志观察进入和返回
public class StackDemo {
static int add(int left, int right) {
System.out.println("2. enter add");
int result = left + right;
System.out.println("3. leave add");
return result;
}
public static void main(String[] args) {
System.out.println("1. enter main");
int result = add(3, 4);
System.out.println("4. result=" + result);
System.out.println("5. leave main");
}
}调用 add 时,main 尚未结束,它的执行上下文留在下层。add 返回后,调用点得到结果,main 继续。
异常沿调用链传播
static void load() {
parse();
}
static void parse() {
Integer.parseInt("not-a-number");
}
public static void main(String[] args) {
load();
}若没有匹配的捕获点,异常从 parse 传播到 load、再到 main,线程最终以未捕获异常结束。栈轨迹通常先显示异常类型和信息,随后列出抛出点及调用者:
NumberFormatException: ...
at ...parse(...)
at ...load(...)
at ...main(...)排错时先找到属于自己代码的最早相关帧,再沿调用者向下理解输入来自哪里。框架、反射、异步边界可能增加或切断直观链路;Caused by 还表示异常因果链,不等同普通调用层级。
递归不断增加活动调用
static long factorial(int value) {
if (value < 0) {
throw new IllegalArgumentException("negative value");
}
if (value > 20) {
throw new ArithmeticException("factorial exceeds long: " + value);
}
if (value <= 1) {
return 1L;
}
return Math.multiplyExact(value, factorial(value - 1));
}每层在下一层返回前保持活动。缺少可达终止条件可能触发 StackOverflowError。
不要写“递归深度超过某个固定数就溢出”。可用深度受线程栈配置、JVM、平台、编译状态和每帧需求影响。StackOverflowError 属于 Error,通常不应捕获后假装程序能安全继续;修正算法或输入界限。
JIT 优化与概念模型
JVM 可即时编译并内联小方法,所以物理机器栈不一定逐个对应源码方法。JVM 必须维持 Java 可观察语义,并可在需要时重建或报告足够的状态。教程中的栈图是理解调用、局部作用域、返回和异常的语言/JVM 模型,不是处理器寄存器和机器帧的实时截图。
这也说明“多写一个小方法一定增加一次昂贵栈调用”并不成立。先写清楚、测量热点,再根据剖析结果优化。
每个线程的栈彼此独立
两个线程调用同一方法时,各自拥有形参和局部变量帧,不会因为局部变量同名而共享。它们仍可能通过静态字段、共享对象、文件或数据库相互影响。线程安全问题主要来自共享可变状态,而不是局部变量名字。
专业速查
| 目标 | 观察点 |
|---|---|
| 还原同步调用链 | 栈轨迹帧顺序与源码行 |
| 找根因 | 异常类型、首个相关业务帧、Caused by 链 |
| 判断局部隔离 | 每线程独立帧;检查引用是否指向共享对象 |
| 递归安全 | 可达基线、输入上界、栈深不固定、数值溢出 |
| 性能判断 | 不从源码方法数猜,使用 profiler/JFR 等测量 |
官方依据:JVMS 17 §2.5.2 Java Virtual Machine Stacks 与 §2.6 Frames。
离开这一页前
画出 main → checkout → calculateTax 的活动帧顺序,再从一段带 Caused by 的栈轨迹中分别标出调用链和异常因果链。帧大小、递归极限和源码方法是否对应物理机器帧都不能写成跨 JVM 的固定结论。
→ 下一步:递归入门