Java 虚拟机入门教程之六 JVM 类加载机制

大纲

双亲委派机制

双亲委派机制(Parent Delegation Model)是 Java 类加载器的一种工作机制,其基本原则是:每个类加载器在接收到类加载请求时,会先将该请求委派给它的父类加载器处理,只有在父类加载器无法完成该请求时,才由自己来加载。通过这种机制,可以防止内存中出现多份同样的字节码(从安全性角度考虑),同时确保了核心类库始终由启动类加载器加载,避免了自定义类库对核心类库的破坏。这种层次化的加载过程有助于提升系统的安全性和稳定性,同时也便于管理不同级别的类库。比如,加载位于 $JAVA_HOMEjre/lib/rt.jar 包中的 Object 类时,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。

  • (1) 当一个类加载器(比如应用程序类加载器)接收到类加载请求时,首先会将请求委派给它的父类加载器(扩展类加载器)。
  • (2) 扩展类加载器在接收到加载请求后,会继续将请求向上委派给它的父类加载器(启动类加载器)。
  • (3) 启动类加载器尝试加载该类,如果加载成功,则加载过程结束;如果加载失败(例如类不在它的搜索路径中),则将加载请求返回给扩展类加载器。
  • (4) 扩展类加载器尝试加载该类,如果加载成功,则加载过程结束;如果加载失败,则将加载请求返回给应用程序类加载器。
  • (5) 最后,应用程序加载器会尝试自己加载类,如果加载失败,则会抛出 ClassNotFoundException 异常。

Java 类加载的过程

Java 类加载过程是将类的字节码加载到内存中,并将其转换为 Java 类对象的过程。这个过程主要包括以下几个步骤:

  • 1. 加载(Loading)

    • 在这个阶段,类加载器从文件系统或网络中读取类的字节码(通常是从 .class 文件中读取)到 JVM 内存中,并创建一个 Class 对象来表示这个类。
    • 字节码可以从多种不同的来源加载,比如本地文件系统、JAR 包或者网络。
  • 2. 连接(Linking):连接阶段分为以下三个子阶段

    • 验证(Verification):确保被加载的类的字节码符合 Java 语言规范,并且不会危害 JVM 的安全。这个阶段会进行各种字节码级别的检查,以确保类的结构和行为是合法的。
    • 准备(Preparation):为类的静态变量分配内存,并将这些变量初始化为默认值(如 0 或者 Null 等)。这个阶段仅分配内存,尚未执行任何代码。
    • 解析(Resolution):将类的符号引用(如类名、方法名等)转换为直接引用(内存地址)。这一步骤可以在连接阶段的任何时候执行,也可以推迟到类被使用的时候执行。
  • 3. 初始化(Initialization)

    • 初始化阶段是执行类的静态代码块(static {})和为静态变量赋予正确的初始值(即编写代码时定义的值)。
    • 这一步骤是类加载过程的最后一步,只有在这一步完成后,类才会被认为是完全加载的。

在类加载的整个过程中,Java 虚拟机会使用双亲委派机制来保证类加载的安全性和稳定性。双亲委派机制要求每个类加载器在接收到类加载请求时,会先将该请求委派给它的父类加载器处理,只有在父类加载器无法完成该请求时,才由自己来加载。

类加载过程的总结

  • 加载:读取 Class 文件并创建 Class 对象。
  • 连接:验证、准备和解析类的符号引用。
  • 初始化:执行静态代码块和静态变量的初始化。

JVM 加载 Class 文件的原理

类加载器负责加载 Class 文件,而 Class 文件在文件开头有特定的文件标识(魔数)。ClassLoader 只负责 Class 文件的加载,至于它是否可以运行,则由 Execution Engine(执行引擎)决定。

魔数的概念

  • Class 文件开头的四个字节的无符号整数称为魔数(Magic Number)。
  • 魔数是 Class 文件的标识。值是固定的,为 0xCAFEBABE
  • 如果一个 Class 文件的头四个字节不是 0xCAFEBABE,虚拟机在进行文件校验的时候会报错。使用魔数而不是文件扩展名来识别 Class 文件,主要是基于安全方面的考虑,因为文件扩展名可以随意更改。

类加载器的种类

类加载器分为四种,前三种是 JVM 自带的类加载器。特别注意,它们之间并没有继承关系,但内部都有一个 parent 属性。

  • 启动类加载器(BootStrapClassLoader),由 C++ 语言实现
    • 不是 Java.lang.ClassLoader 的子类
    • 负责加载 $JAVA_HOMEjre/lib/rt.jar 里所有的 Class,例如 java.lang.Objectjava.lang.String
    • 是 Java 虚拟机的一部分,而且是用本地代码实现的,不是 Java 类,因此在 Java 中无法直接引用它
  • 扩展类加载器(ExtensionClassLoader),由 Java 语言实现
    • 别名叫平台类加载器(PlatformClassLoader)
    • Java.lang.ClassLoader 的子类,全类名为 sun.misc.Launcher$ExtClassLoader
    • 负责加载 Java 平台中扩展功能的一些 Jar 包,包括 $JAVA_HOMEjre/lib/ext/*.jar-Djava.ext.dirs 指定目录下的 Jar 包
  • 应用程序类加载器(ApplicationClassLoader),由 Java 语言实现
    • 别名叫系统类加载器(SystemClassLoader)
    • 负责加载 classpath 中指定的 Jar 包及目录中 Class,这些类通常是由用户编写的
    • 在 Java 应用程序中,它是默认的类加载器,也是 Java.lang.ClassLoader 的子类,全类名为 sun.misc.Launcher$AppClassLoader
  • 自定义加载器(CustomClassLoader),由 Java 语言实现
    • Java.lang.ClassLoader 的子类
    • 用户可以使用自定义类加载器实现特定的类加载需求,比如从网络上加载类,或者从数据库中加载类等
    • 用户可以重写父类的 loadClass(String name, boolean resolve) 方法来改变类加载的行为,还可以重写父类的 findClass(String name) 方法来实现自定义的类查找逻辑

类加载器的工作流程

  • (1) 当 ApplicationClassLoader 加载一个 Class 时,它首先不会自己去尝试加载这个类,而是将类加载请求委派给它的父类加载器 ExtensionClassLoader。
  • (2) ExtensionClassLoader 在接收到加载请求后,会继续将请求向上委派给它的父类加载器 BootStrapClassLoader。
  • (3) BootStrapClassLoader 尝试加载该类,如果加载成功,则加载过程结束;如果加载失败(比如在 $JAVA_HOME/jre/lib/rt.jar 里查找不到到该 Class),则将类加载请求返回给 ExtensionClassLoader。
  • (4) ExtensionClassLoader 尝试加载该类,如果加载成功,则加载过程结束;如果加载失败,则将类加载请求返回给 ApplicationClassLoader。
  • (5) 最后,ApplicationClassLoader 会尝试自己加载类,如果加载失败,则会抛出 ClassNotFoundException 异常。

上述类加载器的工作流程,其本质就是双亲委派机制。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父类加载器去完成,依次向上传递。如果父类加载器都无法完成加载任务,那么自己就会去执行加载任务。