Java基础之Java分派机制

  • 内容
  • 评论
  • 相关

Java分派机制

在Java中,符合“编译时可知,运行时不可变”这个要求的方法主要是静态方法和私有方法。这 两种方法都不能通过继承或别的方法重写,因此它们适合在类加载时进行解析。
Java虚拟机中有四种方法调用指令:
invokestatic:调用静态方法。
invokespecial:调用实例构造器方法,私有方法和super。
invokeinterface:调用接口方法。
invokevirtual:调用以上指令不能调用的方法(虚方法)。
只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段确定唯一的调 用版本,符合这个条件的有:静态方法、私有方法、实例构造器、父类方法,他们在类加载 的时候就会把符号引用解析为改方法的直接引用。这些方法被称为非虚方法,反之其他方法 称为虚方法(final方法除外)。
        虽然final方法是使用 invokevirtual 指令来调用的,但是由于它无法被覆盖,多态的选择 是唯一的,所以是一种非虚方法。

静态分派

对于类字段的访问也是采用静态分派
People man=new Man()
静态分派主要针对重载,方法调用时如何选择。在上面的代码中, People 被称为变量的引用 类型, Man 被称为变量的实际类型。静态类型是在编译时可知的,而动态类型是在运行时可 知的,编译器不能知道一个变量的实际类型是什么。
编译器在重载时候通过参数的静态类型而不是实际类型作为判断依据。并且静态类型在编译 时是可知的,所以编译器根据重载的参数的静态类型进行方法选择。
         在某些情况下有多个重载,那编译器如何选择呢? 编译器会选择”最合适”的函数版本, 那么怎么判断”最合适“呢?越接近传入参数的类型,越容易被调用。

动态分派

动态分派主要针对重写,使用 invokevirtual 指令调用。 invokevirtual 指令多态查找过程:
。找到操作数栈顶的第一个元素所指向的对象的实际类型,记为C。
。如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验, 如果通过则返回这个方法的直接引用,查找过程结束;如果权限校验不通过,返回 java.lang.IllegalAccessError异常。
。否则,按照继承关系从下往上一次对C的各个父类进行第2步的搜索和验证过程。
。如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError异常。

虚拟机动态分派的实现

由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正地进行如此频繁的搜索。面对这种情况,最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表(Vritual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Inteface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。Son重写了来自Father的全部方法,因此Son的方法表没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法,所以它们的方法表中所有从Object继承来的方法都指向了Object的数据类型。

为了程序实现上的方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址。方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。

ps:方法表是分派调用的“稳定优化”手段,虚拟机除了使用方法表之外,在条件允许的情况下,还会使用内联缓存(Inline Cache)和基于“类型继承关系分析”(Class Hierarchy Analysis,CHA)技术的守护内联(Guarded Inlining)两种非稳定的“激进优化”手段来获得更高的性能,关于这两种优化技术的原理和运作过程,可以参考JIT晚期运行期。