JVM类加载过程,超详细文章

43 阅读9分钟

类加载机制

类加载的时机

一个类从被加载到虚拟机内存中到卸载,都会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。 在这里插入图片描述

  • 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段则不一定,在某些情况下可以在初始化阶段之后再开始(为了支持 Java 语言的运行时绑定特性:动态绑定或晚期绑定)。

立即对类初始化的情况

  • 遇到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。
    • 使用 new 关键字实例化对象的时候
    • 读取或设置一个类型的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候
    • 调用一个类型的静态方法的时候
  • 使用 java.lang.reflect 包的方法对类型进行反射调用的时候
  • 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类
  • 被 default 关键字修饰的接口方法,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

类加载

  • 通过全路径类名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口

连接(验证、准备、解析)

验证

  • 验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
  • 验证大概会完成下面四个阶段的检验动作:
    • 文件格式验证、元数据验证、字节码验证和符号引用验证

准备

  • 准备阶段是正式为类中定义的变量(被 static 修饰的静态变量)分配内存并设置类变量初始值的阶段
  • 此阶段针对的是类变量,而不是实例变量。实例变量将会在对象实例化时,随着对象一起分配在 Java 堆中
    • 在准备阶段初始化值的示例:
      public static final int value = 123;
      
    • 在初始化阶段先赋零值再进行具体赋值的示例:
      public static int value = 123;
      
  • 基本数据类型零值参考 在这里插入图片描述

解析

  • 解析阶段是将 Java 虚拟机常量池内的符号引用替换为直接引用的过程
    • 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位目标即可。同一个符号引用可以被多次解析
    • 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的一般不会相同。有了直接引用,那么引用的目标必定已经在虚拟机的内存中存在
类或接口的解析

假设当前代码所处的类为 D,如果要把一个从未解析过的符号引用 R 解析为一个类或接口 C 的直接引用,那虚拟机完成整个解析的过程需要包括以下 3 种情况:

  • 如果 C 不是一个数组类型,那虚拟机将会把代表 R 的全路径类名传递给 D 的类加载器去加载这个 C 类。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就将宣告失败
  • 如果是数组类型,数组类(如 Integer[])不是从 .class 文件加载的,而是由 JVM 在运行时根据其元素类型动态生成的
  • 如果上面两步没有任何异常,那么 C 在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成前还要进行符号引用验证,确认 D 是否具备对 C 的访问权限。如果发现不具备访问权限,将会抛出 java.lang.IllegalAccessError 异常
字段解析

要解析一个未被解析过的字段符号引用,首先将会对字段表内 class_index 项中索引的字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那把这个字段所属的类或接口用 C 表示,按照《Java 虚拟机规范》则对 C 进行后续字段的搜索:

  • 如果 C 本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
  • 否则,如果在 C 中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
  • 否则,如果 C 不是 java.lang.Object 的情况下,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
  • 否则查找失败,抛出 java.lang.NoSuchFieldError 异常。如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出 java.lang.IllegalAccessError 异常
方法解析

方法解析的第一个步骤与字段解析一样,也是需要先解析出方法表的 class_index 项中索引的方法所属的类或接口的符号引用,如果解析成功,那么我们依然用 C 表示这个类,接下来虚拟机将会按照如下步骤进行后续的方法搜索:

  • 由于 Class 文件格式中类的方法和接口的方法符号引用的常量类型定义是分开的,如果在类的方法表中发现 class_index 中索引的 C 是个接口的话,那就直接抛出 java.lang.IncompatibleClassChangeError 异常
  • 如果通过了第一步,在类 C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
  • 否则,在类 C 的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
  • 否则,在类 C 实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类 C 是一个抽象类,这时候查找结束,抛出 java.lang.AbstractMethodError 异常
  • 否则,宣告方法查找失败,抛出 java.lang.NoSuchMethodError。最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出 java.lang.IllegalAccessError 异常
接口方法解析

接口方法也是需要先解析出接口方法表的 class_index 项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用 C 表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索:

  • 与类的方法解析相反,如果在接口方法表中发现 class_index 中的索引 C 是个类而不是接口,那么就直接抛出 java.lang.IncompatibleClassChangeError 异常
  • 否则,在接口 C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
  • 否则,在接口 C 的父接口中递归查找,直到 java.lang.Object 类(接口方法的查找范围也会包括 Object 类中的方法)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
  • 对于规则 3,由于 Java 的接口允许多重继承,如果 C 的不同父接口中存有多个简单名称和描述符都与目标相匹配的方法,那将会从这多个方法中返回其中一个并结束查找
  • 否则,宣告方法查找失败,抛出 java.lang.NoSuchMethodError 异常

类初始化

类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由 Java 虚拟机来主导控制。直到初始化阶段,Java 虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。