类与类加载器

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性,每一个类加载器都拥有一个独立的类名称空间。比较两个类相等的前提是这两个类是由同一个类加载器加载的。

这里所指的相等,包括代表类的 Class 对象的 equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用 instanceof 关键字做对象所属关系判定等各种情况。

双亲委派模型

在 Java 虚拟机的角度只存在两种类加载器:一种是启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分;另一种是其他所有类的类加载器,都由 Java 语言实现,全都继承自抽象类 java.lang.ClassLoader。

在 Java 开发成员的角度加载器分为四种

类加载器双亲委派模型

启动类加载器(Bootstrap Class Loader)

这个类加载器负责加载存放在 <JAVA_HOME>\lib 目录,或者被-Xbootclasspath 参数所指定的路径中存放的,而且是 Java 虚拟机能够识别的(按照文件名识别,如 rt.jar、tools.jar,名字不符合的类库即使放在 lib目录中也不会被加载)类库加载到虚拟机的内存中。

扩展类加载器(Extension Class Loader)

这个类加载器是在类 sun.misc.Launcher$ExtClassLoader 中以 Java 代码的形式实现的。由于是由 Java代码实现的,开发者可以直接在程序中使用扩展类加载器来加载 Class 文件。它负责加载负责加载 <JAVA_HOME>\lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中所有的类库,这是一种 Java 系统类库的扩展机制。

应用程序类加载器/系统类加载器(Application Class Loader)

这个类加载器由 sun.misc.Launcher$AppClassLoader 来实现,负责加载用户类路径(ClassPath)上所有的类库,开发者可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

自定义类加载器

可以加入自定义的类加载器来进行拓展。

双亲委派

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

工作流程

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

优点

Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object 它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都能够保证是同一个类。反之如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,系统中就会出现多个不同的 Object 类。

实现

protected synchronized Class<?> loadClass(Sreing name, boolean resolve) throws ClassNotFoundExcepthon{
    // 首先检查请求的类是否已经被加载过
    Class c = findLoasedClass(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 设计者推荐给开发者们的类加载器实现方式。

到 Java 8 为止出现过三次较大规模的破坏双亲委派模型的情况:

  1. 在双亲委派模型引入之前 ClassLoader 已经存在,引入双亲委派模型时为了兼容之前的代码,避免 loadClass()被子类覆盖,在 ClassLoader 中添加一个 protected 方法 findClass()。
  2. 模型自身的缺陷。双亲委派模型使得越基础的类由越上层的加载器加载,但可能有基础类型要调用回用户的代码,例如 JNDI 服务要调用其他厂商实现的接口,启动加载器不能加载这些代码,所以引入了线程上下文类加载器,通过父类加载器请求子类加载器的行为加载。
  3. 对程序动态性的追求,例如代码热替换、模块热部署等。这依赖于 Java 的模块化,OSGI(IBM 公司主导的 Java 模块化规范)实现模块化热部署的关键是自定义类加载器机制的实现,而这个设计不符合双亲委派的类加载器架构,在 OSGI 环境下类加载器发展为网状结构而不是双亲委派模型的树状结构。

Java 模块化系统

模块化的关键目标

可配置的封装隔离机制。

查找依赖

Java 9 之前基于类路径查找依赖,如果类路径中缺失了运行时依赖的类型,等程序运行到该类型的加载、链接时才会报出异常。

Java 9 之后启用模块化进行封装,模块可以声明对其他模块的显式依赖,Java 虚拟机能够在启动时验证应用程序开发阶段设定好的依赖关系,如有缺失直接启动失败。

模块的兼容性

为了使可配置的封装隔离机制兼容 Java 旧版本的类路径查找机制,Java 9 提出了模块路径。某个类库是模块还是传统的 JAR 包只取决于它存放的路径,具体规则如下:

  • JAR 文件在类路径:文件在类路径的访问规则:所有类路径下的 JAR文件及其他资源文件,都被视为自动打包在一个匿名模块,这个匿名模块可以看见和使用所有类路径和模块路径的导出包。
  • 模块在模块路径:模块路径下的具名模块只能访问它依赖定义中列明依赖的模块和包,看不到匿名模块。
  • JAR 文件在模块路径的:会变成一个自动模块,默认依赖于整个模块路径中的所有模块,可以访问到所有模块导出的包,自动模块也默认导出自己所有的包。

模块化下的类加载器

Java 9 以后的类加载器委托关系

模块下的类加载器的变动

扩展类加载器被平台类加载器取代,因为整个 Java 基于模块化进行构建已经满足可扩展的需求,不需要额外的扩展功能。

平台类加载器和应用程序类加载器都不再派生于 java.net.URLClassLoader,现在启动类加载器、平台类加载器和应用程序类加载器都继承于 jdk.internal.loader.BuiltinClassLoader,变化如下图:

类加载器集成架构变动

类加载的委派关系的变动:当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以就优先委派给负责那个模块的加载器完成加载。

最后修改:2023 年 06 月 05 日
如果觉得我的文章对你有用,请随意赞赏