五、虚拟机字节码执行引擎
1、概述
- 执行引擎是Java虚拟机最核心的组成部分之一
- 从外观上看起来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。
2、运行是栈帧结构
- 栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素。
- 每一个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的富家信息。
- 当前栈帧所关联的方法称为当前方法
2.1、局部变量表
-
是一组变量值存储空间,用于存放发放参数和方法内部定义的局部变量
-
局部变量表的容量以变量槽为最小单位,每一个槽存放一个32位以内类型的数据,对于64位的数据类型,虚拟机会以告慰在前的方式为其分配两个连续的Solt空间。
-
局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的slot是否是原子操作,都不会引起数据的安全问题。
-
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的Slot数量。
-
虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static的方法),那么局部变量表中第0位索引的slot默认是用于传递方法所属所属对象实例的引用。
-
局部变量表中的solt是可重用的,如果当前字节码PC计数器的值已经超过某个变量的作用域,那么这个变量对应的solt就可以交给其他变量使用
-
赋null值的操作在经过虚拟机JIT编译器优化之后会被消除掉,这时候将变量设置为Null实际上是没有意义的。
-
局部变量不一样,如果一个局部变量定义了但是没有赋初始值是不能使用的。
2,2、操作数栈
- 操作数栈为操作栈,后入先出。
- 跟局部变量表一样,操作栈最大深度也在编译的时候被写入到Code属性max_stacks数据项中
- 32位占容量1,64位占容量为2
- 优化,让两个栈帧出现一部分重叠,这样进行方法调用时可以用一部分数据,而无需进行额外的参数复制传递了。
2.3、动态链接
- 每个栈帧都包含第一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
- Class常量池中含有大量的符号引用,这些符号引用一部分会在类加载或者第一次使用的时候转化为直接引用,这种转化为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分成为动态连接。
2.4、方法返回地址
- 当一个方法被执行,有两种方式退出
- 执行引擎遇到任何一个方法返回的字节码指令,这种退出方法的方式成为正常完成出口
- 方法在执行过程中遇到异常,并且这个异常没有在方法体内得到处理。这种退出方法的方式成为异常完成出口。
- 在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。
- 方法退出过程实际上等同于把当前栈帧出栈,执行的操作有
- 恢复上层方法的局部变量表和操作数栈
- 把返回值(如果有的话)压入调用者栈帧的操作数栈中
- 调整PC计数器的值指向发明会发调用指令后面一条指令。
3、方法调用
- 方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
- 一切方法调用在CLass文件里面存储的都只是符号引用,而不是方法在实际运行是内存布局中的入口地址(相当于直接引用)。这就相当于,需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。
3.1、解析
- 在类加载解析阶段,能将一部分符号引用转化为直接引用能够成立前提是
- 方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。也就是说,调用目标在程序代码写好,编译器进行编译时就必须确定下来,这类方法的调用称为解析。
- 符合编译期可知,运行期不可变的要求主要有静态方法和私有方法
- 静态方法与类型直接关联
- 私有方法在外部不可访问
- 都不能通过继承或别的方式重写出其他版本。适合在类加载阶段进行解析
- Java虚拟机里面提供了四条方法调用字节码指令
incokestatic
:调用静态方法invokespecial
:调用实例构造器<init>
方法,私有方法和父类方法。invokevirtual
:调用所有的虚方法invokeinterface
:调用接口方法。会在运行时再确定一个实现此接口的对象。- 是要能被
incokestatic
和invokespecial
调用的方法,都可以确定唯一版本,符合这个条件的有静态方法、私有方法、实例构造器和父类方法四类。他们在类加载时就会吧符号引用解析为该方法的直接引用 - 其余的方法就称为虚方法(除去final方法),
- final方法是使用
invokevirtual
指令来调用,但它无法被覆盖,没有其他版本,所以结果肯定唯一。 - 解析调用一定是一个静态的过程,在编译期间完全确定,在类装载的解析阶段就会将符号引用转变为可确定的直接引用,不会延迟到运行期间去完成、
3.2、分派
-
Java具备面向对象的三个基本特征:继承、封装和多态
-
分派调用过程将会揭示多态性特征的一些最基本的体现,如重载和重写。
-
静态分派
- 我们吧Human称为变量的静态类型或者外观类型,后面的Man称为变量的实际类型
- 两个类型都可以发生变化,区别
- 静态类型仅在使用中变化,本身的静态类型不会被改变,并且最终的静态类型是在编译器可知的
- 实际类型变化的结果在运行期才可以确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BGYeFC1z-1616378723781)(C:\Users\LZH\Pictures\Camera Roll\Snipaste_2021-03-21_21-34-26.png)]
- 在上述Main方法里面,在确定接受者的前提下,使用哪个重载版本完全取决于传入参数的数量和数据类型。
- 虚拟机在重载时是通过参数的静态类型而不是实际类型作为判断依据的。而且静态类型是编译期可知的,所以在编译阶段,静态类型决定哪个重载版本。
- 所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。静态分派最典型应用就是方法重载。
- 编译期虽然能确定出方法的重载版本,但是往往只能确定一个更加合适的版本。如基本数据类型的重载,传入之后会找到最合适的版本
- 静态方法会在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程是通过静态分派完成的。
-
动态分派
-
它和多态性的另外一个重要体现—重写有着密切的关联。
-
所以动态分派实际上是在运行期根据实际类型确定方法执行版本的分派。
-
-
单分派和多分派。
- 方法的接收者与方法的参数统称为方法的宗量
- 单分派是根据一个宗量对目标方法进行选择,多分派则是根据多余一个的宗量对目标进行选择。
- Java语言的静态分派属于多分派类型,Java语言的动态分派属于单分派类型
-
虚拟机动态分派的实现
-
使用虚方法表索引来代替元数据查找以提高性能。
-
虚方法表中存放着各个方法的实际入口地址。
-
如果某个方法在子类中没有被重写,那么子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的
-
如果子类中重写了这个方法,子类方法表中的地址将会被替换为指向子类实现版本的入口地址。
-
方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会吧该类的方法表也初始化完毕。
-
4、基于栈的字节码解析执行引擎
- 许多Java虚拟机的执行引擎在执行Java代码的时候都有解析执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择。
4.1、解析执行
- 因为这一部分动作实在Java虚拟机之外进行的,而解析器在虚拟机的内部,所以Java程序的编译就是半独立实现的。
4.2、基于栈的指令集与基于寄存器的指令集
-
Java编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流里面的指令大部分都是零地址指令,他们一来操作数栈进行工作。
-
与之相对的另外一套常用的指令集架构是基于寄存器的指令集。
-
基于栈的指令集
- 优点
- 可移植性
- 可让访问最频繁的数据放到寄存器中,来获取尽量好的性能,实现起来更加简单
- 代码相对更加紧凑
- 缺点
- 执行速度较慢
- 指令数量较多
- 优点
- 基于寄存器的指令集