jvm-方法调用

重载与重写

重载

在java程序里,如果同一个类中出现多个名字相同,并且参数类型相同的方法,那么它无法通过编译。也就是说,在正常情况下,如果我们想要在同一个类中定义名字相同的方法,那么它们的参数类型必须不同。这些方法之间的关系,我们称之为重载

重载的方法在编译过程中即可完成识别。具体到每一个方法调用,java编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段:

  1. 在不考虑对基本类型自动拆装箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法;
  2. 如果在第1个阶段中没有找到适配的方法,那么在允许自动拆装箱,但不允许可变长参数的情况下选取重载方法;
  3. 如果在第2阶段中没有找到适配的方法,那么在允许自动拆装箱以及可变长的情况下选取重载方法。

如果java编译器在同一阶段中找到了多个适配的方法,那么它会在其中选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系。
例如:

  • void invoke(Object,Object... args){...}
  • void invoke(String s,Object obj,Object... args){...}
    第一个参数当传入null时,由于String是Object的子类,因此java编译器会认为第二个方法更为贴切。

重写

如果子类定义了与父类中非私有方法同名的方法,而且这个方法的参数类型相同,并且这两个方法都不是静态的,那么子类的方法重写了父类中的方法。

jvm的静态绑定和动态绑定

java虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor)。方法描述符是由方法的参数类型以及返回类型所构成。如果同一个类中同时出现多个名字相同且描述符也相同的方法,那么java虚拟机会在类的验证阶段报错。

对于java语言中重写而java虚拟机中非重写的情况,编译器会通过类型擦除和方法桥接来实现java中的重写语义。

由于对重载方法的区分在编译阶段已经完成,所以java虚拟机不存在重载概念。

在java虚拟机中,静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况。

java字节码中与调用相关的指令有5种:

  1. invokestatic:用于调用静态方法。
  2. invokespecial:用于调用私有实例方法、构造器,以及使用super关键字调用父类的实例方法或构造器,和所有接口的默认方法。
  3. invokevirtual:用于调用非私有实例方法。
  4. invokeinterface:用于调用接口方法。
  5. invokedynamic:用于调用动态方法。

虚方法调用

对于invokevirtual和invokeinterface而言,在绝大部分年情况下,虚拟机需要在执行过程中,根据调用者的动态类型,来确定具体的目标方法。这两种指令均属于java虚拟机中的虚方法调用,这个过程也称为动态绑定。相对于静态绑定的非虚方法调用来说,虚方法调用更加耗时。
唯一的例外在于,如果虚拟机能够确定目标方法有且仅有一个,比如虚方法调用目标方法被标记为final方法,那么也可以不通过动态绑定,使用静态绑定该虚调用的目标方法。

对于invokestatic和invokespecial而言,java虚拟机能够直接识别具体的目标方法。这个过程属于静态绑定

方法表

java虚拟机中采取了一种空间换取时间的策略来实现动态绑定,在类加载的准备阶段,它除了为静态字段分配内存,还会构造与该类相关联的方法表,用以快速定位目标方法。

invokevirtual使用虚方法表(virtual method table,vtable),invokeinterface使用接口方法表(interface method table,itable),接口方法表稍微复杂,但原理类似。

方法表本质是一个数组每个数组元素指向一个当前类及其祖先类中非私有实例方法。

  1. 子类方法表包含父类方法表的所有方法;
  2. 子类方法表在方法表中的索引值与它所重写的父类方法的索引值相同。

实际上,使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作。访问栈上的调用者,读取调用者的动态类型,读取该类型的方法表,读取方法表中某个索引值所对应的目标方法。相对于创建并初始化java栈帧来说,这几个内存解引用操作的开销简直可以忽略不计。

内联缓存

方法表优化实际仅存在于解释执行中,或者即时编译代码的最坏情况。即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining)。

内联缓存是一种加快动态绑定的优化技术。它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。

  1. 单态(monomorphic)指的是仅有一种状态的情况。
  2. 多态(polymorphic)指的是有限数量种状态的情况。二态(bimorphic)是多态的其中一种。
  3. 超多态(megamorphic)指的是更多种状态的情况。通常用一个具体数值来区分多态和超多态。

当内联缓存没有命中的情况下,java虚拟机需要重新使用方法表进行动态绑定。对于内联缓存中的内容,java虚拟机选择劣化为超多态,处于这种状态下的内联缓存,实际上放弃了优化的机会。直接访问方法表,来动态绑定目标方法。

单态内联缓存和超多态内联缓存的性能差距

Conditions for inlining methods by the HotSpot VM

It doesn’t matter how many sub-class a class has, the only thing which matter is how many methods could be called from a given line of code. e.g. a method could have two implementations across four class, but if only one is called, it will be as if the methods had only one implementation.

Performance of inlined virtual method invocations in Java