类加载器

类加载器是 Java 虚拟机 (JVM) 的一个核心子系统,它的主要职责是:

  1. 在程序运行时,将存储在 .class 文件中的二进制字节码数据“加载”到 JVM 的内存中。
  2. 生成一个 java.lang.Class 类的对象,并将其指向这个.class 文件

简单来说,当 Java 程序需要使用某个类时,JVM 不会一开始就加载所有类。它会在真正需要这个类的时候(例如创建该类的实例、调用其静态方法等),通过类加载器去找到对应的 .class 文件,读取其内容,并在内存中生成一个代表这个类的 Class 对象。之后,JVM 就可以通过这个 Class 对象来创建实例、访问字段和调用方法了。

如何判别两个类是否是同一个类

  1. 对于任意一个类,需要由加载它的类加载器和类本身共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
  2. 比较两个类是否“相等(这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果)”,只有在这两个类是由同一个类加载器加载的前提下才有比较的意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

三层类加载器

  1. 引导类加载器 (Bootstrap ClassLoader):
    • 作用: 负责加载 Java 的核心类库。
    • 加载: java.lang.*, java.util.* 等包下的所有基础类。
    • 特点: 它是用本地代码 (C/C++) 实现的,是 JVM 的一部分,因此在 Java 代码中无法直接获取到它的引用(String.class.getClassLoader() 返回 null 就是因为它是由 Bootstrap 加载的)。
  1. 扩展类加载器 (Extension ClassLoader):
    • 作用: 负责加载 Java 的扩展类库。
    • 加载路径: JAVA_HOME/jre/lib/ext 目录下的 JAR 包,或者由系统属性 java.ext.dirs 指定的路径中的 JAR 包。
    • 特点: 它是用 Java 编写的,父加载器是 Bootstrap ClassLoader。
  1. 系统类加载器 (Application ClassLoader / System ClassLoader):
    • 作用: 负责加载用户自己编写的类,以及项目 classpath 下的所有 JAR 包和目录。
    • 加载路径: 环境变量 CLASSPATH、命令行参数 -classpath-cp 指定的路径。
    • 特点: 这是我们在开发中最常打交道的类加载器。它是用 Java 编写的,父加载器是 Extension ClassLoader。如果用户没有自定义类加载器,这就是默认的应用程序类加载器

双亲委派模型

双亲委派模型定义

各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents DelegationModel)”。是一个自顶向下层级关系。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

双亲委派模型的工作过程

  • 工作原理:
    1. 当一个类加载器收到加载某个类的请求时,它将判断这个类是否已经加载。
    2. 如果加载过,则不在进行加载,直接返回;
      如果没有加载过它不会立即自己去加载,会先将这个请求委托给自己的父类加载器去尝试加载。
    3. 父类加载器收到请求后,也会,做相同的内容,判断是否需要加载,然后继续向上委托,直到传递给最顶层的 Bootstrap ClassLoader
    4. 只有当父类加载器无法完成加载(例如,在其负责的路径下找不到该 .class 文件)时,子类加载器才会尝试自己去加载。
  • 示意图:

应用程序类加载器 -> 请求 -> 扩展类加载器 -> 请求 -> 启动类加载器
                                                  |
                                                  | (找不到)
                                                  v
                                              扩展类加载器尝试加载
                                                  |
                                                  | (找不到)
                                                  v
                                          应用程序类加载器尝试加载

为什么使用双亲委派模型

  • 避免重复加载: 确保一个类只会被加载一次。
  • 保证核心类的安全: 防止用户自定义的、与核心类同名的恶意类被加载。例如,你不能自己写一个 java.lang.String 类来替换掉系统的 String 类,因为当请求加载 java.lang.String 时,最终会由 Bootstrap ClassLoader 去加载系统自带的那个,而你的类永远得不到机会。

双亲委派模型的底层实现

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    // 首先,检查请求的类是否已经被加载过了
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // 如果父类加载器抛出ClassNotFoundException
            // 说明父类加载器无法完成加载请求
        }
        if (c == null) {
            // 在父类加载器无法加载时
            // 再调用本身的findClass方法来进行类加载
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

实现过程:

  1. 先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的loadClass()方法。
  2. 若父加载器为空则默认使用启动类加载器作为父加载器。
  3. 假如父类加载器加载失败,抛出ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。

注:图片来源马士兵教育

破坏双亲委派模型

由于双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委派模型主要出现过3次较大规模“被破坏”的情况。

第一次“被破坏”

第一次被破坏由于版本原因,由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。

第二次“被破坏”

第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),但是如果有基础类型又要调用回用户的代码,那该怎么办呢?

解决方法

类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。使用这个线程上下文类加载器去加载所需的子类加载器服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则。

第三次“被破坏”

由于用户对程序动态性的追求(代码热替换(Hot Swap)、模块热部署(Hot Deployment))而导致的。

模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:

1)将以java.*开头的类,委派给父类加载器加载。

2)否则,将委派列表名单内的类,委派给父类加载器加载。

3)否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。

4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。

5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器

加载。

6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。

7)否则,类查找失败。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐