首页 > 试题广场 >

说一说你对双亲委派模型的理解

[问答题]

说一说你对双亲委派模型的理解

推荐

得分点

​ 三层类加载器、双亲委派流程

参考答案

标准回答

​ 双亲委派模型依赖于三层类加载器:

  1. 启动类加载器:这个类加载器负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库加载到虚拟机的内存中。
  2. 扩展类加载器:这个类加载器负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
  3. 应用程序类加载器:这个类加载器负责加载用户类路径(classpath)上所有的类库。

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

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

加分回答

​ 双亲委派模型主要有3种“被破坏”的情况:

  1. 双亲委派模型的第一次“被破坏”发生在双亲委派模型出现之前

    双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协。为了兼容这些已有代码,只能在之后的ClassLoader中添加一个protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。

  2. 双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的

    一个典型的例子便是JNDI服务,JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?

    为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器。这个类加载器可以通过Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

    有了线程上下文类加载器,JNDI服务就可以使用它去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。

  3. 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的

    这里所说的动态性指的是一些非常热门的名词:代码热替换、模块热部署等。说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用。

    在这个领域,IBM公司主导的OSGi 提案是业界事实上的Java模块化标准。OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。

延伸阅读

​ 双亲委派模型的类加载器示意图:


图片说明

​ 双亲委派模型实现的核心源码:

// ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 首先检查这个类是否已经被加载过了
        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
            }

            if (c == null) {
                // 若父类加载器加载失败则调用findClass方法的来完成加载
                long t1 = System.nanoTime();
                c = findClass(name);
                ...
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
编辑于 2021-09-15 10:43:14 回复(0)

上图是类加载器的层次关系,称为类加载器的双亲委派模型。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。

一言概之,双亲委派模型,其实就是一种类加载器的层次关系
1. 工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。

因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

在分析类加载的源码时,我们还会再一次细致的提及类加载的过程。

2. 好处
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。 例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
避免了多份同样字节码的加载,内存是宝贵的,没必要保存相同的两份 Class 对象,例如 System.out.println() ,实际我们需要一个 System 的 Class 对象,并且只需要一份,如果不使用委托机制,而是自己加载自己的,那么类 A 打印的时候就会加载一份 System 字节码,类 B 打印的时候又会加载一份 System 字节码。而使用委托机制就可以有效的避免这个问题。


发表于 2021-11-18 00:09:51 回复(0)
双亲委派机制是指类加载的时候不是自己直接加载,而是把请求提交给父类加载器,父类加载器如果无法完成加载,子类加载器才会尝试自己加载。
确保类的准确性和一致性,避免类的重复加载。
还保证核心类库的安全性,不被篡改。
发表于 2024-11-15 12:45:41 回复(0)
双亲委派机制是指当一个类要加载时首先要把请求交给父类加载器加载,如果父类加载器不能找到该类信息再向下加载这样可以防止一些string或者工具类被篡改功能。
发表于 2023-10-04 12:56:14 回复(0)