一. 运行时栈帧结构
1. 栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的元素。栈帧存储了方法的局部变量,操作数栈,动态链接,方法返回地址等信息。
2. 对于执行引擎来讲,活动的线程,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。
局部变量表
1. 局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在java程序被编译为Class文件时,在方法的Code属性的max_locals数据项中确定该方法所需要分配的最大局部变量表的容量。
2. 局部变量表的容量以变量槽:Slot为最小单位,这些Slot是可重用的。
3. 旁门索道
Public static void main(String[] args) { { Byte[] b = new Byte[64*1024*1024]; } System.gc(); } //这时并不能立马进行GC |
Public static void main(String[] args) { { Byte[] b = new Byte[64*1024*1024]; } Int a = 0; System.gc(); } //这时立马进行GC |
原因:局部变量表中的slot是可重用的,方法体中定义的变量,其作用域不一定会覆盖整个方法体,如果当前字节码的PC计数器的值已经超出了整个变量的作用域,那么这个变量对应的slot既可以交给其它的变量使用。
对照上面的代码int a = 0;变量a的定义已经超过了b的作用范围,所以a会重新使用b的slot(这个slot指的是局部变量表的基本单位),这时执行垃圾回收就会回收b所占用的空间。
操作数栈
1. 操作数栈是一个后入先出的栈。操作数栈的最大深度也是在编译时写入到Code的max_stacks数据项中。
2. 操作数栈的数据类型必须与字节码指令的序列严格匹配,否则会发生错误。
动态链接
1. 指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
2. 静态解析:部分引用会在类加载阶段或第一次使用时转化为直接引用。
3. 动态解析:部分引用将在每一次的运行期间转化为直接引用。
方法返回地址
1. 执行引擎退出方法的途径:遇到任意一个方法返回的字节码指令--正常退出;方法执行过程中遇到了异常--异常退出。
2. 不论采用任何方式退出,在方法退出之后都需要返回到方法调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复他的上层方法的执行状态。
二. 方法调用
1. 方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,暂时不涉及方法内部的具体运行过程。
解析
1. 所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。
2. 符合"编译器可知,运行期不变"的方法:静态方法,私有方法。
3. 方法调用指令:
invokestatic | 调用静态方法 |
invokespecial | 调用实例够在其<init>方法、私有方法、父类方法 |
invokevirtual | 调用所有的虚方法 |
invokeinterface | 调用接口方法,会在运行时再确定一个实现此接口的对象 |
4. 使用的方法:静态方法,私有方法,实例构造器,父类方法。
5. 解析调用是一个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用转变为可确定的直接引用,不会延迟到运行期间去完成。
分派
1. 静态分派:所有依赖静态类型来定位方法执行版本的分派动作都是静态分配。静态分派发生在编译阶段,确定静态分配的动作实际上不是由虚拟机来执行的。有时候重载的版本并不是唯一的,但编译器只能确定一个更合适的版本。
2. 动态分派:在运行时期根据实际类型确定方法执行版本的分派过程称为动态分配。
3. 单分派和多分派根据分派基于多种宗量可以将分派划分为多分派和单分派(宗量:方法的接收者与方法的参数统称为方法的宗量)。
4. 静态分派属于多分派类型;动态分派属于单分派类型。