双亲委派机制(奥利给干就完了)

98 阅读9分钟

本文大量挪用我竟然被” 双亲委派” 给虐了!-HollisChuang's Blog
如果可以还请看原文,原文写的非常好!!!强烈建议观看原文并点赞
并且全网写得比较全的,我找了好几篇,基本都是抄的这个文章!!!

定义:

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

什么情况下父加载器会无法加载某一个类呢?

  1. Bootstrap ClassLoader(启动类加载器) :主要负责加载 Java核心类库,%JRE_HOME%\lib下的rt.jarresources.jarcharsets.jarclass等。
  2. Extension ClassLoader(扩展类加载器):主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
  3. Application ClassLoader(应用程序类加载器) :主要负责加载当前应用的 classpath 下的所有类
  4. User ClassLoader(用户自定义类加载器) : 用户自定义的类加载器,可加载指定路径的class文件

也就是说,一个用户自定义的类,是永远不会被 Bootstrap ClassLoader Extension ClassLoader 加载的

但是一个用户自定义的类,他也会被一直委托到 Bootstrap ClassLoader,但是因为 Bootstrap ClassLoader 不负责加载该类,那么会再由 Extension ClassLoader 尝试加载,而 Extension ClassLoader 也不负责这个类的加载,最终才会被 Application ClassLoader 加载。

为什么需要双亲委派?

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

另外,通过双亲委派的方式,还保证了安全性。因为 Bootstrap ClassLoader 在加载的时候,只会加载 JAVA_HOME 中的 jar 包里面的类,如 java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的 JDK,而就算他破坏你的JDK,因为你已经用Bootstrap ClassLoader加载了类,所以并不影响。

“父子加载器” 之间的关系是继承吗?

很多人看到父加载器、子加载器这样的名字,就会认为 Java 中的类加载器之间存在着继承关系。

甚至网上很多文章也会有类似的错误观点。

这里需要明确一下,双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码的。

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

public abstract class ClassLoader {

    // The parent class loader for delegation
    private final ClassLoader parent;
}

双亲委派是怎么实现的?

双亲委派模型对于保证 Java 程序的稳定运作很重要,但它的实现并不复杂。

实现双亲委派的代码都集中在 java.lang.ClassLoader 的 loadClass() 方法之中:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
    
        // 第一步,先检查类是否已经被加载过 
        Class<?> c = findLoadedClass(name);
        
        
        if (c == null) {
        
            // 第二步,若没有加载则调用父加载器的 loadClass() 方法进行加载
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
               } else {
               
                   // 第三步,若父加载器为空则默认使用 Bootstrap ClassLoader 作为父加载器 
                   c = findBootstrapClassOrNull(name);
               }
           } catch (ClassNotFoundException e) {
               // 第四步,如果父类加载失败,抛出 ClassNotFoundException 异常
               // ClassNotFoundException thrown if class not found
               // from the non-null parent class loader
           }
           
           if (c == null) {
               // 第五步,如果前面都失败,再调用自己的 findclass() 
               long t1 = System.nanoTime();
               c = findClass(name);
               // this is the defining class loader; record the stats
               PerfCounter.getParentDelegationTime().addTime(t1 - t0);
               PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
               PerfCounter.getFindClasses().increment();
           }
       }
       if (resolve) {
           resolveClass(c);
       }
       return c;
   }
}

代码不难理解,主要就是以下几个步骤:

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

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

知道了双亲委派模型的实现,那么想要破坏双亲委派机制就很简单了。

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

loadClass()、findClass()、defineClass()区别

ClassLoader 中和类加载有关的方法有很多,前面提到了 loadClass,除此之外,还有 findClass 和 defineClass 等,那么这几个方法有什么区别呢?

loadClass()
就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。

findClass()
根据名称或位置加载 .class 字节码

definclass()
.class 字节码转化为 Class

这里面需要展开讲一下 loadClassfindClass,我们前面说过,当我们想要自定义一个类加载器的时候,并且像破坏双亲委派原则时,我们会重写 loadClass 方法。

那么,如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?

这时候,就可以继承 ClassLoader,并且重写 findClass 方法。findClass() 方法是 JDK1.2 之后的 ClassLoader 新添加的一个方法。

这是显而易见的,ClassLoader是一个抽象类,并且 findClass(String name) 是一个没有具体实现的方法,并且在上面我们看到了loadClass()里面调用了findClass()

image.png

如图:这个方法只抛出了一个异常,没有默认实现。

JDK1.2 之后已不再提倡用户直接覆盖 loadClass() 方法,而是建议把自己的类加载逻辑实现到 findClass() 方法中。

因为在 loadClass() 方法的逻辑里,如果父类加载器加载失败,则会调用自己的 findClass() 方法来完成加载。

所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承 ClassLoader,并且在 findClass() 中实现你自己的加载逻辑即可。

双亲委派被破坏的例子

双亲委派机制的破坏不是什么稀奇的事情,很多框架、容器等都会破坏这种机制来实现某些功能。

  1. 第一种被破坏的情况是在双亲委派出现之前。
    由于双亲委派模型是在 JDK1.2 之后才被引入的,而在这之前已经有用户自定义类加载器在用了。所以,这些是没有遵守双亲委派原则的。

  2. 第二种,是 JNDI、JDBC 等需要加载 SPI 接口实现类的情况。

  3. 第三种是为了实现热插拔热部署工具。
    为了让代码动态生效而无需重启,实现方式时把模块连同类加载器一起换掉就实现了代码的热替换。

  4. tomcat 等 web 容器的出现。

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

为什么 JNDI,JDBC 等需要破坏双亲委派?

我们日常开发中,大多数时候会通过 API 的方式调用 Java 提供的那些基础类,这些基础类时被 Bootstrap 加载的。

但是,调用方式除了 API 之外,还有一种 SPI 的方式。

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

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

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

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

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

这段代码,会尝试加载 classpath 下面的所有实现了 Driver 接口的实现类。

那么,问题就来了。

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

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

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

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

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

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

说实话,我没怎么深入了解过JDBC,这一段没看太懂

为什么 Tomcat 要破坏双亲委派

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

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

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

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

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

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

模块化技术与类加载机制

近几年模块化技术已经很成熟了,在 JDK 9 中已经应用了模块化的技术。

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

image.png

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

总结

... 没有总结