什么是双亲委派机制

jvm对class文件采用的是按需加载的方式,当需要该类时才会将它的class文件加载到内存中。
加载时采用的是双亲委派机制,即将请求交给父类处理的任务委派模式。
双亲委派机制的原理图如下:
双亲委派机制

双亲委派机制的原理

  1. 类加载器收到类加载请求,先检查是否有父类加载器,若有,则将请求委托给父类加载器加载。
  2. 如果父类还有父类,则继续向上委托,直至 引导/启动类加载器 Bootstrap ClassLoader
  3. 如果父加载器可以完成加载,则返回成功结果。否则,由子类自行加载。如果子类加载失败则会抛出ClassNotFoundException
    加过过程图解如下:
    双亲委派过程
    对应源码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public abstract class 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 {
                           //最顶级父类bootstrapClassLoader加载
                           c = findBootstrapClassOrNull(name);
                      }
                  } catch (ClassNotFoundException e) {
                       // ClassNotFoundException thrown if class not found
                       // from the non-null parent class loader
                  }
                   //父类中既没有缓存,也无法加载,则当前classloader尝试加载
                   if (c == null) {
                       // If still not found, then invoke findClass in order
                       // to find the class.
                       long t1 = System.nanoTime();
                       //自定义classLoader只能重写findClass方法
                       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、defineClass方法的区别

  • loadClass: 内置双亲委派的实现机制
  • findClass:根据名称或者路径加载.class文件
  • defineClass:把.class字节码转化为Class对象

双亲委派机制的优势?

  1. 避免类的重复加载:通过双亲委派机制可以避免类的重复加载,当父类已经加载过某一个类时,子类加载器就不会再次加载该类。
  2. 保护程序安全,防止核心API被随意篡改: 引导类加载器加载时,只加载JAVA_HOME中的jar包,例如java.lang.String

双亲委派机制的缺点?

子类可以使用父类加载的类,但是父类无法使用自类加载器加载过的类。
JAVA中有许多SPI接口,允许第三方提供实现。比如数据库实现JDBC。这些接口由java核心类库提供,交由第三方实现。如果沿用双亲委派机制,就会存在提供者由bootstrap ClassLoader加载,而实现由第三方自定义类加载器加载,此时,顶层类加载器就无法使用子类加载器加载的类。 此时就需要打破双亲委派机制。

什么是反向委派机制

java中存在很多Service Provider Interface,SPI,这些借口允许第三方提供实现。比如jdbc、jndi等这些SPI属于java核心接口,一般存在于rt.jar中,由BootStrapClassloader加载。而BootStrapClassloader无法直接加载SPI的实现类。同时由于双亲委派机制的存在,BootStrapClassloader也无法反向委托应用类加载器加载SPI的实现类。
那该如何加载SPI的实现类呢?
通过委派线程上下文类加载器jdbc.jar中的实现类加载到内存中以供使用。这个过程中摒弃了原双亲委派机制,程序可以逆向的加载类,故而称之为反向委派

反向委派机制流程

打破双亲委派机制

打破双亲委派机制有如下两种方式:

  1. 重写loadClass方法
  2. 使用线程上下文类加载器
重写loadClass方法

双亲委派的实现在loadClass中,自定义类加载器,重写加载逻辑避免继续执行原生类加载过程即可。

使用 线程上下文类加载器

利用线程上下文加载器(Thread Context ClassLoader)也可以打破双亲委派。
Java 应用上下文加载器默认是使用 AppClassLoader。若想要在父类加载器使用到子类加载器加载的类,可以使用 Thread.currentThread().getContextClassLoader()
线程上下文类加载器

反向双亲委派机制的使用场景

1. 解决依赖冲突

详见 https://juejin.cn/post/6931972267609948167

2. tomcat打破双亲委派机制

打破双亲委派机制的场景很多: JDBC、JNDI、Tomcat等,我们以Tomcat为例来说明
tomact是一个web容器,主要解决以下问题:

  1. 一个web容器可能要部署两个或多个应用程序,不同的应用程序之间可能会依赖同一个第三方类库的不同版本,因此要保障每个应用程序的类库是独立的、相互隔离的。
  2. 不输在同一个web容器的相同的类库的相同版本可以共享,否则会有重复的类库被加载进jvm中。
  3. web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离。
  4. web容器支持jssp文件修改后不用重启,jsp文件也要编译为.class文件,支持hotswap功能。

而默认的类加载器无法加载两个相同类库的不同版本,因为加载时是以类的全限定名来确定的,无法处理上述的隔离问题。
同时在修改jsp文件后,因为类名一致,默认的类加载器不会重新加载,而是使用方法区中已经存在的类,所以每一个jsp都需要对应一个唯一的类加载器,当修改jsp文件时,需要卸载此唯一的类加载器,然后重新创建类加载器,并加载修改后的jsp文件。

tomcat的类加载器结构

tomcat类加载器结构

上图中自上而下有五个自定义类加载器:

  1. CommonClassLoader: tomcat中最基本的类加载器,加载路径中 class可以被tomcat和各个webapp访问
  2. CatalinaClassLoader: tomcat私有类加载器,对webapp不可见。webapp不能访问其加载路径下的class
  3. SharedClassLoader: 各个webapp共享的类加载器,但tomcat不可见
  4. webAppClassLoader: webapp私有的类加载器,仅对当前webapp可见
  5. JasperClassLoader:JSP的类加载器
    其中,每个web程序都有一个webappClassLoader,每个jsp文件都有一个JasperClassLoader,这两个类加载器有多个实例。
tomcat打破双亲委派机制的工作原理
  1. CommonClassLoader能加载的类都可以被CatalinaClassLoader使用,从而实现了公有类库的共用。
  2. CatalinaClassLoader 和 SharedClassLoader 加载的类相互隔离,以此保障了tomcat和web容器的隔离性。
  3. webAppClassLoader可以使用 SharedClassLoader 加载的类,但是多个 webAppClassLoader 加载的类 相互隔离。
  4. JasperClassLoader的加载范围仅限于这个JSP文件所编译出的.class文件。当web容器检测到JSP文件被修改时,会替换掉之前的JasperClassLoader实例,再创建一个Jsp类加载器来实现JSP文件的HotSwap功能
    tomcat的目录结构:
  • /common/*
  • /server/*
  • /shared/*
  • /WEB-INF/*

默认情况下,conf目录下的catalina.properties文件,没有指定server.loader以及shared.loader,所以tomcat没有建立CatalinaClassLoader和SharedClassLoader实例,这两个都会使用CommonClassLoader来代替。Tomcat6之后,把common、shared、server目录合成一个lib目录,所以我们服务器里看不到common、shared、server目录。

tomcat应用的默认加载顺序
  • 从JVM的 BootStrapClassLoader 中加载
  • 加载 web应用目录下 /WEB-INF/classes 中的类
  • 加载 web目录下 /WEB-INF/lib/*.jar 中的 jar包中的类
  • 委托给 system、common类加载器去加载
tomcat 类加载过程
  1. 在本地缓存中查找是否已经加载过该类(加载过会被缓存在resourceEntries中)
  2. 如果没有,尝试使用 引导类加载器 尝试加载该类
  3. 如果没有,使用当前类加载器加载 webAppClassLoader 加载
  4. 如果未加载,则遵循双亲委派机制 委派给 AppClassLoader此时加载顺序是 AppClassLoader、commonClassLoader、SharedClassLoader 进行加载

3. 热加载

4. 热部署

5. 加密保护

SPI

什么是SPI

服务提供接口(SPI,Service Provider Interface) 是 JDK 内置的一种「服务提供发现机制」,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件(可通过 SPI 机制实现模块化)。SPI 的整体机制图如下:
SPI机制

引用

1. Java类加载器 — classloader 的原理及应用
2. 线程上下文类加载器
3. 类加载器相关内容全解
4.