虚拟机字节码执行引擎
- 执行引擎执行java代码时有解释执行(通过解释器执行)和编译执行(通过及时编译器产生本地代码执行)和两种选择,也可能两者兼备。
- 运行时栈帧结构
- 它是虚拟机运行时数据区中的虚拟机栈的栈元素
- 栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息
- 每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程
- 栈的深度和需要多大的局部变量表等,在代码编译阶段就被写到class文件的Code属性里了,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响
- 对于执行状态的方法,在活动线程中,只有位于栈顶的栈帧才是最有效的,执行引擎运行的所有字节码指令都只针对当前栈帧进行操作
- 局部变量表
- 变量值存储空间,用于存放方法参数和方法内定义局部变量
- 变量表的容量以变量槽(variable slot)为最小单位,大部分基础数据类型,都可以使用32为或更小物理内存存放,与处理器,操作系统或虚拟机不同而变化
- 64位数据,虚拟机会以高位对齐为其分配两个连续的slot空间
- java语言明确64位的数据类型只有long和double,操作为线程私有操作,无论是否原子都不会引起数据安全问题
- 当前字节码PC计数器的值如果已经超出了某个变量的作用域,这个对应变量的slot就可以被其他变量使用
- 操作数栈
- 后入先出栈
- 一般用于计算,或调用其他方法时通过操作数栈进行参数传递
- java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的栈就是操作数栈
- 在大多数虚拟机实现中会做优化,让下面栈帧的部分操作数栈和上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以公用部分数据,不用进行额外的复制传递
- 动态连接
- 每个栈帧都有一个指向运行时常量池中该栈帧所属方法的引用,用于支持方法调用过程中的动态连接。(符号引用在运行过程中转为直接引用)
- 方法返回地址
- 分为两种返回:正常完成出口(Normal Method Invocation Completion),异常完成出口(Abrupt Method Invocation Completion)
- 异常完成出口:因异常无法被正确的异常处理器捕获而导致的方法调用退出,一个方法使用异常完成出口的方式退出,是不会给它的长层调用者产生任何返回值的
- 方法退出的过程:恢复上层方法的局部变量表及操作数栈,如果有返回值的话将返回值压入操作数栈进行操作,调整PC计数器指向方法调用指令的下一条指令
- 附加信息
- 虚拟机规范允许一些虚拟机添加一些规范中没有的描述信息到栈帧中,如调试信息
- 实际开发中,把动态连接、方法返回地址、附加信息归为一类,叫栈帧信息
- 方法调用
- 方法调用不同于方法执行,只是确定调用的方法版本。
- 由于Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用再class文件中存储的都是符号引用,而非实际运行时的内存布局中的入口地址(相当于直接引用),所以使得方法调用再类加载期间甚至到运行期间才能确定目标方法直接引用
- 解析调用
-
方法在程序运行之前就有一个可以确定的调用版本,在编译期间就知道要调用哪个版本的方法,这类方法调用成为解析
-
java虚拟机提供了5条方法调用字节码指令:
① invokestatic:调用静态方法
② invokespecial:调用实例构造器方法、私有方法和父类方法
③ invokevirtual:调用所有的虚方法
④ invokeinterface:调用接口方法,会在运行时再确定实现类的对象
⑤ invokedynamic:运行时动态解析出调用点限定符所引用的方法,然后在执行该方法。 -
前四条指令分派逻辑固定在虚拟机中,最后一条由用户设定引导方法
-
只要被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,在类加载阶段就会把符号引用解析为该方法的直接引用。
-
final修饰的方法虽然用invokevirtual调用,但属于非虚方法。
- 分派(Dispatch)调用
- 解析调用是静态的,分派调用可能是静态可能是动态
- ① 静态分派:
- Human man = new Man(); Human为静态类型,Man为实际类型,方法的话也是;编译器在重载时是通过参数的静态类型,而非实际类型作为依据;调用重载方法用invokevirtual;所有依赖静态类型确定方法版本的分派动作成为静态分派,静态分派的典型应用是方法重载;
- ② 动态分派:
- 重写方法操作;invokevirtual指令的多态查找过程用于指定调用的方法版本;在分配对象时Human man=new Man()对象会被压入栈,指令会去栈顶来匹配方法,如果匹配上就直接返回方法的直接引用,若未匹配则会去父类查找,如果还没找到,会抛出异常;invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这就是方法重写的本质。我们把这种运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
- ③ 单分派与多分派
- 根据宗量数量决定是否为单分派or多分派
- 一个语句只有一个宗量作为选择依据,所以java语言的动态分派属于单分派类型
- java属于静态多分派,动态单分派
- 虚拟机动态分派的实现:基于性能考虑,在类的方法区建立一个虚方法表(vtable),在invokeinstance时也会用到接口方法表(itable),如果子类没有重写父类方法,所有方法都直接指到父类的实现入口,如果子类重写了,在子类的方法表中地址会替为子类实现版本的入口地址。
- 除了方法表(稳定优化),还有内联缓存(Inline Cache) 和 基于继承关系分析技术的守护内联(Guarded Inlining)(激进优化)
- 动态类型语言支持
- invokedynamic
- java.lang.invoke ****MethodHandle:比reflect轻量级,模拟的是字节码层次的方法调用,相应的字节码操作也可以模拟(但支持不全)
- 基于栈的字节码解释执行引擎
- 解释执行(通过解释器执行) & 编译执行(通过即时编译器产生本地代码执行)