概念

双亲委派模式是在 Java 1.2 后引入的,其工作原理是: 如果一个类加载器收到类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器, 则进一步向上委托,依次递归, 请求最终将到达顶层的启动类加载器, 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派。

双亲委派的好处

  • 每一个类都只会被加载一次,避免了重复加载
  • 每一个类都会被尽可能的加载(从引导类加载器往下,每个加载器都可能会根据优先次序尝试加载它)
  • 有效避免了某些恶意的加载(比如自定义了 Java.lang.Object 类,一般而言在双亲委派模型下会加载系统的 Object 类而不是自定义的Object类)

如何破坏双亲委派模型

  1. JDK 1.2 之前,那时候还没有双亲委派模型,不过已经有了 ClassLoader 这个抽象类, 所以已经有人集成这个抽象类,重写 LoadClass 方法来实现用户自定义类加载器。而在JDK 1.2 的时候要引入双亲委派模型,为了向前兼容,loadClass 这个方法还得保留着使之得以重写,新搞了个findClass 方法让用户去重写,并呼吁大家不要重写 loadClass 只要重写 findClass 。 这就是第一次对双亲委派模型的破坏,因为双亲委派的逻辑在loadClass 上,但是又允许重写 loadClass, 重写了之后就可以破坏委派逻辑了。
  2. 双亲委派第二次“被破坏” 是 ServiceLoader 和 Thread.setContextClassLoader() 。 即线程上下文类加载(contextClassLoader)。双亲委派模型很好的解决了各个类加载器的基础类统一问题(越基础的类越由上层的加载器进行加载), 基础类之前所被称为“基础”, 是因为它们总是作为被调用的API。 但是,如果基础类又要调用用户的代码,那该怎么办呢?线程上下文加载器就出现了。
    1. SPI ,全称为 Service Provider Interface,是一种服务发现机制。 这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader() 方法进行设置, 如果创建线程时还为设置,他将从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器, JNDI 服务使用 这个线程上下文类加载器去加载所需要的SPI 代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但是也是无可奈何的事情。 Java中所有涉及 SPI 的加载动作基本上都采用这种方式,例如 JNDI ,JDBC等
    2. 线程上下文类加载默认情况下就是 AppClassLoader , 那为什么不直接通过 getSystemClassLoader() 获取类加载器来加载 classpath 路径下的类呢? 其实是可行的,但这种直接使用 getSystemClassLoader() 方法获取 AppClassLoader 加载类有一个缺点,那就是代码部署到不同服务器时会出现问题,如把代码部署到 Java Web 应用服务器或者 EJB 之类的服务将会出现问题,因为这些服务使用的线程上下文类加载器并非 AppClassLoader ,而是 Java Web 应用服务自家的类加载器, 类加载器不同。所以我们应用该少用 getSystemClassLoader() 。总之不同的服务使用的可能默认 ClassLoader 是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的 CLassLoader , 从而避免不必要的问题。
  3. 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门” 的名词: 代码热替换, 模块热部署 , 模块热部署 等。简单的说就是机器不同重启,只要部署上就能用
  4. 在JDK 9 引入模块系统之后,类加载器的实现其实做了一波更新。当收到类加载请求,回先判断该类在具名模块中是否有定义,如果有定义就自己加载了,没的话在委派给父类。

破坏双亲委派模型的案例

  • JDBC
  • Tomcat