JVM--加载Java类及方法调用
2018-09-30 本文已影响0人
_fatef
1. JVM是如何加载Java类的?
加载
- 是指查找字节流,并且据此创建类的过程。
- 前面提到,对于数组类来说,它并没有对应的字节流,而是由 Java 虚拟机直接生成的。
- 对于其他的类来说,Java 虚拟机则需要借助类来说,Java 虚拟机则需要借助类加载器来完成查找字节流的过程。
连接
把类的二进制数据合并到JRE中,分为三个阶段。
- 校验:确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 准备:为被加载类的静态变量分配内存,以及构造与该类相关的方法表。
- 解析:将符号引用转化为实际引用。
初始化
- 对类的静态变量,静态代码块执行初始化操作
- 如果直接赋值的静态字段被final 所修饰,并且它的类型是基本类型或字符串时,那么字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由JVM 完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被Java 编译器置于同一方法中,并把它命名为< clinit >。
注:类的初始化触发的必要条件
- 当虚拟机启动时,初始化用户指定的主类;
- 当遇到new、getstatic、putstatic或invokestatic这4条字节码指定时, 如果类没有进行过初始化,则需要先触发器初始化。生成这四条指定的最常见的Java代码场景是:使用new 关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候;
- 子类的初始化会先进行父类的初始化;
- 如果一个接口定义了 default 方法,那么直接或者间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射 API 对某个类进行反射调用时,初始化这个类;
- 当初次调用MethodHandle 实例时, 初始化 该MethodHandle 指向的方法所在的类。
2. JVM如何识别目标方法的?
- 重载:同一个类中定义名字相同的方法,你们它们的参数类型必须不同。即重载的方法在编译过程中即可完成识别。
- 具体到每一个方法的调用,Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取过程共分为三个阶段:
- 在不考虑对基本类型自动装拆箱(auto-boxing,auto-unbonxing),以及可变长参数的情况下选取重载方法;
- 如果在第一阶段中没有找到适配的方法, 那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;
- 如果在第二阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。
注:
如果Java 编译器在同一个阶段中找到了多个适配的方法,那么它会在其中选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系。
- 如果子类定义了与父类中非私有方法同名的方法,而这两个方法的参数类型相同,那么这两个方法之间是什么关系?
- 如果这两个方法都是静态的,那么子类中的方法隐藏了父类的方法。
- 如果这两个方法都不是静态的,且都不是私有的,那么子类的方法重写了父类的方法。
3. JVM的静态绑定和动态绑定
- JVM识别方法的关键在于类名、方法名以及方法描述符(method descriptor)(方法的参数类型以及返回类型所构成)。
- JVM允许参数类型相同,返回类型不同的方法出现在同一个类中,JVM能够准确识别目标方法。
- 静态绑定(static binding)(重载):也叫编译时多态(compile-time polymorphism),在解析时JVM 可以直接识别目标方法;
- 动态绑定(dynamic binding)(重写):JVM 在运行过程中根据调用者的动态类型来识别目标方法。
- Java字节码中与调用相关的指令:
- invokestatic:用于调用静态方法;
- invokespecial:用于调用私有实例方法、构造器,以及使用super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法;
- invokevirtual:用于调用非私有实例方法;
- invokeinterface:用于调用接口方法;
- invokedynamic:用于调用动态方法。
注:
invokestatic、invokespecial,JVM可以直接识别具体的目标方法,而invokevirtual、invokeinterface 在绝大多数情况下,JVM需要在执行过程中,根据调用者的动态类型,来确定目标的目标方法。
4. JVM调用指令的符号引用
- 在编译过程中,我们并不知道目标方法的具体内存地址。因此,Java编译器会暂时用符号引用来表示该目标方法。这一符号引用包括目标方法所在的类或者接口的名字,以及目标目标方法的方法名和方法描述符。
- 符号引用存储在class文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号(InterfaceMethodref)引用和非接口符号(Methodref)引用。
- 符号引用 => 实际引用:
- 非接口符号引用,假定该符号引用所指向的类为C,则JVM 会按照如下步骤进行查找。
- 在C中查找符合名字及描述符的方法;
- 如果没找到,在C 的父类中继续搜索,直至Object 类;
- 如果没找到,在C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需满足 C 与该接口之间没有其他符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个。
注:
静态方法也可以通过子类来调用。此外子类的静态方法会隐藏父类找那个的同名、通描述符的静态方法。
- 接口符号引用,假定该符号引用所指向的接口为 I,则JVM 会按照如下步骤查找。
- 在 I 中查找符合名字及描述符的方法;
- 如果没有找到,在 Object 类中的共有实例方法中搜索;
- 如果没有找到,则在 I 的超接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需满足 C 与该接口之间没有其他符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个。
5. 动态绑定策略
- 方法表(空间换时间):其本质是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法。
- (虚)方法表(virtual method table,vtable)满足两个特质:其一,
子类方法表中包含父类方法表中的所有方法
;其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同
。
6. 内联缓存(inlining cache)
- 内联缓存是一种加快动态绑定的优化技术。 它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。
- 在针对多态的优化手段中,有三个术语:
- 单态(monomorphic)指的是仅有一种状态的情况。
- 多态(polymorphic)指的是有限数量种状态的情况。二态(bimorphic)是多态的其中一种。
- 超多态(megamorphic)指的是更多种状态的情况。(通常有一个具体的数值来区分多态和超多态)
- 实现
- 比较缓存的动态类型,如果命中,则直接调用对应的目标方法
- 多态内联缓存则缓存了多个动态类型及其目标方法。它需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法。