JVM 中的类加载器

55 阅读8分钟

类加载器是 Java 虚拟机(JVM)的重要组成部分,但许多开发人员认为类加载器很神秘。本文旨在通过提供对 JVM 中类加载工作原理的基本了解来揭开这一主题的神秘面纱。

什么是类加载器

在 Java 虚拟机(JVM)中,class是通过一个称为类加载的过程动态加载和查找的。类加载是将类从其二进制表示(通常是 .class 文件)加载到内存中,以便 JVM 执行的过程。这就是我们需要类加载器的地方。类加载器用于将 .class 文件加载到内存中。

JVM 中类是如何加载的

类的加载分为 3 个步骤:

  1. 创建和加载步骤。首先发生的事情是使用类加载器加载类文件。有两种类加载器:JVM 提供的引导类加载器和用户定义的类加载器。然后,java.lang.Class创建类的实例。是什么使该类可供 JVM 进一步执行可以在 Java 虚拟机规范中找到详细的分步算法。

  2. 链接步骤
    在类准备好执行之前,JVM 需要执行许多准备操作,其中包括验证和准备类的执行。
    链接步骤如下:

    1. 字节码验证。 验证可确保类或接口的二进制表示在结构上正确且未损坏,否则类文件将无法链接并会引发VerifyError 错误。可以通过选项关闭验证-noverify。关闭验证可以加快JVM的启动速度,但是禁用字节码验证会破坏Java的安全保障。

    2. 准备。 为静态字段分配 RAM 并使用默认值初始化它们。

    3. 符号链接的解析。由于所有对字段、方法和其他类的引用都是符号引用。在 JVM 中,为了执行类,需要将引用转换为内部表示。

  3. 初始化步骤。

    类成功加载和链接后,就可以进行初始化。在这个阶段,会调用静态类初始化器或静态变量初始化器,以确保静态初始化块只被执行一次,静态变量被正确初始化。

此外,值得注意的是 Java 实现了延迟(或懒惰)加载类。这意味着,在应用程序明确引用已加载类的引用字段之前,不会对这些引用字段进行类加载。换句话说,字符引用解析是可选的,默认情况下不会发生。

类加载器特点

类加载器具有三个值得记住的重要功能。

  • 委托模式

当请求查找类或资源时,类加载器会将类或资源的搜索委托给其父类加载器,然后再尝试查找类或资源本身。

  • 能见度

父类加载器加载的类对其子类加载器可见,但子类加载器加载的类对其父类加载器或子类加载器不可见。

  • 独特性

在Java中,一个类是唯一标识的,ClassLoader + Class因为同一个类可以由两个不同的类加载器加载。
Class A loaded by ClassLoader A != Class A loaded by ClassLoader B
它有助于为不同的类加载器定义不同的保护和访问策略。

类加载器关系

我们应该记住,Java 中的类是按需加载的。也就是说,只有在请求时才加载类。

如您所知,用 Java 编写的每个程序的入口点都是public static void main(String[] args)方法。Main方法是加载第一个类的地方。所有随后加载的类都是由已经加载并运行的类加载的。

当正在运行的程序请求某个类时,系统类加载器会在应用程序类路径中搜索它。如果没有找到类,则搜索Platform类加载器,如果仍然没有找到,则搜索Bootstrap类加载器。

如果在父类加载器中找到请求的类,则该类加载器将加载该类。如果没有,系统类加载器将加载该类。如果之前没有加载过该类,则类加载器会将其加载到内存中,并创建代表加载的类的 Class 对象的新实例。

需要注意的是,类加载层次结构本质上是分层的,每个类加载器都有一个父类加载器。这种父子关系确保每个类加载器只负责加载自己的类,并将父类的加载委托给其父类加载器。

不同类型的类加载器

JVM 中的类加载机制并不只使用一个类加载器。每个 Java 程序至少有三个类加载器:

  • Bootstrap(原始)类加载器

这是根类加载器,负责加载核心 Java 类,例如 java.lang.Object 以及 Java 标准库(也称为 Java 运行时环境或 JRE)中的其他类。它以本机代码实现,是 JVM 本身的一部分。虽然每个类加载器都有自己的ClassLoader对象,但 Bootstrap 类加载器并没有对应的对象。例如,如果您运行这行代码
String.class.getClassLoader(),您将得到null

  • 扩展类加载器

该类加载器负责从扩展目录(例如 JRE 安装中的 jre/lib/ext 目录)加载类,并且是 Bootstrap 类加载器的子级。您还可以通过系统属性指定扩展目录的位置java.ext.dirs

  • 系统(应用程序)类加载器

这是加载特定于应用程序的类的类加载器,通常从运行 Java 应用程序时指定的类路径加载。类路径可以包含目录、JAR 文件和其他资源。可以使用 CLASSPATH 环境变量、-classpath 或 -cp 命令行选项来设置类路径。系统/应用程序类加载器也是用 Java 实现的,并且是扩展类加载器的子类。

)类加载器以及与之相关的类加载器随着时间的推移而变化

在上面的介绍中,我们已经知道了 Java 中的类加载器层次结构,但是 Java 9 对其进行了修改。

自 Java 9 以来新的类加载器层次结构如下所示:

  • Bootstrap(原始)类加载器

这是根类加载器,负责加载核心 Java 类,例如java.lang.ObjectJava 标准库(也称为 Java 运行时环境或 JRE)中的其他类。它以本机代码实现,是 JVM 本身的一部分。尽管每个类加载器都有自己的ClassLoader对象,但没有与 Bootstrap 类加载器对应的此类对象,并且通常表示为null, 并且没有父对象。例如,如果您运行这行代码
String.class.getClassLoader(),您将得到null

  • 平台类加载器(以前的扩展类加载器)

Java SE 平台中的所有类都保证通过平台类加载器可见。仅仅因为类通过平台类加载器可见并不意味着该类实际上是由平台类加载器定义的。Java SE 平台中的某些类由平台类加载器定义,而其他类则由 Bootstrap 类加载器定义。应用程序不应依赖于哪个类加载器定义哪个平台类。

  • 系统(应用程序)类加载器

这是加载特定于应用程序的类的类加载器,通常从运行 Java 应用程序时指定的类路径加载。类路径可以包含目录、JAR 文件和其他资源。可以使用 CLASSPATH 环境变量、-classpath 或 -cp 命令行选项来设置类路径。系统/应用程序类加载器也是用 Java 实现的,并且是扩展类加载器的子类加载器。

Java 9 中引入了更多与类加载器相关的更改,即:

  • Application 类加载器不再是一个实例URLClassLoader,而是一个内部类。现在,有一些ClassLoaders类本身包含 3 个内置类加载器的实现。例如:

    1. 引导类加载器

    2. 平台类加载器

    3. 应用类加载器

      • 但是,引导类加载器应该通过类使用BootLoader,而不是通过ClassLoaders类使用。
  • 扩展类加载器已重命名为平台类加载器。Java 8 Extension 类加载器和 Java 9 Platform 类加载器之间的实质性区别在于 Platform 类加载器不再是URLClassLoader. 但在大多数情况下,平台类加载器相当于过去所谓的扩展类加载器。重命名它的一个动机是删除了扩展机制,我们将在下一段中讨论这一点。

  • 删除了扩展机制 在 Java 9 之前的版本中,扩展机制允许运行时环境查找并加载扩展类,而无需在类路径中显式提及它们。然而,在 JDK 9 中,这种机制已被删除。要使用扩展类,请确保其 JAR 文件包含在类路径中。

  • 删除了rt.jar和tools.jar

    • rt.jar包含基本 Java 运行时环境的所有已编译类文件。
    • tools.jar包含 JDK 但 JRE 不需要的所有工具(javac、javadoc、javap)。