虚拟机字节码执行引擎【方法调用(一)解析】
方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。在程序运行时,进行方法调用时最普遍、最频繁的操作之一。
Class文件的编译过程中不包含传统语言编译的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(即直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使Java方法调用过程变的相对复杂,某些调用需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。
解析
在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期间是不可改变的。即调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。此类方法的调用被称为解析(Resolution)。
在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要有静态方法(与类型直接关联)和私有方法(在外部不可被访问)两大类,它们各自的特点决定了它们都不可能通过继承或别的方式重写出其他版本,因此它们都适合在类加载阶段进行解析。
调用不同类型的方法,字节码指令集里设计了不同的指令。在Java虚拟机支持以下5条方法调用字节码指令,分别是:
- invokestatic,用于调用静态方法。
- invokespecial,用于调用实例构造器<init>()方法、私有方法和父类中的方法。
- invokevirtual,用于调用所有的虚方法。
- invokeinterface,用于调用接口方法,会在运行时再确定一个实现该接口的对象。
- invokedynamic,先在运行时动态解析处调用点限定符所引用的方法,然后再执行该方法,前面4条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。
只要能被invokestatic和invokespecial指令调用的方法,都可以解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为“非虚方法”(Non-Virtual Method),与之相反,其它方法被称为“虚方法”(Virtual Method)。
Java中的非虚方法除了使用invokestatic、invokespecial调用的方法之外还有一种,就是被final修饰的实例方法。虽然由于历史设计的原因,final方法是使用invokevirtual指令来调用的,但是因为它无法被覆盖,没有其他版本的可能,所以也无须对方法接收者进行多态选择,又或者说多态选择的结果肯定是唯一的。在《Java语言规范》中明确定义了被final修饰的方法是一种非虚方法。
解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期再去完成 。而另一种主要的方法调用形式:分派(Dispatch)调用则要复杂许多,它可能是静态的也可能是动态的,按照分派依据的宗量数可分为单分派和多分派。这两类分派方式两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4中分派组合情况。