方法调用阶段唯一的任务就是确定被调用方法的版本(即调用的是哪一个方法)

概述

Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的只是符号引用,而不是方法在时机运行时内存布局中的入口地址(直接引用),这就是Java语言动态扩展能力的基础,代价是Java方法调用过程变得相对复杂,需要在类加载期间,甚至运行期间才能确定目标方法的直接引用。

解析Resolution

调用目标在程序代码写好、编译器进行编译时就必须确定下来,静态过程

静态方法

  • 与类型直接关联
  • 不能通过继承或其他方式重写,在类加载阶段进行解析

私有方法

  • 外部不可访问
  • 不能通过继承或其他方式重写,在类加载阶段进行解析

字节码指令

分派逻辑固化在JVM内部

  • invokestatic:调用静态方法
  • invokespecial:调用实例构造器<init>方法、私有方法和父类方法
  • invokevirtual:调用所有的虚方法
  • invokeinterface:调用接口方法,会在运行时确定一个实现此接口的对象

分派逻辑由用户所设定的引导方法决定

  • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法然后再执行改方法

非虚方法:

  • invokestaticinvokespecial指令调用的方法、final修饰的方法
  • 可以在解析阶段确定唯一的调用版本
  • 在类加载阶段把方法的符号引用解析为直接引用

final修饰的方法虽然用invokevirtual指令调用,但是由于它无法被覆盖,保证了方法的唯一性

虚方法:

除去非虚方法之外的

分派Dispatch

  • 可静态可动态
  • 根据宗量数分为:单分派、多分派
  • 静态单分派、静态多分派、动态单分派、动态多分派
  • Java语言多态性特征的基本体现

静态分派

Method Overload Resolution,依赖静态类型来定位方法执行版本的分派动作,典型应用是方法重载,发生在编译阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class StaticDispatch {
static abstract class Human {
}

static class Man extends Human {
}

static class Woman extends Human {
}

public void sayHello(Human h) {
System.out.println("hello human");
}

public void sayHello(Man h) {
System.out.println("hello Man");
}

public void sayHello(Woman h) {
System.out.println("hello Woman");
}

public static void main(String[] args) {
//Human为变量的静态类型Static Type或外观类型Apparent Type
//Man为变量的实际类型Actual Type
Human man = new Man();
Human woman = new Woman();

StaticDispatch s = new StaticDispatch();
//hello human
s.sayHello(man);
//hello human
s.sayHello(woman);
}
}

变量的静态类型和实际类型在程序中都可以发生变化,但静态类型的变化仅仅在使用时发生,不会改变,编译器可知;实际类型变化虚在运行期确定。编译器通过参数的静态类型确定。

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

动态分派

在运行期根据实际类型确定方法执行版本的分派过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}

static class Man extends Human {

@Override
protected void sayHello() {
System.out.println("man say hello");
}
}

static class Woman extends Human {

@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}

public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
//man say hello
man.sayHello();
//woman say hello
woman.sayHello();
man = new Woman();
//woman say hello
man.sayHello();
}
}

sayHello()方法使用invokeVirtual指令来完成方法调用,invokeVirtual指令的运行时解析过程为

  • 找到操作数栈顶的第一个元素所指向的对象的实际类型,计作C
  • 在类型C中找到与常量中的描述符和简单名称都相符的方法
    • 找到
      • 进行访问权限校验,通过则返回这个方法的直接引用,查找过程结束
    • 没有找到
      • 返回java.lang.IllegalAccessError异常
  • 按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程
  • 都没有找到,抛出java.lang.AbstractMethodError异常
  • Java语言中方法重写的本质:invokeVirtual指令
    • 在运行期确定接受者的实际类型,把常量池中的类方法符号引用解析道了不同的直接引用上

单分派与多分派

宗量:方法的接受者、参数

单分派:根据一个宗量对目标方法进行选择

多分派:根据多于一个对目标方法进行选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Dispatch {
static class QQ {}

static class _360 {}

public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}

public void hardChoice(_360 arg) {
System.out.println("father choose _360");
}
}

public static class Son extends Father {
public void hardChoice(QQ arg) {
System.out.println("son choose qq");
}

public void hardChoice(_360 arg) {
System.out.println("son choose _360");
}
}

public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
//father choose _360
father.hardChoice(new _360());
//son choose qq
son.hardChoice(new QQ());
}
}
  • 编译阶段进行静态分派时,依据静态类型及参数,会生成两条invokeVirtual指令,分别指向Father.hardChoice(QQ)Father.hardChoice(_360)方法的符号引用。因为是根据两个宗量进行选择,所以静态分派属于多分派类型
  • 运行期进行动态分派时,在执行son.hardChoice(new QQ());语句时,由于编译期已经决定方法签名必须为hardChoice(QQ),唯一可以影响虚拟机选择的因素只有方法接受者的实际类型,因为是根据一个宗量进行选择,所以动态分派属于单分派类型

JVM动态分派实现

  • 动态分派的版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,JVM基于性能考虑,在类的方法区中建立一个虚方法表(Virtual Method Table,vTable),使用虚方法表索引来代替元数据查找。
  • 虚方法表中存放着各个方法的时机入口地址
    • 方法在子类中没有被重写
      • 子类的虚方法表中的地址入口和父类的入口一致,都指向父类的实现入口
    • 方法在子类中被重写
      • 子类方法表中的地址将会替换为指向子类实现版本的入口地址

JVM优化手段

  • 稳定优化
    • 方法表
  • 激进优化
    • 内联缓存(Inline Cache)
    • 守护内联(Guarded Inlining):基于“类型继承关系分析(Class Hirarchy Analysis,CHA)”技术

动态类型语言支持

JDK7新增invokeDynamic指令,是实现”动态类型语言Dynamically Typed Language”支持而做的改进之一,也是为JDK8可以实现Lambda表达式做的技术准本。

动态类型语言

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

静态类型语言在编译期确定类型,利于稳定性及代码达到更大规模;

动态类型语言在运行期确定类行,实现会更加清晰简洁,提升开发效率。

java.lang.invoke

JDK 1.7实现JSR-292,新增java.lang.invoke包,目的是提供一种新的动态确定目标方法的机制,称为MethodHandle。这样一来,Java语言也可以拥有类似于函数指针或委托的方法别名的工具了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;

public class MethodHandleTest {
static class ClassA {
public void println(String a) {
System.out.println(a);
}
}

public static void main(String[] args) throws Throwable {
/**
* MethodHandleTest$ClassA或java.io.PrintStream
*/
Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
getPrintlnMH(obj).invokeExact("super2bai");
}

private static MethodHandle getPrintlnMH(Object receiver) throws Throwable {
// 方法返回值,具体参数
MethodType mt = MethodType.methodType(void.class, String.class);
/**
* lookup(): MethodHandles.lookup(), 在指定类中查找符合给定方法名称、方法类型,并且符合调用权限的方法句柄
* findVirtual: 调用虚方法, 参数: 方法接收者, 方法名字, 方法类型
*/
return lookup().findVirtual(receiver.getClass(), "println", mt).bindTo(receiver);
}
}

MethodHandleReflection都是模仿方法调用,区别为

  • 层次
    • MethodHandle:模拟字节码层次的方法调用
      • MethodHandles.lookup()中的findStatic()findVirtual()findSpecial分别对应指令invokestaticinvokevirtual&invokeinterfaceinvokespecial这几条字节码指令的执行权限校验行为,这些底层细节在使用Reflction时是不需要关心的
    • Reflection:模拟Java代码层次的方法调用
  • 包含信息
    • MethodHandle:轻量级
      • java.lang.invoke.MethodHandle仅包含与执行该方法相关的信息
    • Reflection:重量级
      • java.lang.reflect.Method对象是方法在Java一端的全面映像,包含方法签名、描述信符和方法属性表以及运行权限等运行时信息
  • 优化
    • MethodHandle:对字节码的方法指令调用的模拟,一些优化手段诸如方法内联,可以采用类似思路支持
    • Reflection:不支持
  • 支持语言
    • MethodHandle:可服务于所有JVM支持的语言
    • Reflection:仅支持Java

invokedynamic指令

invokedynamicMethodHandle机制的作用是一样的,都是为了解决原有4条invoke*指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码中。二者思路也相同,一个用上层Java代码和API来实现,另一个用字节码和Class中其他属性、常量来完成。

动态调用点Dynamic Call Site:每一处含有invokedynamic指令的位置

  • 指令第一个参数为CONSTANT_InvokeDynamic_info常量
    • 可以得到引导方法(Bootstrap Method)、方法类型和名称
      • 引导方法:存放在新增的BootstrapMethods属性中,固定参数,返回值为java.lang.invoke.CallSite对象,代表真正要执行的目标方法调用

根据上述信息,JVM可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法。

方法分派规则

invokedynamic指令与其他4条invoke*指令的最大区别就是分派逻辑是程序员决定的,不再固化在虚拟机中。

新公司太忙了。。。导致好久都没有更新,一直在忙于熟悉新公司的业务,对不住了您内

参考资料

JVM Specifications- Linking