栈帧内的数据结构
- 局部变量表 (Local Variables):记录非静态方法的this指针、方法参数、局部变量
- 操作数栈 (Operand Stack):用于计算的栈结构
- 动态链接 (Dynamic Link):指向运行时常量池的方法引用
- 方法返回地址 (Return Address):方法正常退出或异常退出的定义,以及方法间返回值传递( 注意: java中的方法除了正常返回还可能因为异常退出,所以方法返回地址不一定就是方法退出的地址)
- 附加信息
局部变量表
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;
}
上面代码的局部变量表如图,可见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;
}
上面代码编译的字节码如上图,可见i++和++i编译出来的字节码其实是一样的,首先都是bipush指令将i的初值压入操作数栈,然后 用istore指令出栈并存入局部变量表。然后用iinc指令将局部变量表中的数据取出并+1
情景2
public void test(){
int i1 = 10;
int d1 = i1++;
int i2 = 20;
int d2 = ++i2;
}
上面代码编译后字节码如图,可以发现用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;
}
上述代码的字节码如上图
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
每个栈帧中包含的指向运行时常量池中该栈帧所属方法的引用
- 指向运行时常量池的方法引用
- 动态链接的作用就是将符号引用转换为直接引用