jvm类的加载及方法调用过程

类的加载过程

  • 加载
    • 加载是指查找字节流,并且据此创建类的过程。
    • 加载需要借助类加载器,在 Java 虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。
  • 链接
    • 验证:确保被加载类能够满足 Java 虚拟机的约束条件
    • 准备:为被加载类的静态字段分配内存;部分 Java 虚拟机还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表
    • 解析:将符号引用解析成为实际引用;如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)
  • 初始化
    • 常量值(ConstantValue)初始化直接由 Java 虚拟机完成。
    • 除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >。
    • 初始化触发时机
      • 当虚拟机启动时,初始化用户指定的主类;
      • 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
      • 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
      • 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
      • 子类的初始化会触发父类的初始化;
      • 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
      • 使用反射 API 对某个类进行反射调用时,初始化这个类;
      • 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

jvm方法调用

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

对于 Java 语言中重写而 Java 虚拟机中非重写的情况,编译器会通过生成桥接方法 [2] 来实现 Java 中的重写语义。

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

方法调用相关指令

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

符号引用

在编译过程中,我们并不知道目标方法的具体内存地址。因此,Java 编译器会暂时用符号引用来表示该目标方法。这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符。

符号引用存储在 class 文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号引用(InterfaceMethodref)和非接口符号引用(Methodref)。

在执行使用了符号引用的字节码前,Java 虚拟机需要解析这些符号引用,并替换为实际引用。

  • 对于非接口符号引用,查找方法过程
    假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找。
    • 在 C 中查找符合名字及描述符的方法。
    • 如果没有找到,在 C 的父类中继续搜索,直至 Object 类。
    • 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需满足 C 与该接口之间没有其他符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个。
  • 接口符号引用,查找方法过程
    假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找。
    • 在 I 中查找符合名字及描述符的方法。
    • 如果没有找到,在 Object 类中的公有实例方法中搜索。
    • 如果没有找到,则在 I 的超接口中搜索。这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致。

经过上述的解析步骤之后,符号引用会被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。

静态绑定

在 Java 虚拟机中,静态绑定包括用于调用静态方法的 invokestatic 指令,和用于调用构造器、私有实例方法以及超类非私有实例方法的 invokespecial 指令。如果虚方法调用指向一个标记为 final 的方法,那么 Java 虚拟机也可以静态绑定该虚方法调用的目标方法。

动态绑定

Java 虚拟机中采取了一种用空间换取时间的策略来实现动态绑定。它为每个类生成一张方法表,用以快速定位目标方法。

invokevirtual指令方法表

方法表两个特性,其一,子类方法表中包含父类方法表中的所有方法;其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同

方法调用指令中的符号引用会在执行之前解析成实际引用。对于静态绑定的方法调用而言,实际引用将指向具体的目标方法。对于动态绑定的方法调用而言,实际引用则是方法表的索引值(实际上并不仅是索引值)。

在执行过程中,Java 虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。

Java 虚拟机执行某个类方法时,先会找到该类实际类型的方法表,然后根据解析时绑定的方法表索引值,获取目标方法。

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

内联缓存

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