【Java】JVM-双亲委派模型(Parents Delegation Model)

Posted by 西维蜀黍 on 2019-03-07, Last Modified on 2021-09-21

双亲委派模型(Parents Delegation Model)

**双亲委派模型(Parents Delegation model)**要求:除了顶层的启动类加载器(BootStrap ClassLoader)外,其余的类加载器都应当有自己的父类加载器。

注意,这里类加载器之间的父子关系不是以继承(inheritance)的关系来实现的,而是当加载一个 Class 类时,不同的类加载器在执行这个加载任务时,拥有不同的优先级(priority),其中父类加载器的优先级更高,因而父类加载器总是优先执行加载任务。

另外,值得一提的是,双亲委派的原文是 “parents delegate”。parents 常翻译为“父母”,但其实也有表达“上溯,母体,祖先”的意思。因此,在双亲委派模型(Parents Delegation model)中, “parents delegate” 是指子类加载器总是先委派任务给自己的父类加载器(去执行类加载任务),而不是自己直接执行。因此,不是指子类加载器有两个父类加载器

类加载器(Class Loader)

在 Java 中,默认提供的三种类加载器,分别是 BootStrapClassLoader(启动类加载器)、ExtClassLoader(扩展类加载器)、AppClassLoader(应用程序类加载器)

  • BootStrapClassLoader(启动类加载器):它由 C++ 实现,在 Java 程序中不能显式地获取到。它负责加载存储在 %JAVA_HOME%/jre/lib%JAVA_HOME%/jre/classes 以及 -Xbootclasspath 参数指定的路径中的类。
  • ExtClassLoader(扩展类加载器):它是由sun.misc.Launcher$ExtClassLoader 实现,负责加载 %JAVA_HOME%/jre/lib/ext 路径以及java.ext.dirs 系统变量指定的路径中的类库。
  • AppClassLoader(应用程序类加载器):由sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(classpath)中的指定类,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器(CustomClassLoader)默认就是用这个加载器。

同时,我们也可以定义自己的类加载器 (Custom ClassLoader),那么它的 parent 肯定就是 AppClassLoader(应用程序类加载器)了。类加载器的这种层次关系称为双亲委派模型(Parents Delegation Model)


如果站在 JVM 的角度来看,只存在两种类加载器:

  • 启动类加载器(Bootstrap ClassLoader):由 C++ 实现,它负责加载存储在 %JAVA_HOME%/jre/lib%JAVA_HOME%/jre/classes 以及 -Xbootclasspath 参数指定的路径中的类。
  • 其他类加载器:由 Java 实现,继承自抽象类 ClassLoader ,具体包括:
    • 扩展类加载器(Extension ClassLoader):负责加载 %JAVA_HOME%/jre/lib/ext 路径以及java.ext.dirs 系统变量指定的路径中的类库。
    • 应用程序类加载器(Application ClassLoader):负责加载用户类路径(classpath)中的指定类,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器(CustomClassLoader)默认就是用这个加载器。
    • 开发者定义的类加载器 (CustomClassLoader)

双亲委派模型的工作过程

  1. 当前类加载器首先从自己已经加载的类(缓存)中,查询是否此类已经加载,如果已经加载,则直接返回原来已经加载的类。
  2. 如果在当前类加载器的缓存中,没有找到期待被加载的类时,则委托父类加载器去加载。父类加载器采用同样的策略,首先查看自己的缓存,(如果仍然没有)则继续委托其父类加载去加载,一直到 BootStrapClassLoader(启动类加载器)。
  3. 当所有的父类加载器都没有加载此类时,才由当前的类加载器加载,并将其放入自己的缓存中,以便下次有加载请求时直接返回。

ClassLoader 的源码:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

分析

即当前类加载器先检查,该类是否已经被加载过。若没有,则调用父类加载器的 loadClass() 方法,依次向上递归。

若父类加载器为空,则说明递归到启动类加载器了。如果从父类加载器到启动类加载器的上层次的所有加载器都加载失败,则调用自己的 findClass() 方法进行加载。

双亲委派模式优势

避免类的重复加载

采用双亲委派模式的好处是,Java 类随着它的类加载器一起,具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载

当父亲已经加载了该类时,就没有必要子类加载器再加载一次。

安全因素

其次是考虑到安全因素,Java 核心 API 中定义类不能被修改。

假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的 Integer.class,这样便可以防止核心 API 库被随意篡改。

可能你会想,如果我们在自定义一个类加载器,并且通过我们自己定义的类加载器来加载我们的 java.lang.Integer 类会怎么样呢?

在这种情况下,看起来我们定义的 java.lang.Integer 类会被加载到 JVM 中,而事实上,这样的做法会触发 JVM 的保护机制,因为我们定义的类所在的包(java.lang.Integer)已经被核心 Java API 使用而且进行保护。

最终运行程序会提示 Exception in thread “main” java.lang.SecurityException: Prohibited package name: java.lang.lang

破坏双亲委派模型

背景

双亲委托模型并不是一个强制性的约束,而是 Java 设计者推荐给开发者的类加载器实现方式。

在 Java 的世界中大部分的类加载器都遵循这个模型,但也有例外,到目前为止,双亲委派模型主要出现过3个较大规模的“被破坏”的情况。

为什么需要破坏双亲委派?

而且,在某些情况下,由于受到加载范围的限制,父类加载器无法加载到需要的文件,因而父类加载器需要委托子类加载器去加载类文件。

以 Driver 接口为例,由于 Driver 接口定义在 JDK 当中的,而其实现由各个数据库的服务商来提供,比如 MySQL 就写了 MySQL Connector

那么问题就来了,DriverManager(也由 JDK提供)要加载各个实现了 Driver 接口的实现类。而 DriverManager 由启动类加载器(Bootstrap ClassLoader)进行加载,而其具体实现是由服务商提供的,由应用程序类加载器(Application ClassLoader)加载。

这个时候就需要启动类加载器来委托子类来加载 Driver 的各种实现类,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。

总结

双亲委派模型的实现依赖于loadClass方法,因此

  • 如果不想不破坏双亲委派模型,只要去重写 findClass 方法
  • 如果想要去破坏双亲委派模型,需要去重写 loadClass 方法

细节可参考 https://blog.csdn.net/Ditto_zhou/article/details/79972240

Reference