一、什么是双亲委派机制

Java语言系统支持以下4种类加载器:

  • Bootstrap ClassLoader 启动类加载器
  • Extention ClassLoader 标准扩展类加载器
  • Application ClassLoader 应用类加载器
  • User ClassLoader 用户自定义类加载器

层次关系

双亲委派_java

定义 :

当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。

各种加载器的职责:

  • Bootstrap ClassLoader ,主要负责加载 Java 核心类库,% JRE_HOME%\lib 下的 rt.jar、resources.jar、charsets.jar 和 class 等。
  • Extention ClassLoader,主要负责加载目录 % JRE_HOME%\lib\ext 目录下的 jar 包和 class 文件。
  • Application ClassLoader ,主要负责加载当前应用的 classpath 下的所有类
  • User ClassLoader , 用户自定义的类加载器,可加载指定路径的 class 文件当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。

二、为什么需要双亲委派

1.通过委派的方式,可以避免类的重复加载
当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。

2.通过双亲委派的方式,还保证了安全性
因为 Bootstrap ClassLoader 在加载的时候,只会加载 JAVA_HOME 中的 jar 包里面的类,如 java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的 JDK。这样,就可以避免有人自定义一个有破坏功能的 java.lang.Integer 被加载。这样可以有效的防止核心 Java API 被篡改。


三、父子加载器之间是继承的关系么?

非继承关系。是组合关系。

如下为 ClassLoader 中父加载器的定义:

点击查看代码
public abstract class ClassLoader {
 
        // The parent class loader for delegation
 
        private final ClassLoader parent;
 
    }

四、双亲委派是怎么实现的?

代码:

点击查看代码
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    //1 首先检查类是否被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
             //2 没有则调用父类加载器的loadClass()方法;
                c = parent.loadClass(name, false);
            } else {
            //3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
           //4 若父类加载失败,抛出ClassNotFoundException 异常后
            c = findClass(name);
        }
    }
    if (resolve) {
        //5 再调用自己的findClass() 方法。
        resolveClass(c);
    }
    return c;
}

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

五、如何主动破坏双亲委派机制

想要破坏这种机制,那么就自定义一个类加载器,重写其中的 loadClass 方法,使其不进行双亲委派即可。


六、loadClass(), findClass(), defineClass()的区别

  • loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
  • findClass() 根据名称或位置加载.class 字节码
  • definclass() 把字节码转化为 Class

这里重点区分一下loadClass()和findClass()

(1).如果你想破坏双亲委派,那就重写loadClass()方法。
(2).如果你想自定义类加载器,但是又不想破坏双亲委派,那就继承classLoader,并重写findClass()方法


七、双亲委派被破坏的例子

1,是 JNDI、JDBC 等需要加载 SPI 接口实现类的情况。

2. tomcat 等 web 容器的出现。

3. OSGI、Jigsaw 等模块化技术的应用。


八、JNDI和JDBC为什么要破坏双亲委派

调用方式除了API之外,还有SPI的方式

如典型的 JDBC 服务,我们通常通过以下方式创建数据库连接:

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");

在以上代码执行之前,DriverManager 会先被类加载器加载,因为 java.sql.DriverManager 类是位于 rt.jar 下面的 ,所以他会被根加载器加载。

类加载时,会执行该类的静态方法。其中有一段关键的代码是:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
这段代码,会尝试加载 classpath 下面的所有实现了 Driver 接口的实现类。

那么,问题就来了。

DriverManager 是被根加载器加载的,那么在加载时遇到以上代码,会尝试加载所有 Driver 的实现类,但是这些实现类基本都是第三方提供的,根据双亲委派原则,第三方的类不能被根加载器加载。

那么,怎么解决这个问题呢?

于是,就在 JDBC 中通过引入 ThreadContextClassLoader(线程上下文加载器,默认情况下是 AppClassLoader)的方式破坏了双亲委派原则。

我们深入到 ServiceLoader.load 方法就可以看到:

点击查看代码
public static <S> ServiceLoader<S> load(Class<S> service) {

        ClassLoader cl = Thread.currentThread().getContextClassLoader();

        return ServiceLoader.load(service, cl);

    }

第一行,获取当前线程的线程上下⽂类加载器 AppClassLoader,⽤于加载 classpath 中的具体实现类。

九、为什么tomcat要破坏双亲委派

我们知道,Tomcat 是 web 容器,那么一个 web 容器可能需要部署多个应用程序。

不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。

如多个应用都要依赖 hollis.jar,但是 A 应用需要依赖 1.0.0 版本,但是 B 应用需要依赖 1.0.1 版本。这两个版本中都有一个类是 com.hollis.Test.class。

如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。

所以,Tomcat 破坏双亲委派原则,提供隔离的机制,为每个 web 容器单独提供一个 WebAppClassLoader 加载器。

Tomcat 的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器 ——WebAppClassLoader 负责加载本身的目录下的 class 文件,加载不到时再交给 CommonClassLoader 加载,这和双亲委派刚好相反。

十、模块化技术与类加载机制

其实早在 JDK 9 之前,OSGI 这种框架已经是模块化的了,而 OSGI 之所以能够实现模块热插拔和模块内部可见性的精准控制都归结于其特殊的类加载机制,加载器之间的关系不再是双亲委派模型的树状结构,而是发展成复杂的网状结构。

在 JDK 中,双亲委派也不是绝对的了。

在 JDK9 之前,JVM 的基础类以前都是在 rt.jar 这个包里,这个包也是 JRE 运行的基石。

这不仅是违反了单一职责原则,同样程序在编译的时候会将很多无用的类也一并打包,造成臃肿。

在 JDK9 中,整个 JDK 都基于模块化进行构建,以前的 rt.jar, tool.jar 被拆分成数十个模块,编译的时候只编译实际用到的模块,同时各个类加载器各司其职,只加载自己负责的模块。

点击查看代码
Class<?> c = findLoadedClass(cn);

    if (c == null) {

        // 找到当前类属于哪个模块

        LoadedModule loadedModule = findLoadedModule(cn);

        if (loadedModule != null) {

            //获取当前模块的类加载器

            BuiltinClassLoader loader = loadedModule.loader();

            //进行类加载

            c = findClassInModuleOrNull(loadedModule, cn);

         } else {

              // 找不到模块信息才会进行双亲委派

                if (parent != null) {

                  c = parent.loadClassOrNull(cn);

                }

          }
    }