一看就明白的Java 类加载器

createh52个月前 (01-12)技术教程27

什么是类加载器

Java类加载器是Java运行时环境的一部分,它的主要职责是动态地将Java类加载到Java虚拟机的内存空间中。简单地说,当运行Java程序时,JVM 负责将必要的类加载到内存中,验证字节码的正确性,分配必要的资源,并通过将字节码转换为可由主机理解的机器语言指令来执行代码。

但是JVM载的真正含义是什么呢?Java 程序由类和接口组成,并以人类可读的 Java 代码编写。要在机器上运行此代码,需要将其转换为机器可理解的字节码。该字节码存储在JVM 可以读取和执行的.class文件中。

因此,当我们谈论“加载类”时,我们指的是在硬盘上查找对应的 .class 文件 、读取其内容并将其引入 JVM 运行时环境运行Java程序的过程。

实际上,类加载器系统不仅仅查找类,还通过强制执行 Java 运行时的二进制结构和命名空间规则来确保 Java 应用程序的完整性和安全性。同时提供了从各种来源加载类的灵活性——不仅是本地文件系统,还可以通过网络、数据库,甚至是动态生成的类。下面通过分解步骤,深入探讨。

1. 加载——初始阶段

当类加载器负责定位特定类时,该过程就开始了。这可以由 JVM 本身启动或由代码中的命令触发。本质上,ClassLoader 的工作是获取完全限定的类名(如java.lang.String)并将相应的类文件(如String.class)从硬盘上的位置加载到 JVM 内存中。

加载子系统不是一个单独的行为;它是一个独立的系统。这是一个分层中继。每个类加载器、父类加载器和子类加载器协同操作,传递责任的接力棒,直到加载正确的类。

  1. 指导协调类加载过程的基本原则是:
  2. 可见性:子类加载器可以看到其父类加载的类,但反之则不然,确保封装性;
  3. 唯一性:父类加载的类不会被子类再次加载,提高效率;
  4. 委托层次结构:应用程序类加载器将类加载请求传递给平台和引导类加载器。如果他们找不到该类,请求会沿着链向下查找;

现在让我们深入研究每个类加载器。

引导类加载器Bootstrap ClassLoader

Bootstrap ClassLoader,负责加载位于JVM 所需<JAVA_HOME>/jmods文件夹中的核心 Java 库(例如java.lang.、java.util.等)。从图中可以看出,其他 ClassLoader 都是用 Java 编写的(java.lang.ClassLoader的对象 ),这意味着它们也需要加载到 JVM 中——这也是 Bootstrap ClassLoader 承担的任务。

许多地方将 Bootstrap ClassLoader 描述为其余类加载器的“父级”。这表示逻辑继承而不是直接 Java 继承,因为 Bootstrap ClassLoader是用本机代码编写的。这可以通过以下代码确认:

jshell> System.out.println(java.lang.ClassLoader.class.getClassLoader());
null

Bootstrap ClassLoader 也是Oracle 规范中明确描述的唯一一个 ClassLoader 。其余的定义称为“用户定义”,由特定 VM 开发者自行实现。

平台类加载器Platform ClassLoader(Java 8 及更早版本中使用 Extension ClassLoader)

Java SE 20 文档说明如下:

平台类加载器负责加载平台类。平台类包括 Java SE 平台 API、它们的实现类以及由平台类加载器或其祖先定义的特定于 JDK 的运行时类。平台类加载器可以用作ClassLoader实例的父类。

但是平台类与Bootstrap ClassLoader 加载的核心类有什么区别呢?让我们尝试观察它本质上加载的内容:

jshell> ClassLoader.getPlatformClassLoader().getDefinedPackages();
$1 ==> Package[0] { } // empty

事实证明,在一个完全空的 Java 程序中,什么都没有。现在,让我们显式使用某个标准包中的类:

jshell> java.sql.Connection.class.getClassLoader()
$2 ==> jdk.internal.loader.ClassLoaders$PlatformClassLoader@27fa135a

jshell> ClassLoader.getPlatformClassLoader().getDefinedPackages()
$3 ==> Package[1] { package java.sql }

通过对比,我们可以看出Bootstrap 加载启动 JVM所需的核心运行时类,而 Platform 则加载开发人员可能需要的系统模块的公共类型。

注:Platform ClassLoader已经取代了Java 8 及更早版本中使用的 Extension ClassLoader。这一变化是随着模块系统 (JEP-261)的引入而带来的,加载器继承关系从URLClassLoader变成了BuiltinClassLoader。它不再通过扩展机制加载类,该机制已被JEP 220删除

应用程序类加载器

应用程序类加载器(Application ClassLoader),也称为系统类加载器(System ClassLoader),可以说是日常 Java 开发环境中最常遇到的。在 Java SE 20 中,它仍然保留其传统的角色和功能。

该类加载器负责从已设置的类路径加载所有类。这可能来自目录、JAR 文件或类路径中指定的其他来源。当 Java 程序启动时,大多数用户定义的代码都是在这里加载的。

public  class  MediumTeller { 

    public  static  void  main (String[] args) { 
        // jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7
         System.out.print(MediumTeller.class.getClassLoader()); 
    } 
}

从类加载器层次结构的角度来看,应用程序类加载器是平台类加载器的逻辑子级。这意味着,当加载一个类时,如果应用程序类加载器找不到该类,则请求将向上委托给平台类加载器,如果需要,还会进一步向上委托给引导程序,从而确保委托机制。

除了我们讨论的三个主要类加载器之外,还可以直接在代码中创建自己的类加载器。此功能提供了一种确保应用程序独立性的途径,并由类加载器委托模型提供便利。Tomcat 等 Web 应用程序服务器利用这种方法来确保托管在同一服务器上的不同Web 应用程序可以独立运行。

每个类加载器都维护自己的名称空间,记录它已加载的类。当类加载器负责加载类时,首先查询此命名空间,搜索完全限定类名 (FQCN) 以确定该类是否已加载。即使一个类与另一个类共享相同的 FQCN,如果它们存在于不同的命名空间中,它们也被视为不同的类。在不同的命名空间中拥有一个类意味着它是由不同的类加载器加载的,从而加强了应用程序不同部分之间的自治和隔离。

加载阶段的结果是 JVM 中类或接口类型的二进制表示。然而,此时该类还没有准备好使用。

2. 链接

链接阶段涉及几个复杂的步骤,以确保程序的顺利执行。此阶段将加载的类或接口作为输入,并执行基本任务来验证代码的完整性、准备执行并解决它可能具有的任何依赖项。

一旦类被加载,它就会经历一个称为链接的阶段。此阶段涉及一系列步骤:

校验

此阶段对于维持 Java 运行时环境的健壮性至关重要。它查看类或接口的字节码,以确保其结构正确性、与 JVM 的兼容性,并验证它是由合法编译器编译生成的。

在可以通过网络传输并可能由恶意编译器生成的Java 世界中,这个过程变得至关重要。它检查符号表中的一致性、最终方法或类是否被不正确地重写、访问控制关键字的正确性、参数的准确数量和类型、正确的堆栈操作等等。

最后,如果验证检测到任何异常,它会抛出java.lang.VerifyError,导致java.lang.LinkageError.

准备

在此步骤中,JVM为类或接口的静态变量分配内存,并使用默认值初始化它们。

此阶段不执行用户定义的初始化代码。

如果类或接口具有实例字段,一旦整个类层次结构的静态字段被寻址,这些实例字段也会被分配内存并分配默认值。此准备步骤为程序的执行奠定基础,从而实现高效的运行时性能。由于链接涉及新数据结构的分配,因此可能会失败并出现OutOfMemoryError错误。

解析

在这个步骤中,类或接口中的任何符号引用(指向其他类或接口的逻辑引用)都将替换为它们的实际内存地址。这种从符号引用到直接引用的转换(通常称为动态链接)可确保类或接口的所有依赖项在运行时可用。

这一步可以“惰性”执行,即当带有符号引用的语句时才会执行。大多数 JVM 使用的这种方法可以节省资源,因为它可以防止不必要地加载,比如可能永远不会被调用的类或接口。如果无法找到符号引用引用的类,则会引发java.lang.ClassDefNotFoundjava.lang.ClassNotFound异常。

3. 初始化

初始化将执行每个加载类或接口的初始化逻辑(例如调用类的构造函数)。由于JVM是多线程的,类或者接口的初始化必须同步,防止多个线程同时初始化,保证线程安全。

JVM 调用特殊<clinit>方法(静态块和变量赋值的字节码版本),将所有静态变量设置为其指定的初始值。至此,所有的类终于做好了执行的准备。

相关文章

这一篇文章,可以把Java中的类加载器了解的七七八八了

前言对于每个开发人员来说,java.lang.ClassNotFoundExcetpion这个异常几乎都遇到过,而追求其该异常的来源的话,就免不了谈一谈Java的类加载器了。本文就基于启动类加载器、扩...

Java类是如何加载的?

有小伙伴最近在面试过程中遇到这样一个问题:Java 中的类是如何加载的?这个问题还是很有意思,今天松哥来尝试和大伙梳理一下。一 整体思路整体上来说,类的加载主要是下面这几个步骤:上面这张图就是一个类的...