第 7 章 虚拟机类加载机制

felix.shao2025-02-16

第 7 章 虚拟机类加载机制

7.1 概述

7.2 类加载的时机

7.3 类加载的过程

 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、 初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析 3 个部分统称为连接(Linking)。如下图所示。
class_lifecycle.png

 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按步就班的开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的运行时绑定特性。
 需要额外注意以下核心要点。

  • 六种情况必须立即对类进行“初始化”。

7.3.1 加载

 加载阶段,Java 虚拟机需要完成以下 3 件事情。

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

 加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能以及开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部分,这两个阶段的开始时间仍然保持着固定的先后顺序。

7.3.2 验证

 验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成下面 4 个阶段的检验动作。

  • 文件格式验证。
  • 元数据验证。
  • 字节码验证。
  • 符号引用验证。

7.3.3 准备

 准备阶段是正式为类中定义的变量(被 static 修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
 这个阶段中有两个容易产生混淆的概念需要强调一下:首先,这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:public static int value=123; 那变量 value 在准备阶段过后的初始值为 0 而不是 123,因为这时候尚未开始执行任何 Java 方法,而把 value 赋值为 123 是后续的初始化环节。

7.3.4 解析

 解析阶段是 JVM 将常量池内的符号引用替换为直接引用的过程。
 需要额外注意以下核心要点。

  • 符号引用和直接引用的概念。
  • 4 种引用的解析过程。
    • 类或接口的解析。
    • 字段解析。
    • 方法解析。
    • 接口方法解析。

7.3.5 初始化

 类的初始化阶段是类加载过程的最后一个步骤。到此步骤时,Java 虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。
 进行准备阶段时,变量已经赋给一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
 需要额外注意以下核心要点。

  • 父类、子类的类变量、实例变量、静态代码块加载顺序。

7.4 类加载器

 “通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”。

7.4.1 类与类加载器

 需要额外注意以下核心要点。

  • 比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。

7.4.2 双亲委派模型

 以 Java 虚拟机的角度看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由 Java 语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
 以开发角度看,Java 一直保持着三层类加载器、双亲委派的类加载器架构。  JDK9 之前的 Java 应用都是以下三种类加载器互相配合来完成加载的。

  • 启动类加载器:Bootstrap ClassLoader,用来加载 Java 核心类库,无法被 Java 程序直接引用,如加载存放在 JDK\jre\lib(JDK 代表 JDK 的安装目录,下同)下,或被 -Xbootclasspath 参数指定的路径中的,并且能被虚拟机识别的类库。
  • 扩展类加载器:Extension ClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 JDK\jre\lib\ext 目录中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如 javax.* 开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器:Application ClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
  • 用户自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现。
ClassLoadStructure.png

 双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
 使用双亲委派模型的好处如下。

  • Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系。即保证 Object 类在程序的各种类加载器环境中都能够保证是同一个类。

7.4.3 破坏双亲委派模型

 双亲委派模型主要出现过 3 次较大规模“被破坏”的情况。

  • 第一次是发生在双亲委派模型出现之前。
  • 第二次是由这个模型自身的缺陷导致的。双亲委派很好地解决了各个类加载器协作时基础类型(它们总是被用户代码继承)的一致性问题,但是很难满足基础类型又要调用回用户的代码的场景。典型示例是 JNDI 服务。
  • 第三次是由于用户对程序动态性的追求导致的。这里的动态性指代码热替换、模块热部署等。

7.5 Java 模块化系统

7.5.1 模块的兼容性

7.5.2 模块化的类加载器

3.5 初始化

4.3 破坏双亲委派模型

5 Java 模块化系统

5.1 模块的兼容性

  略。

5.2 模块化下的类加载器

  模块化下的类加载器发生了一些变动(也算是对双亲委派的第四次破坏),主要如下。

  • 扩展类加载器(Extension ClassLoader)被平台类加载器(Plantform ClassLoader) 取代。因为模块化后,JDK9 已天然支持可扩展的需求。
  • 平台类加载器和应用程序类加载器都不再派生自 java.net.URLClassLoader。现在启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader,在 BuiltinClassLoader 中实现了新的模块化架构下类如何从模块中加载的逻辑,以及模块中资源可访问性的处理。

 JDK9 以后的三层类加载器的架构如图所示。
ClassLoadStructureNew.jpeg

 JDK9 中虽然仍然维持着三层类加载器和双亲委派的架构,但类加载的委派关系也发生了变动。当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。

参考文献

Last Updated 2/16/2025, 4:13:06 PM