Skip to content

栈存储结构与栈帧内部结构

栈帧:方法调用的最小单元

前面我们说了方法调用会产生栈帧。那么一个栈帧里面到底装了什么?

JVM 规范定义了栈帧的组成结构:

┌─────────────────────────────────────────────┐
│              栈帧(Stack Frame)             │
│                                             │
│  ┌─────────────────────────────────────┐  │
│  │         局部变量表(Local Variables) │  │
│  │   slot 0 │ slot 1 │ slot 2 │ ...    │  │
│  └─────────────────────────────────────┘  │
│                                             │
│  ┌─────────────────────────────────────┐  │
│  │         操作数栈(Operand Stack)      │  │
│  │   [    ] [    ] [    ] [    ] ←top  │  │
│  └─────────────────────────────────────┘  │
│                                             │
│  ┌─────────────────────────────────────┐  │
│  │         动态链接(Dynamic Linking)    │  │
│  │      指向运行时常量池的符号引用        │  │
│  └─────────────────────────────────────┘  │
│                                             │
│  ┌─────────────────────────────────────┐  │
│  │         方法返回地址                  │  │
│  │     正常返回 / 异常退出              │  │
│  └─────────────────────────────────────┘  │
│                                             │
│  ┌─────────────────────────────────────┐  │
│  │         附加信息(可选)              │  │
│  │      调试信息、对齐填充等             │  │
│  └─────────────────────────────────────┘  │
└─────────────────────────────────────────────┘

局部变量表

局部变量表存放方法参数和方法内部定义的局部变量。

Slot:变量存储的基本单位

JVM 用 Slot(槽) 作为局部变量表的最小单位。一个 Slot 可以存放:

  • intshortbytecharbooleanfloat —— 1 个 Slot
  • longdouble —— 2 个连续的 Slot
java
public class LocalVariablesDemo {
    public static void method(long l, double d, Object o, int i) {
        // 局部变量表分配:
        // slot 0: this 引用(非 static 方法才有)
        // slot 1-2: long 类型的 l(占 2 个 Slot)
        // slot 3-4: double 类型的 d(占 2 个 Slot)
        // slot 5: Object 类型的 o
        // slot 6: int 类型的 i

        int x = 10;  // slot 7
        String s = "hello";  // slot 8
        boolean flag = true;  // slot 9
    }
}

Slot 的复用

局部变量表中的 Slot 不是每个变量独占的。当一个变量的作用域结束后,它的 Slot 可以被后续的变量复用:

java
public class SlotReuse {
    public void method() {
        {
            int a = 1;  // slot 1
            // a 的作用域在这里结束
        }
        // slot 1 现在可以复用

        int b = 2;  // 复用 slot 1
    }
}

Slot 复用会影响 GC 的行为:如果一个局部变量表中还有对对象的引用,但该引用实际上已经「不在作用域内」了(但 Slot 还被复用前的内容占用),GC 可能认为这个对象仍然可达。

局部变量的初始化

局部变量表中的变量必须经过显式赋值才能使用,否则编译失败:

java
public class UninitializedError {
    public static void main(String[] args) {
        int x;  // 未初始化
        System.out.println(x);  // 编译错误:variable x might not have been initialized
    }
}

这和类变量的默认值(零值)不同。类变量(static 变量)在准备阶段就有零值,而局部变量必须手动赋值。

操作数栈

操作数栈是一个后进先出的栈,用于执行字节码指令时的临时数据存储。

工作方式

JVM 的字节码指令大多是对操作数栈的操作:

java
public int compute() {
    int a = 1;
    int b = 2;
    int c = a + b;
    return c;
}

对应的字节码:

java
// compute() 方法的字节码
iconst_1       // 常量 1 入栈      → 栈:[1]
istore_1       // 出栈存到 slot 1  → 栈:[]
iconst_2       // 常量 2 入栈      → 栈:[2]
istore_2       // 出栈存到 slot 2  → 栈:[]
iload_1        // slot 1 入栈       → 栈:[1]
iload_2        // slot 2 入栈       → 栈:[1, 2]
iadd           // 出栈 2+1 入栈     → 栈:[3]
istore_3       // 出栈存到 slot 3  → 栈:[]
iload_3        // slot 3 入栈       → 栈:[3]
ireturn        // 返回栈顶值       → 栈:[]

操作数栈的大小

操作数栈的大小在编译时确定——编译器根据字节码的最大栈深度,为每个方法分配固定大小的操作数栈。

java
public class StackDepth {
    // 这个方法的操作数栈深度为 2
    public int add() {
        // 最多同时有 2 个值在栈上
        return 1 + 2;
    }
}

动态链接

每个栈帧都包含一个指向运行时常量池的引用,这就是动态链接。

符号引用与直接引用

字节码中引用其他类、字段、方法时,使用的是符号引用(Symbolic Reference)。在方法执行过程中,这些符号引用需要被解析成直接引用(Direct Reference)。

栈帧

  └── 动态链接 → 指向运行时常量池中的符号引用

                      │ 解析

                 直接引用


                 实际的内存地址

为什么叫「动态」

因为解析发生在运行时。对于虚方法调用,编译器在编译时不知道具体是哪个类的方法(可能是子类),只能在运行时通过动态分派确定。这需要运行时通过动态链接去查找。

方法返回地址

方法返回地址记录了方法执行完毕后应该回到哪里继续执行

两种退出方式

退出方式说明
正常返回执行引擎遇到 ireturn/lreturn/areturn/return 等返回指令
异常退出方法执行过程中抛出未捕获的异常

正常返回 vs 异常退出的区别

  • 正常返回:返回地址由 invokexxx 指令隐式保存,栈帧弹出后,PC 寄存器恢复到调用者的下一条指令
  • 异常退出:返回地址由异常处理表决定,不通过返回地址恢复执行

栈帧的内存占用估算

一个栈帧的大小取决于局部变量表和操作数栈的大小:

栈帧大小 ≈ 局部变量表大小 + 操作数栈大小 + 其他固定开销
java
public class StackFrameSize {
    // 参数 a (int) → slot 1
    // 参数 b (long) → slot 2-3(占2个)
    // 参数 c (Object) → slot 4
    // 局部变量 x (int) → slot 5
    // 局部变量 y (String) → slot 6
    // 局部变量 z (double) → slot 7-8
    // 操作数栈最大深度:3
    // 局部变量表:9 个 slot × 4 字节 ≈ 36 字节
    // 操作数栈:3 × 4 字节(最大深度)≈ 12 字节
    // 加上动态链接、返回地址等固定开销
    // 总计大约 64~96 字节每个栈帧
    public static void method(int a, long b, Object c) {
        int x = 10;
        String y = "hi";
        double z = 3.14;
    }
}

本节小结

栈帧由五个部分组成:

组成部分作用关键点
局部变量表存储方法参数和局部变量Slot 为单位,long/double 占 2 Slot
操作数栈临时计算数据的存放最大深度编译时确定
动态链接指向运行时常量池符号引用 → 直接引用
方法返回地址记录返回位置正常返回 vs 异常退出
附加信息调试信息等可选

下一节,我们来看 局部变量表(slot/静态vs局部变量),深入理解局部变量表的细节。

基于 VitePress 构建