执行引擎是Java虚拟机最核心的组成部分之一,,所有的Java虚拟机的执行引擎都是一致的:
输入:字节码文件
处理:字节码解析
输出:执行结果
在介绍虚拟机栈时就提到,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧中需要多大的局部变量表和多深的操作数栈在编译代码的过程中已经完全确定,并写入到方法表的Code属性中。在活动的线程中,位于当前栈顶的栈帧才是有效的,执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
一切方法调用在Class文件里面都是一个常量池中的符号引用。在类加载解析阶段,可能会将其中一部分符号引用转化为直接引用,也有可能会在初始化阶段之后再开始,取决于方法在运行之前是否有有确定的调用版本,且在运行期间不变。
在Java中符合 编译器可知,运行期不可变 的方法,主要包括静态方法和私有方法两大类,它们都不可能通过某些方式重写其他版本,因此它们都适合在类加载阶段进行解析
JVM提供了5条方法调用字节码:
invokestatic:调用静态方法
invokespecial:调用实例构造器方法、私有方法和父类方法
invokevirtual:调用所有的虚方法
invokedynamic:运行时动态解析出调用点限定符所引用的方法,再执行该方法
只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,它们在类加载时会把符号引用解析为该方法的直接引用。
示例:
public class Person { public static void sayHello() { System.out.println("hello world"); } public static void main(String[] args) { sayHello(); } }
javap -verbose查看
public class Person { public static void main(String[] args) { new Person(); } }
javap -verbose查看
通过invokestatic和invokespecial指令调用的方法称为非虚方法,与之相反,其他方法称为虚方法(除去final方法),虚方法的调用是一个分派的过程,有静态也有动态,可分为静态单分派、静态多分派、动态单分派和动态多分派。
所有依赖静态类型来定位方法执行版本的分派,静态分派的典型应用是方法重载,静态分派发生在编译阶段,因此静态分派的动作不由虚拟机执行。eg:
public class Person { public void sayHello(Object obj){ System.out.println("hello Object"); } public void sayHello(String str){ System.out.println("hello String"); } public static void main(String[] args) { Person p = new Person(); Object obj = new String(); p.sayHello(obj); } }
结果:
hello Object
相对于变量obj,Object是其静态变量,String是其实际变量,在编译阶段,Java编译器会根据参数的静态类型决定调用哪个重载版本。用javap -verbose再看下
动态分派根据实际类型确定方法执行版本
public class Person { public static void main(String[] args) { Object p = new Person(); Object obj = new String("比利时"); System.out.println(obj.toString()); System.out.println(p.toString()); } }
输出
比利时 jvm.Person@63ce0e18
同一个静态类型,调用toString方法,结果完全不同,原因就是因为这两个变量的实际类型不同。通过java -verbose 查看字节码指令
21、31行指令将之前存放到局部变量表1、2位置的对象引用(接受者)压入操作数栈的栈顶,22、32行是方法调用指令,虽然指令一样都是Object.toString,但是这两个指令最终执行的目标方法不相同
①.找到操作数栈的栈顶元素所指向的对象的实际类型,记为C
②.如果C中存在描述符和简单名称都相符的方法,则进行访问权限验证,如果验证通过,则直接返回这个方法的直接引用,否则返回java.lang.IllegalAccessError异常
③.如果C中不存在对应的方法,则按照继承关系对C的各个父类进行第2步的操作
④.如果各个父类也没对应的方法,则抛出异常
所以上述两次invokevirtual指令将相同的符号引用解析成了不同对象的直接引用,这个过程就是Java语言中重写的本质
如何实现?
为类在方法区中建立虚方法表,虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口一致,都指向父类的实现入口。
以上面代码为例,Person类没有重写toString()方法,所以toString()方法指向Object类型数据,而String重写了toString()方法,所以没有(未列Object其他方法)。