jvm学习笔记:栈帧

 2023-01-07
原文作者:xxJAY 原文地址:https://juejin.cn/post/6979589472883048456

栈帧内的数据结构

  • 局部变量表 (Local Variables):记录非静态方法的this指针、方法参数、局部变量
  • 操作数栈 (Operand Stack):用于计算的栈结构
  • 动态链接 (Dynamic Link):指向运行时常量池的方法引用
  • 方法返回地址 (Return Address):方法正常退出或异常退出的定义,以及方法间返回值传递( 注意: java中的方法除了正常返回还可能因为异常退出,所以方法返回地址不一定就是方法退出的地址)
  • 附加信息

202301011609097571.png

局部变量表

Each frame contains an array of variables known as its local variables. The length of the local variable array of a frame is determined at compile-time and supplied in the binary representation of a class or interface along with the code for the method associated with the frame

主要存储方法的参数、方法内的局部变量,数据类型包括基本数据类型、对象引用(数值地址)、返回地址。局部变量表具有以下特点:

  • 局部变量表数组的容量大小在编译时期确定,后面不会变化
  • 局部变量表是每个线程私有的,不存在线程安全问题

非静态方法的局部变量表

  • 构造方法、实例方法会自带一个this引用变量
  • static方法不能使用this的原因:静态方法的局部变量表中不带有this的引用

变量槽(Slot)

局部变量表中最基本的存储单元,每个槽大小32位

  • 局部变量表是index0开始的数组,数组的每个元素我们称为变量槽
  • 局部变量表中,32位以内类型占一个槽(int),64位占两个槽(long、double),引用类型占用一个槽
  • 调用占有两个槽的变量使用起始索引

slot重复利用

  • 栈帧中局部变量表的槽位是可以复用的,如果某个局部变量的作用域已经结束了,那么在它后面声明的局部变量可以使用它的槽位

  • 这样的设计使局部变量表的空间利用率更高,不会造成空位的情况。

    重复利用的例子:

        public void test1(){
        	int a = 0;
        	{
        		int b = 0;
        		b = a + 1;
        	}
        	int c = 1;
        }

202301011609103112.png

上面代码的局部变量表如图,可见b和c的槽位都是index=2,也就是说b的作用域结束后,c会重复使用b的槽位。

局部变量表与垃圾回收

  • 局部变量表中的变量是重要的垃圾回收节点,只有被局部变量表中直接或者间接引用的对象才不会被回收

操作数栈(Operand Stack)

Each frame contains a last-in-first-out (LIFO) stack known as its operand stack. The maximum depth of the operand stack of a frame is determined at compile-time and is supplied along with the code for the method associated with the frame

操作数栈,在方法执行的过程中根据字节码指令,往栈中写入或提取数据

  • 某些字节码指令将值压入操作数栈,其他指令又可以取出数值,使用后又可以再压入栈
  • 比如:复制操作、交换、求和
  • 字节码指令由执行引擎翻译成机器指令
  • 操作数栈是一个栈帧中的结构,它随着一个方法的开始执行而被创建
  • 每一个操作数栈都在编译时确定了固定的深度,这个深度保存在code属性的max_stack值
  • 32位占一个栈深度、64位占两个栈深度

i++和++i区别

情景1

    public void test(){
           int i1 = 10;
           i1++;
    
           int i2 = 20;
           ++i2;
       }		

202301011609109253.png

上面代码编译的字节码如上图,可见i++和++i编译出来的字节码其实是一样的,首先都是bipush指令将i的初值压入操作数栈,然后 用istore指令出栈并存入局部变量表。然后用iinc指令将局部变量表中的数据取出并+1

情景2

    public void test(){
           int i1 = 10;
           int d1 = i1++;
    
           int i2 = 20;
           int d2 = ++i2;
       }

202301011609114094.png

上面代码编译后字节码如图,可以发现用i++赋值和++i赋值的字节码是有差异的。

int d = i++的情况 :首先还是用 bipush指令把初始值放入操作数栈,然后用istore取出并存入局部变量表。在给d赋值时,会先用 iload指令读取局部变量表的 i 到操作数栈(暂时存在栈中,不出栈),然后用 iinc指令为局部变量表的 i 加1,完成后再用istore指令将操作数栈中没有加一的 i 出栈,存到新的局部变量表槽中。

int d = ++i的情况 :与上述情况唯一的不同是,这种情况会先调用 iinc 指令将局部变量表的 i 加一,然后再用 iload指令将加一后的 i存入操作数栈,最后用 istore赋值给局部变量

情景3(丧心病狂)

    public void test(){
           int i1 = 10;
    
           i1 = i1++;
    
           int i2 = 10;
           i2 = ++i2;
       }

202301011609118395.png

上述代码的字节码如上图

i = i ++情况 :首先使用了 bipush 将10 压入了操作数栈,然后使用了 istore将初值10出栈并存入局部变量表,然后用 iload指令将局部变量 10 入栈到了操作数栈(接下来的操作该数值没改变)。接着使用了 iinc 指令将局部变量 i 取出、入栈、加一、出栈、存入局部变量表。这一过程没有改变最初入栈的初值10。最后,使用了 istore 指令将这个 10 覆盖了之前加一的局部变量。所以这种情况其实 i 的值没变。

i = ++i情况 :同样,还是先用bipush、istore将初值10存入了局部变量表。因为++再i之前,所以先使用了 iinc 指令将局部变量表中的 10+1 改成了 11,然后再用 iload、istore指令来给 i 赋值,最终i会变成 11

其实通过上面两种情况的分析,我们可以发现,第二种情况下最后的iload、istore指令其实是没有实际意义的,它们只是将 i 入栈操作数栈又出栈存入原来的局部变量表位置。

动态链接(Dynamic Linking)

Each frame contains a reference to the run-time constant pool for the type of the current method to support dynamic linking of the method code

每个栈帧中包含的指向运行时常量池中该栈帧所属方法的引用

  • 指向运行时常量池的方法引用
  • 动态链接的作用就是将符号引用转换为直接引用

202301011609122776.png