方法调用

方法调用并不等同于方法执行,方法调用阶段唯一任务是确定被调用方法的版本,暂时不涉及方法内部具体运行过程。程序运行时,进行方法调用是最普遍最频繁的操作。

Class文件编译过程中不包含传统编译中的连接步骤,一切方法的调用在Class文件中存储的都只是符号引用,而非方法在实际运行内存布局中的入口地址(直接引用)。该特性带来了强大的动态扩展性,但同时使得方法调用过程变得相对复杂,需要在类加载期间、甚至运行期间才能确定目标方法的直接引用

解析调用

所有方法调用中的目标方法Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中在编译期可知,运行期不可变的方法的符号引用转化为直接引用

编译器可知,运行期不可变的方法主要包括:静态方法私有方法实例构造器父类方法final修饰的方法。这类方法被称为非虚方法,其他方法称为虚方法

虚拟机提供5方法调用字节码指令,前四条指令的分派逻辑是固化在Java虚拟机内部的,而invokedynamic的分派逻辑是由用户所设定的引导方法决定的:

  • invokestatic:调用静态方法
  • invokespecial:调用实例构造器<init>方法、私有方法、父类方法
  • invokevirtual:调用所有的虚方法,以及final修饰的方法
  • invokeinterface:调用接口方法,在运行时再确定一个实现此接口的对象
  • invokedynamic:在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法

只要能被invokestaticinvokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,包括了静态方法私有方法实例构造器父类方法4类。

解析调用一定是一个静态过程,编译器就可以完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期去完成。

分派调用

分派调用可能是静态的也可能是动态的,根据宗量方法的接收者方法的参数统称为方法的宗量)分为单分派多分派,两类分派方式两两组合就构成了:静态单分派、静态多分派、动态单分派、动态多分派。

分派的调用过程其实就是Java多态的实现原理,如重写重载在Java虚拟机中是如何实现的。

静态分派

说到静态分派,首先先说一下变量的静态类型或者叫外观类型,以及变量的实际类型。假设有抽象类Human和其实现类Man、Woman,若Human man = new Man();,则Human是man的静态类型Man是其实际类型

静态类型和实际类型在程序中都可能发生一些变化,静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,且最终的静态类型是在编译器可知的;实际类型的变化结果在运行期才可以确定,编译器在编译程序时并不知道对象的实际类型是什么。

1
2
3
4
5
6
// 实际类型的变化
Human man = new Man();
man = Woman();
// 静态类型的变化
sr.sayHello((Man) man);
sr.sayHello((Woman) man);

虚拟机(编译器)在调用重载Overload)时是通过参数的静态类型而不是实际类型作为判定依据,在编译阶段Javac编译器会根据参数的静态类型来确定具体调用哪个重载版本的方法。

所有依赖 静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型应用是方法重载

静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,编译器虽然能确定方法的重载版本,但很多情况下这个重载版本并不唯一,往往只能确定一个更加合适的版本,主要原因是字面量不需要定义,所以字面量是没有显式的静态类型,其静态类型只能通过语言上的规则去理解和推断。

动态分派

动态分派多态的另一个重要体现重写Override)有着密切的关联。

动态分派其实就是invokevirtual指令的多态查找的过程。由于该指令第一步就是在运行其确定接收者的实际类型,所以对于不同的调用,该指令会将常量池中的类方法符号引用解析到不同的直接引用上,该过程就是Java中方法重写的本质。

我们将这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派

解析分派这两者之间的关系并不是二选一的排他关系,它们是在不同层次上去筛选确定目标方法的过程。

单分派与多分派

单分派是根据一个宗量对目标方法进行选择,多分派是根据多个宗量对目标方法进行选择。到目前为止可以说Java语言是一门静态多分派、动态单分派的语言。

虚拟机动态分派的实现

由于动态分派是非常频繁的动作,而且动态分派的方法版本的选择过程需要运行时在类的方法元数据中搜索合适的目标方法,在虚拟机的实际实现中基于性能考虑会对其进行一些优化,最常用的稳定优化手段是为类在方法区中建立一个虚方法表或接口方法表使用虚方法表索引来代替元数据查找提高性能

虚方法表中存放着各个方法的实际入口地址。具有相同签名的方法,在之类和父类的虚方法表中都应当具有相同的索引序号,在类型变换时便于查找。

虚拟机除了使用方法表之外,在条件允许下,还会使用内联缓存和基于类型继承关系分析技术的守护内联两种非稳定的优化手段来获取更高的性能。

动态类型语言支持

动态类型语言的关键特征是它的类型检查主体过程是在运行期而不是编译期变量无类型而变量值才有类型这也是动态语言的一个重要特征。

静态类型语言在编译期确定类型,可以提供严谨的类型检查,与类型相关的问题在编码时就能及时发现,利于稳定性及代码达到更大规模

动态类型语言在运行期确定类型,为开发人员提供更大的灵活性,代码会更加清晰简单,也意味着高效的开发效率

Reflection与MethodHandler

JDK7提供了java.lang.invoke包,其主要目的是用于提供一种新的动态确定目标方法的机制,称为MethodHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static class ClassA {
public void println(String string) {
System.out.println(string);
}
}

public static void main(String[] args) throws Throwable {
Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();

MethodType methodType = MethodType.methodType(void.class, String.class);
MethodHandle methodHandle = MethodHandles.lookup().findVirtual(obj.getClass(), "println", methodType).bindTo(obj);

methodHandle.invokeExact("param");
}

MethodHandler的使用方法与Reflection有众多相似之处,以及以下区别:

  • 本质上MethodHandlerReflection都是在模拟方法调用Reflection是在模拟Java层次的方法调用,MethodHandler是在模拟字节码层次的方法调用。
  • Reflection中的Method对象远比MethodHandler中的MethodHandler对象所包含的信息多。Method包含了方法签名、描述符、方法属性表中各种属性、以及执行权限等运行期信息。而MethodHandler仅包含与执行该方法相关的信息。
  • MethodHandler理论上可以采用类似虚拟机在字节码上做的各种优化思路。而Reflection不行。
  • Reflection仅支持Java语言,MethodHandler支持所有语言。