跳到内容

第 1 卷 / 第 5 章 / 第 5 课

5.5 调用栈、栈帧与栈轨迹 ​

前置知识:方法参数与返回值

预计时间:35 分钟

完成标志:能从栈轨迹反向还原调用链,并理解运行时优化边界

“这就是调用栈的模型:后进入的方法先返回。”

每个线程有自己的 JVM 栈 ​

当线程调用方法时,JVM 为该调用创建帧。规范层面的帧包含局部变量区、操作数栈,以及对当前类运行时常量池等信息的引用;它还参与返回和异常传播。

text
线程的 JVM 栈(概念图)
┌──────────────┐
│ calculateTax │ ← 当前执行
├──────────────┤
│ checkout     │
├──────────────┤
│ main         │
└──────────────┘

“栈帧就是局部变量 + 返回地址”适合第一步,但不完整;“每帧几十到几百字节”也不是可移植保证。帧布局、栈大小和优化由 JVM 实现、方法字节码和运行参数共同决定。

用日志观察进入和返回 ​

java
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 继续。

异常沿调用链传播 ​

java
static void load() {
    parse();
}

static void parse() {
    Integer.parseInt("not-a-number");
}

public static void main(String[] args) {
    load();
}

若没有匹配的捕获点,异常从 parse 传播到 load、再到 main,线程最终以未捕获异常结束。栈轨迹通常先显示异常类型和信息,随后列出抛出点及调用者:

text
NumberFormatException: ...
    at ...parse(...)
    at ...load(...)
    at ...main(...)

排错时先找到属于自己代码的最早相关帧,再沿调用者向下理解输入来自哪里。框架、反射、异步边界可能增加或切断直观链路;Caused by 还表示异常因果链,不等同普通调用层级。

递归不断增加活动调用 ​

java
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 的固定结论。

→ 下一步:递归入门

Built with VitePress | Software Systems Atlas