【Java】JVM(六)虚拟机字节码执行引擎
一、概述
执行引擎是虚拟机中最核心的部分之一, 虚拟机自己实现引擎,自己定义指令集和执行引擎的结构体系。
二、栈帧
栈帧包含(1)局部变量表、(2)操作数栈、(3)动态链接、(4)方法返回地址、(5)额外的附加信息
栈顶的栈帧为当前栈帧,是唯一有效的。
2.1 局部变量表
用于存放方法参数和方法内部定义的局部变量。单位为槽(Slot),每个槽可以存放一个变量(Boolean,byte,char,short,int,float,reference,returnAddress),lonng,double需要两个槽。
2.2 操作数栈
Java虚拟机引擎称为“基于栈的执行引擎”,栈就是操作数栈。
2.3 动态链接
每个栈帧都有一个指向运行时常量池的所属方法引用,目的是:支持动态链接。
静态链接:Class文件中有大量的符号引用,字节码中方法调用指令,以常量池中指向方法的符号引用为参数在类加载阶段或者第一次使用时候会转化为直接引用。
动态链接,在运行期间化为直接引用。
2.4 方法返回地址
两种方法退出:(1)遇到返回关键字。(2)遇到异常
实际操作:当前栈帧出栈,恢复上层方法的局部变量表和操作数栈,返回值压栈(操作数栈)
2.5 附加信息
允许具体的虚拟机实现增加一些描述信息。
三、方法调用
方法调用:确定要调用的版本(即:调用哪个方法)
3.1 解析
所有方法在Class文件中都是一个字符串的常量引用,在类加载期间会将其转换为直接引用。
解析:方法在真正运行之前必须有一个确定版本,并在运行期是不可变的。
3.2 分派
3.2.1静态分派
依赖静态类型来进行方法执行版本的分派动作。
发生时期:编译阶段
调用指令:invokestatic
应用:方法重载
public class StaticDispatch { static abstract class Human { } static class Man extends Human { } static class Woman extends Human { } public void sayhello(Human guy) { System.out.println("Human guy"); } public void sayhello(Man guy) { System.out.println("Man guy"); } public void sayhello(Woman guy) { System.out.println("Woman guy"); } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); StaticDispatch staticDispatch = new StaticDispatch(); staticDispatch.sayhello(man);// Human guy staticDispatch.sayhello(woman);// Human guy } }
输出结果:
Human guy
Human guy
原因:
下一行代码:
Human man = new Man();
其中Human为静态类型, Man为 实际类型
两者的区别是:静态类型编译期间可知, 实际类型运行期可知。
重载是通过编译期间的静态类型确定调用哪个函数, 所以Javac会根据静态类型 Human来确定调用的函数为 sayhello(Human guy)
3.2.2 动态分配
运行期根据实际类型确定方法执行版本的过程为动态分配。
调用指令:invokevirtual
应用:方法重写
public class DynamicDisptch { static abstract class Human { abstract void sayhello(); } static class Man extends Human { @Override void sayhello() { System.out.println("man"); } } static class Woman extends Human { @Override void sayhello() { System.out.println("woman"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); man.sayhello(); woman.sayhello(); man = new Woman(); man.sayhello(); } }
运行结果:
man say hello woman say hello woman say hello
动态分配运行时解析过程:
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作 C。
- 如果在类型 C 中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常。
- 否则,按照继承关系从下往上一次对 C 的各个父类进行第 2 步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常。
invokevirtual 指令在运行期确定接受者的实际类型,所以会将同一个方法解析到不同的函数上。这就是Java重写的本质。
3.3 单分派与多分派
方法接收者与方法参数统称为方法的宗量,根据分派基于多少种宗量,可以分为单分派和多分派两种,
单分派:根据一个宗量对目标的方法选中
多分派:根据多个宗量进行目标方法选中
/** * 单分派、多分派演示 * */ public class Dispatch { static class QQ {} static class _360 {} public static class Father { public void hardChoice(QQ arg) { System.out.println("father choose qq"); } public void harChoice(_360 arg) { System.out.println("father choose 360"); } } public static class Son extends Father { public void hardChoice(QQ arg) { System.out.println("son choose qq"); } public void harChoice(_360 arg) { System.out.println("son choose 360"); } } public static void main(String[] args) { Father father = new Father(); Father son = new Son(); father.harChoice(new _360()); son.hardChoice(new QQ()); } }
运行结果:
father choose 360 son choose qq
静态分配:在编译阶段,也就是静态分配过程中,选择目标的依据有两点:(1)静态类型是 Father 还是 Son(2) 方法的参数是 QQ还是 360。 所以静态分配为多分派。
动态分配:在运行阶段,也就是动态分配过程中,由于静态分配过程中已经知道函数必须是 hardChoice(QQ arg) ,其中参数的类型编译器不会关心, 只会在意方法的接受者到底是Father 还是 Son 所以动态分配为 单分派。
3.4 虚拟机动态分派的实现
动态分配需要查找合适的方法,为了提高性能, 一般的处理方法是:用稳定的手段,为类在方法区建立一个虚的方法表。使用索引表来代替查找。
虚方法表中存储了各个方法的实际入口,如果在子类中没有重写该方法,那么子类的虚方法表中的地址和父类的一样, 如果重写了,则指向重写之后的方法入口地址。
初始化时间:虚拟方法表一般在类加载链接期间进行初始化,准备好初值之后虚拟机会将类方法初始化完毕。