Skip to main content

类加载器

David LiuAbout 9 min

类加载器

从上面的介绍可以看出:

  • 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
  • 每个 Java 类都有一个引用指向加载它的 ClassLoader
  • 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。

类加载器虽然只用于实现类的加载动作,但它在 Java 程序中起到的作用却远超类加载阶段。对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个 Java 虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

这里所指的“相等”,包括代表类的 Class 对象的 equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用 instanceof 关键字做对象所属关系判定等各种情况。如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果,代码清单 7-8 中演示了不同的类加载器对 instanceof 关键字运算的结果的影响。

类加载器加载规则

JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。

对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

双亲委派模型

站在 Java 虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由 Java 语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

站在 Java 开发人员的角度来看,类加载器就应当划分得更细致一些。自 JDK 1.2 以来,Java 一直保持着三层类加载器、双亲委派的类加载架构,尽管这套架构在 Java 模块化系统出现后有了一些调整变动,但依然未改变其主体结构,我们将在 7.5 节中专门讨论模块化系统下的类加载器。

三层类加载器

  • 启动类加载器(Bootstrap Class Loader):前面已经介绍过,这个类加载器负责加载存放在 <JAVA_HOME>\lib目录,或者被-Xbootclasspath 参数所指定的路径中存放的,而且是 Java 虚拟机能够 识别的(按照文件名识别,如 rt .jar、t ools.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类 库加载到虚拟机的内存中。
  • 扩展类加载器(Extension Class Loader):这个类加载器是在类 sun.misc.Launcher$ExtClassLoader 中以 Java 代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被 java.ext.dirs 系统变量所 指定的路径中所有的类库。
  • 应用程序类加载器(Application Class Loader):这个类加载器由
    sun.misc.Launcher$AppClassLoader 来实现。由于应用程序类加载器是 ClassLoader 类中的 getSy stem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。

双亲委派模型

图 7-2 中展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

读者可能注意到前面描述这种类加载器协作关系时,笔者专门用双引号强调这是“通常”的协作关系。类加载器的双亲委派模型在 JDK 1.2 时期被引入,并被广泛应用于此后几乎所有的 Java 程序中,但它并不是一个具有强制性约束力的模型,而是 Java 设计者们推荐给开发者的一种类加载器实现的最佳 实践。

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

双亲委派模型对于保证 Java 程序的稳定运作极为重要,但它的实现却异常简单,用以实现双亲委

派的代码只有短短十余行,全部集中在 java.lang.ClassLoader 的 loadClass()方法之中,如代码清单 7-10 所示。

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;
}

这段代码的逻辑清晰易懂:先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的 loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败, 抛出 ClassNotFoundException 异常的话,才调用自己的 findClass()方法尝试进行加载。

双亲委派模型的好处

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。

如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。双亲委派模型可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoaderBootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。

JVM 双亲委派模型是一种类加载机制,它的主要作用是保证 Java 核心库的安全性和稳定性,同时避免重复加载类。

在 JVM 中,类加载器按照一定的层次关系进行组织,每个类加载器都有一个父类加载器,最终的父类加载器是 Bootstrap ClassLoader,它是由 JVM 实现的,用于加载 Java 核心库。当一个类加载器需要加载一个类时,它首先会委派给它的父类加载器去加载,如果父类加载器无法加载该类,则该类加载器才会尝试自己去加载。这样的委派过程一直持续到 Bootstrap ClassLoader,如果 Bootstrap ClassLoader 无法加载该类,则会抛出 ClassNotFoundException 异常。

这种双亲委派模型的好处在于:

  1. 避免重复加载类:如果一个类已经被父类加载器加载了,那么子类加载器就不需要再次加载该类,从而避免了重复加载类的问题。

  2. 保证 Java 核心库的安全性和稳定性:由于 Java 核心库是由 Bootstrap ClassLoader 加载的,因此它的安全性和稳定性得到了保证。如果允许应用程序加载 Java 核心库,那么就可能会出现不同版本的 Java 核心库之间的冲突,从而导致应用程序出现不可预测的错误。

  3. (支持类的升级:如果一个类已经被父类加载器加载了,那么子类加载器就无法重新加载该类。这样就可以支持类的升级,即在不重启 JVM 的情况下,使用新版本的类替换旧版本的类。)

总之,JVM 双亲委派模型是一种非常重要的类加载机制,它保证了 Java 核心库的安全性和稳定性,同时避免了重复加载类的问题,支持类的升级,是 Java 语言的一个重要特性。

JVM 的双亲委派模型并不直接支持类的升级,因为在该模型中,类加载器会首先委派给其父类加载器来加载类,如果父类加载器无法加载,则会由子类加载器来加载。因此,如果一个类已经被父类加载器加载过了,那么子类加载器就无法再次加载该类,也就无法实现类的升级。

不过,JVM 提供了一种类加载器的机制,即热部署(HotSwap),可以在运行时动态地替换已经加载的类。热部署机制的实现依赖于 JVM 的调试接口(JVMTI),通过该接口可以在运行时修改已经加载的类的字节码,从而实现类的升级。

具体来说,热部署机制的实现步骤如下:

  1. 使用 JVMTI 接口获取需要替换的类的 Class 对象。

  2. 使用 JVMTI 接口获取需要替换的类的字节码。

  3. 修改字节码,生成新的字节码。

  4. 使用 JVMTI 接口将新的字节码加载到 JVM 中。

  5. 使用 JVMTI 接口通知 JVM 更新 Class 对象的字节码。

需要注意的是,热部署机制只适用于一些特定的场景,例如开发环境、测试环境等,不适用于生产环境。在生产环境中,应该使用版本控制等工具来管理类的升级。