JVM运行时栈帧

在JVM中,每个线程都包含n个栈帧,每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。

栈帧的生命周期随着方法的创建而创建,随着方法的结束而销毁,无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算方法的结束。

在某条线程执行过程中的某个时间点上,只有目前正在执行的那个方法的栈帧是活动的。这个栈帧称为当前栈帧,这个栈帧对应的方法称为当前方法,定义这个方法的类称为当前类对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的局部变量表和操作数栈所进行的操作。

**注意:**栈帧是线程本地私有的数据,不可能在一个栈帧 之中引用另外一个线程的栈帧

图片

局部变量表

局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数方法内部定义的局部变量

存储方法

局部变量表的容量以变量槽(Variable Slot)为最小单位,一般在虚拟机中,一个Slot占用32位存储空间(这不是固定的,虚拟机可以自行改变每个槽占用空间的大小,但一般都是32位)。

Java虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的变量槽数量。如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽,如果访问的是64位数据类型的变量,则说明会同时使用第N和N+1两个变量槽。

eg:

在Java中,long在内存占64位,所以局部变量表用2个slot来存储

图片

对于两个相邻的共同存放一个64位数据的两个变量槽,虚拟机不允许采用任何方式单独访问其中的某一个,《Java虚拟机规范》中明确要求了如果遇到进行这种操作的字节码序列,虚拟机就应该在类加载的校验阶段中抛出异常。

long和double的非原子性协定

在Java内存模型中,对于64位的数据类型(long和double),在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性,这就是所谓的**“long和double的非原子性协定”(Non-Atomic Treatment of doubleand long Variables)**。

虽然有这个协定,但是,由于局部变量表(Local Variable Table)是建立在线程堆栈中的,属于线程私有的数据,无论读写两个连续的变量槽是否为原子操作,都不会引起数据竞争和线程安全问题

初始值问题

我们已经知道类的字段变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。

局部变量就不一样了,如果一个局部变量定义了但没有赋初始值,那它是完全不能使用的。所以不要认为Java中任何情况下都存在诸如整型变量默认为0、布尔型变量默认为false等这样的默认值规则。

eg:

// 这个方法会报:
// Error:(12, 28) java: variable y might not have been initialized
public class JVMTest {
    public static void main(String[] args) {
        int y;
        int z=3;
        System.out.println(y+z);
    }
}


// 这个会正常输出 3; 因为int的初始值为0
public class JVMTest {
    private static int y;
    public static void main(String[] args) {
        int z=3;
        System.out.println(y+z);
    }
}

操作数栈

操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。Javac编译器的数据流分析工作保证了在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。

eg:

public class JVMTest {
    public static void main(String[] args) {
        long y=9223372036854775800L;
        int z=2;
        long x=y+z;
    }
}

我们用javap -verbose JVMTest来查看他的class文件的字节码指令

图片

在操作栈中的流程大致为:

图片

动态链接

每个栈帧都包含一个指向当前方法所在类型的运行时常量池的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。在Class文件里,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用(symolic reference)来表示,动态链接的作用就是将这些以符号引用所表示的方法转换为实际方法的直接引用。

什么是符号引用?

图片

通过查看字节码,上面的#7#8#9等等都是符号引用,他在class文件里只是个符号,就像你定义一个变量名称一样,变量名只是和字符符号,并不是真正的指向内存的地址指针。这些符号都指向运行时常量池的引用。

方法返回地址

Java在调用方法时,只有两种返回方法,一种是正常返回,一种是异常返回

正常返回

正常返回指的就是在执行方法时,中间并没有异常抛出,或者已正确处理抛出的异常,这时就称当前方法正常调用完成,如果有返回值,就会给他调用者返回一个值,如果没有返回值(void)就正常返回。

这种场景下,当前栈帧承担着恢复调用者状态的责任,**包括恢复调用者的局部变量表和操作数栈,以及正确递增程序计数器,以跳过刚才执行的调用方法指令等。**调用者的代码在被调用方法的返回值压入调用者栈帧的操作数栈后,会正常执行。

异常返回

在调用一些方法时,一些异常没有被正确捕获,就会导致方法终止,此时称方法异常调用完成,那一定不会有方法返回值返回给其调用者。

无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。

怎么理解这个必须返回到最初方法被调用时的位置呢?

eg:

图片

上面异常是在13行发生的,但是它并没有停在13行,而是回到了最初调用它第10行的位置。