Java 虚拟机以方法作为最基本的执行单位,栈帧支持着方法调用和方法执行,是虚拟机栈的栈元素,虚拟机栈在下面这篇文章中已经介绍过:

栈帧的结构如下图:

栈帧的概念结构

局部变量表

作用

局部变量表是一组变量值的存储空间,存放方法参数和方法内部定义的局部变量。在编译的时候该方法的局部变量表的最大容量已经确定。

存放的数据类型

局部变量表存放了编译期可知的各种 Java 虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,可能是一个指向对象的直接指针,也可能指向一个代表对象的句柄)和 returnAddress 类型(指向了一条字节码指令的地址)。

存储空间

局部变量表的存储空间以局部变量槽(Slot)为最小单位,其中64位长度的 long 和 double 类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小,这里指变量槽的数量不变,但虚拟机真正使用多大的内存空间(譬如1个变量槽占用32个bit、64个bit或者更多)来实现一个变量槽,这是由具体的虚拟机实现自行决定的。

方法调用时的空间分配

当一个方法被调用时,Java 虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果执行的是实例方法(没有被 static 修饰的方法),那局部变量表中第0位索引的变量槽默认用于传递方法所属对象实例的引用,在方法中可以通过关键字this来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量槽,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的变量槽。

变量槽的复用

局部变量表中的变量槽是可以复用的,如果当前字节码 PC 计数器的值超过了某个变量的作用域,那这个变量的变量槽就可给其他的变量复用。这样设计可以节省栈帧空间,但可能会影响系统的垃圾收集行为。例子如下:

public static void main(String[] args){
    byte[] placeholder = new byte[64*1024*1024];
    System.gc();
}

上述代码向内存填充了64MB的数据,然后进行垃圾收集,执行代码会发现没有回收掉这64MB内存,因为执行 gc 时变量placeholder还处于作用域之内。

public static void main(String[] args){
    {
        byte[] placeholder = new byte[64*1024*1024];
    }
    System.gc();
}

上述代码placeholder的作用域被限制在花括号之内,从代码逻辑上讲,在执行 gc 时placeholder已经不能再被访问了,但执行代码发现这64MB内存还是没有被回收,原因看下面一个例子。

public static void main(String[] args){
    {
        byte[] placeholder = new byte[64*1024*1024];
    }
    int a = 0;
    System.gc();
}

执行上述代码发现内存被回收了,根本原因是placeholder的变量槽被复用了。在第二段代码中虽然已经离开了变量的作用域,但变量占用的变量槽没有被其他变量复用,而局部变量表作为 GC Roots 的一部分与这个变量保持着关联,因此不会被回收。第三段代码中变量a复用了placeholder的变量槽,placeholder断开了与 GC Roots 的引用链,也就是不可达,因此被垃圾回收。

操作数栈

操作数栈是一个后入先出栈,也是在编译的时候就确定了最大深度。

栈帧的重叠

栈帧的重叠

在虚拟机的实现中会让两个栈帧一部分重叠,让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠,这样在调用方法时调用方将实参存储在操作数栈中,而被调用方可以直接从自身局部变量表中共享的部分取出方法参数。

栈帧信息

一般把动态连接、方法返回地址和其他附加信息统一叫作栈帧信息。

动态连接

每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用,为了支持方法调用的动态连接:就是在每一次方法运行时才将符号引用转化为直接引用。

方法返回地址

有两种方式退出一个方法:

  1. 执行引擎遇到方法返回的字节码指令。这时候可能有返回值传递给调用者,这种退出方式叫正常调用完成。
  2. 在方法执行过程中遇到异常,并且没有在方法体中妥善处理,即在本方法的异常表中没有搜索到匹配的异常处理器。这时候不会给调用者提供返回值,这种退出方式叫异常调用完成。

为了方法退出时能够返回到方法被调用时的位置,栈帧中可能保存一些相关信息。方法正常退出时,主调方法的 PC 计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值;方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。

最后修改:2023 年 06 月 05 日
如果觉得我的文章对你有用,请随意赞赏