1.栈帧
2.运行时栈帧结构
2.1简介
Java虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈(Virtual MachineStack)的栈元素。
栈帧是线程本地的私有数据,不可能在一个栈帧中引用另外一个线程的栈帧
栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态连接、方法返回值和异常分派。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法正常完成还是异常完成都算作方法结束。
栈帧的存储空间由创建它的线程分配在Java虚拟机栈之中,每一个栈帧都有自己的本地变量表(局部变量表)、操作数栈和指向当前方法所属的类的运行时常量池的引用
2.2栈帧存储内容
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程
2.3栈帧的大小
在编译Java程序源码的时候,栈帧中需要多大的局部变量表,需要多深的操作数栈就已经被分析计算出来,并且写入到方法表的Code属性之中
2.4当前栈帧和当前方法
一个线程中的方法调用链可能会很长,以Java程序的角度来看,同一时刻、同一条线程里面,在调用堆栈的所有方法都同时处于执行状态。
而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”(Current Stack Frame),与这个栈帧所关联的方法被称为“当前方法”(Current Method)。
3局部变量表
3.1简介
局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量
3.2大小
在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量
3.3存储单元
3.3.1存储单元-变量槽
局部变量表的容量以变量槽(Variable Slot)为最小单位,《Java虚拟机规范》中并没有明确指出一个变量槽应占用的内存空间大小,只是很有导向性地说到每个变量槽都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。这8种数据类型,都可以使用32位或更小的物理内存来存储,但这种描述与明确指出“每个变量槽应占用32位长度的内存空间”是有本质差别的,它允许变量槽的长度可以随着处理器、操作系统或虚拟机实现的不同而发生变化,保证了即使在64位虚拟机中使用了64位的物理内存空间去实现一个变量槽,虚拟机仍要使用对齐和补白的手段让变量槽在外观上看起来与32位虚拟机中的一致
3.3.2存储32位以内的数据
boolean、byte、char、short、int、float、reference[1]和returnAddress这8种类型直接存储在一个变量槽。前面6种不需要多加解释,读者可以按照Java语言中对应数据类型的概念去理解它们(仅是这样理解而已,Java语言和Java虚拟机中的基本数据类型是存在本质差别的),而第7种reference类型表示对一个对象实例的引用,《Java虚拟机规范》既没有说明它的长度,也没有明确指出这种引用应有怎样的结构。但是一般来说,虚拟机实现至少都应当能通过这个引用做到两件事情,一是从根据引用直接或间接地查找到对象在Java堆中的数据存放的起始地址或索引,二是根据引用直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,。第8种returnAddress类型目前已经很少见了,它是为字节码指令jsr、jsr_w和ret服务的,指向了一条字节码指令的地址,某些很古老的Java虚拟机曾经使用这几条指令来实现异常处理时的跳转,但现在也已经全部改为采用异常表来代替了
3.3.3存储64位的数据类型
对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。Java语言中明确的64位的数据类型只有long和double两种。
3.4访问局部变量表数据
Java虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的变量槽数量。
如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽
如果访问的是64位数据类型的变量,则说明会同时使用第N和N+1两个变量槽。对于两个相邻的共同存放一个64位数据的两个变量槽,虚拟机不允许采用任何方式单独访问其中的某一个
3.5方法调用时,局部变量表存储数据
当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。
如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量槽,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的变量槽
3.6重用
为了尽可能节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的变量槽就可以交给其他变量来重用
3.7局部(成员)变量需要赋值才能使用
类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。因此即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值,不会产生歧义。但局部()变量就不一样了,如果一个局部变量定义了但没有赋初始值,那它是完全不能使用的
4.操作数栈
4.1简介
操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈
4.2大小
操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中
4.3存储
操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。
32位数据类型所占的栈容量为1
64位数据类型所占的栈容量为2
4.4访问
虽然操作数栈是使用数组的方式来实现的,但是操作数栈只能用入栈和出栈的操作来完成一次数据访问,而不能使用索引的方式
4.5方法调用时,操作数栈存入和取出数据
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。
譬如在做算术运算的时候是通过将运算涉及的操作数栈压入栈顶后调用运算指令来进行的又譬如在调用其他方法的时候是通过操作数栈来进行方法参数的传递。
举个例子,例如整数加法的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈
4.6操作数栈的元素类型和指令严格匹配
以上面的iadd指令为例,这个指令只能用于整型数的加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况
4.7示例
4.7.1准备一段简单代码
package jvn;
public class OperandStackTest {
public void testAddOperation() {
//byte、short、char、boolean:都以int型来保存
byte i = 15;
int j = 8;
int k = i + j;
}
}
4.7.2使用Javap -v OperandStackTest 反编译上面的java代码产生的OperandStackTest.class。
D:\Eclipse_workspace\JVM\jvn\bin\jvn>javap -v OperandStackTest.class
Classfile /D:/Eclipse_workspace/JVM/jvn/bin/jvn/OperandStackTest.class
Last modified 2020-7-3; size 429 bytes
MD5 checksum 6b1bb437ad85e3c11b4d35892faaec7a
Compiled from "OperandStackTest.java"
public class jvn.OperandStackTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // jvn/OperandStackTest
#2 = Utf8 jvn/OperandStackTest
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ljvn/OperandStackTest;
#14 = Utf8 testAddOperation
#15 = Utf8 i
#16 = Utf8 B
#17 = Utf8 j
#18 = Utf8 I
#19 = Utf8 k
#20 = Utf8 SourceFile
#21 = Utf8 OperandStackTest.java
{
public jvn.OperandStackTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljvn/OperandStackTest;
public void testAddOperation();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 15
2: istore_1
3: bipush 8
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
LineNumberTable:
line 6: 0
line 7: 3
line 8: 6
line 9: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Ljvn/OperandStackTest;
3 8 1 i B
6 5 2 j I
10 1 3 k I
}
SourceFile: "OperandStackTest.java"
4.7.3 code这段分析
stack=2, locals=4, args_size=1
0: bipush 15
2: istore_1
3: bipush 8
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
stack=2, locals=4, args_size=1 : stack=2表示的是操作数栈的大小为2。locals=4表示的是局部变量表的长度是4。
在刚开始执行这个方法的时候,PC寄存器记录了下一条要执行的字节码指令的起始地址。局部变量表和操作数栈都是空的。
1)第一条指令是0: bipush 15。表示的是将数字15压入操作数栈。
2)第二条指令是 2: istore_1。i表示的是存储在操作数栈中的数据的类型是int。
istore_1表示的是将操作数栈中的数据取出来放到局部变量表的索引等于1的位置。(为什么不是放在局部变量表索引为0的位置?因为对于非静态的方法,局部变量表中索引为0的位置存放的是this)。
3)第三条指令是 3: bipush 8。表示的是将数字8压入操作数栈。
4)第四条指令是 5: istore_2。表示的是将操作数栈中的数字8取出来放入局部变量表中索引为2的位置。
5)第五条指令是 6: iload_1。表示的是将数字15从局部变量表中索引为1的位置取出来压入操作数栈。
6)第六条指令是 7: iload_2。表示的是将数字8从局部变量表中索引为2的位置取出来压入操作数栈。
这时候操作数栈中有两个数据:8和15。
7)第七条指令8: iadd。表示的是将操作数栈中的两个数据出栈并且做一个求和运算,最后将求得的结果23压入操作数栈。
8)第八条指令9: istore_3。表示的是将操作数栈中的数据23出栈并且放入局部变量表的索引为3的位置。
9)最后的指令 10: return。表示的是直接返回。
5.动态连接
5.1简介
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接
6.方法返回地址
6.1简介
在方法退出之后,都必须返回到最初方法(调用当前方法的方法)被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等
6.2退出方法的两种情况
6.2.1正常退出
执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为“正常调用完成”
6.2.2异常退出
在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理(try catch)。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用完成(Abrupt Method Invocation Completion)”。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的