方法调用不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普遍、最频繁的操作,Class文件的编译过程不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法实际运行时内存布局中的入口地址。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。
解析
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来,这类方法的调用称为解析(Resolution)。
在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类,前者符合类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。
只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类。它们在类加载的时候会把符号引用解析为该方法的直接引用,这些方法可以成为非虚方法,与之相反的成为虚方法(出去final方法)。Java中非虚方法除了使用invokestatic、invokespecial调用方法之外还有一种,就是被final修饰的方法。虽然final方法使用invokevirtual指令来调用,但是又有它无法被覆盖,没有其他版本,所以也无须对方法接收者进行多态选择,在Java语言规范中明确说明了final方法是一种非虚方法。
分派
静态分派
所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。静态分分派的典型应用是方法重载,发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
动态分派
invokevirtual指令在运行时解析过程大致分为以下几个步骤:
- 1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记做C
- 2、如果在类型C中找到与常量中描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常
- 3、否则,按照继承关系从下到山依次对C的各个父类进行第2不搜索和验证过程
- 4、如何始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokvirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程是Java语言中方法重写的本地,我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
单分派与多分派
方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少宗量可分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
虚拟机动态分派的实现
由于动态分派是非常频繁的动作,且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此虚拟机基于性能的考虑,大部分实现都不会真正的进行如此频繁的搜索。最常用的“稳定优化”手段就是在类方法区中建立一个虚方法表(Virtual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Interface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。
虚拟机除了使用方法表之外,在条件允许的情况下,还会使用内联缓存(Inline Cache)和基于“类型继承关系分析”(Class Hierarchy Analysis,CHA)技术的守护内联(Guarded Inlining)两种稳定的“激进优化”手段来获得更高的性能。
动态类型语言支持
动态类型语言
动态语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期。常见动态语言:PHP、Python、Ruby、JavaScript等,相对的,在编译期就进行类型检查过程的语言(如C++和Java)就是最常用的静态类型语言。
JDK1.7与动态类型
方法的符号引用在编译时产生,这样Java虚拟机上实现动态类型语言就不得不使用其他方式(如编译期时留个占位符号类型,运行时动态生成字节码实现具体类型到占位符类型的适配)来实现,增加了实现复杂度,也可能带来额外的性能或者内存开销。
java.lang.invoke包
该包提供一种新的动态确定目标方法的机制,称为MethodHandle。
invokedynamic指令
invokedynamic指令与MethodHandle机制的作用一样,都是为解决原有4条“invoke”指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户(包含其他语言的设计者)有更高的自由度。
参考资料
- 深入理解Java虚拟机 JVM高级特性与最佳实践 第2版
网友评论