八.运行时栈帧结构以及方法调用
- 运行时栈帧结构
1.局部变量表:
1. 是一组变量值的储存空间,用于存放方法参数和方法内部定义的局部变量。被编译为Class文件时,就确定了该方法所需分配的最大容量。
2. 以变量槽为最小单位,每个变量槽应该能存放一个boolean,byte,short,int,float,referrnce和returnAddress类型的数据,它允许变量槽的长度可以随着处理器,操作系统或虚拟机的不同而变化。
3. reference类型表示对一个对象实例的引用,作用:1.根据引用直接或间接地查找到对象在堆中的数据存放的起始地址或索引;2.根据引用直接或间接地查找到对象所属数据类型在方法区储存的类型信息。
4. returnAddress目前已经很少见了,曾经用于实现异常处理的跳转,现在也全部改用异常表来代替了。
5. 对于64位的数据类型,虚拟机会以高位对齐的方式为其分配2个连续的变量槽空间。PS:由于局部变量表示建立在线程堆栈中的,线程私有,都不会引起数据竞争和线程安全问题。
6. 虚拟机通过索引定位的方式使用局部变量表。范围是从0开始至最大的变量槽数量。32位的变量,索引N就代表使用第N个变量槽,64位的话。会同时使用N和N+12个变量槽。对于2个相邻的共同存放一个64位数据的2个变量槽,不允许采用任何方式单独访问其中一个,如果遇到,在类加载的校验阶段抛出异常。
7. 当一个方法被调用时,虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果执行的是实例方法(没有被static修饰),局部变量表第0位索引的变量槽默认用于传递方法所属对象实例的引用,即this。参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的变量槽。
8. 为了节省栈帧耗用的内存空间,变量槽是可以重用的。如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量槽可以交给其他变量来重用。PS:副作用:超出了作用域,但在此之后,再没有发生过任何对局部变量表的读写操作,原来占用的变量槽还没有被其他变量所复用,仍然保持着对它的关联。
9. 如果一个局部变量定义了但没有赋初始值,那它是完成不能使用的。
2.操作数栈:
1. 同局部变量表一样,最大深度也是在编译的时候就写入了。操作数栈的每一个元素都可以是任意Java数据类型,32位数据类型占栈容量为1,64位数据类型占栈容量为2。
2. 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的。在方法的执行过程中,会有各种字节码指令往操作数栈写入和提取内容。
3. 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配。编译器必须严格保证这一点,在类校验的数据流分析中还要再次验证这一点。
4. 2个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。PS:优化:让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些空间,最重要的是在进行方法调用时就可以直接共用一部分数据。
3.动态连接:
1. 每个栈帧都包含一个指向运行时常量池该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。Class文件中的符号引号在类加载阶段或第一次使用的时候就被转化为直接引用,这种转化叫静态解析,另外一部分将在每一次运行期间都转化为直接引用,这部分叫动态连接。
4.方法返回地址:- 当一个方法开始执行后,只有两种方式退出这个方法。第一种是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者。另一种退出方式是遇到了异常,并且这个异常没有得到妥善处理,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。
- 无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行。一般来说,正常退出时,主调方法的PC计数值的值可以作为返回地址,栈帧中很可能会保存这个计数值,而方法异常退出,返回值要根据异常处理器表来确定,就不会保存这部分信息。
- 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的
局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值
以指向方法调用指令后面的一条指令等。
5.附加信息:
《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、
性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现,在讨论概念时,一
般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
- 方法调用:
1.解析:
1. 调用目标在程序代码写好,编译期进行编译那一刻就已经确定下来,这类方法的调用叫解析。
2. 符合“编译期可知,运行期不可变”的方法主要有静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问。各自的特点决定了它们不可能通过继承或别的方式重写出其他版本.
3. 静态方法,私有方法,实例构造器,父类方法,被final修饰的方法这5种方法在类加载的时候就可以把符号引用解析为该方法的直接引用,这些 统称为"非虚方法"。
2.分派:- 静态分派:
1.虚拟机在重载时是通过参数的静态类型而不是实例类型作为判定依据的,编译期在编译阶段就根据参数的静态类型决定会使用哪个重载版本。发生在编译阶段。
2.编译期虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是唯一的,往往只能确定一个相对更合适的版本,原因是字面量天生的模糊性,它不需要定义,就没有显式的静态类型。(例如’a’既可以是字符串,也可以代表int的97等等) - 动态分派:
1.在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。 - 单分派与多分派:
1.既要选择静态类型,又要选择方法参数,根据2个宗量进行选择,所有Java的静态分派属于多分派类型。
2.静态类型,实际类型对方法的选择不会构成影响,只有一个宗量作为选择依据,所以java语言的动态分派选择单分派类型。 - 虚拟机动态分派的实现
1.为类型在方法区建立一个虚方法表,存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表地址入口和父类是一致的,都指向父类的实现入口。虚方发表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把虚方法表也一同初始化完毕。
- 静态分派: