JVM|双亲委派模型(Parents Delegation Model)(JDK 8)

类加载器的双亲委派模型在 JDK 1.2 时期被引入,并被广泛应用于此后几乎所有的 Java 程序中,但它并不是一个具有强制性的约束力的模型,而是 Java 设计者们推荐给开发者的一种加载器实现的最佳实践。
一、类加载器
Java 虚拟机设计团队有意把类加载阶段中的 “ 通过一个类的全限定名来获取描述该类的二进制字节流 ” 这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为 “ 类加载器 ”(Class Loader)。
判断两个类的是否 “ 相等 ”,是由类和加载类的类加载器共同决定的。
这里包括 Class 对象的 equal(),isAssignableFrom(),isInstance()和 instanceof 关键字的判断。
二、双亲委派模型 2.1、双亲委派模型的建立 从虚拟机的角度,只有两种类加载器,一种启动类加载器(Bootstrap ClassLoader),使用 C++实现,是虚拟机的一部分,另一种是继承自抽象类 java.lang.ClassLoader的启动器。
但是在虚拟机实际中,提供了三种类加载器:
  • 启动类加载器(Bootstrap Class Loader):主要负责加载存放在\lib目录下,或者被-Xbootclasspath参数指定的路径中存放的,Java虚拟机能识别的类名(如rt.jar、tools.jar等),的类。(加载请求委派给引导类加载器时,使用null的。)
  • 扩展类加载器(Extension Class Loader):主要负责加载\lib\ext目录下,或者被java.ext.dirs系统变量所指定的路径中的所有类。sun.misc.Launcher.ExtClassLoader
  • 应用程序类加载器(Application Class Loader):复杂加载用户类路径(Classpath)上所有的类库,开发者可以直接在代码中使用这个类加载器(java.lang.ClassLoader#getSystemClassLoader)。一般情况下系统默认的类加载器,也称 “ 系统类加载器 ”。sun.misc.Launcher.AppClassLoader
在虚拟机内部,加载器之间存在一定的关系如下:
JVM|双亲委派模型(Parents Delegation Model)(JDK 8)
文章图片

而这种层级关系,是JDK 1.2以来,虚拟机设计者为开发者提供的类加载器最佳实践模型。这种类似父子关系的层级模型称为 “ 双亲委派模型 (Parents Delegation Model)”。这种父子关系并不是依赖于继承,而是使用的组合。
双亲委派模型的工作过程如下:
  1. 当前类加载器首先从自己已经加载的类(缓存)中,查询是否此类已经加载,如果已经加载,则直接返回原来已经加载的类。
  2. 如果在当前类加载器的缓存中,没有找到期待被加载的类时,则委托父类加载器去加载。父类加载器采用同样的策略,首先查看自己的缓存,(如果仍然没有)则继续委托其父类加载去加载,一直到 BootStrapClassLoader(启动类加载器)。
  3. 当所有的父类加载器都没有加载此类时,才由当前的类加载器加载,并将其放入自己的缓存中,以便下次有加载请求时直接返回。
java.lang.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 { //没有父类加载器时,使用启动类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader }if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); // 自己来加载类 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; } }

虚拟机中三大类加载器建立双亲委派模型的过程源码:
sun.misc.Launcher
public Launcher() { Launcher.ExtClassLoader var1; try { //初始化【扩展类加载器】,没有入参,使用的是【启动类加载器】 var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); }try { // 使用【扩展类加载器】初始化【应用程序类加载器】 this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } // 使用【应用程序类加载器】初始化【线程上下文类加载器】 Thread.currentThread().setContextClassLoader(this.loader); //这是部分代码 }

扩展类加载器创建过程:
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException { final File[] var0 = getExtDirs(); try { return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction() { public Launcher.ExtClassLoader run() throws IOException { int var1 = var0.length; for(int var2 = 0; var2 < var1; ++var2) { MetaIndex.registerDirectory(var0[var2]); }return new Launcher.ExtClassLoader(var0); } }); } catch (PrivilegedActionException var2) { throw (IOException)var2.getException(); } } public ExtClassLoader(File[] var1) throws IOException { // classLoader 为null 就是默认使用启动类加载器 super(getExtURLs(var1), (ClassLoader)null, Launcher.factory); SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this); }

应用程序类加载:
// 这里创建时,传入的是扩展类加载器 public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { final String var1 = System.getProperty("java.class.path"); final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1); return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() { public Launcher.AppClassLoader run() { URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2); return new Launcher.AppClassLoader(var1x, var0); } }); }AppClassLoader(URL[] var1, ClassLoader var2) { //使用扩展类加载器作为父加载器创建 super(var1, var2, Launcher.factory); this.ucp.initLookupCache(this); }

2.2、双亲委派模式优势 采用双亲委派模式的是好处是:
  • Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载
  • 其次是考虑到安全因素,java核心api中定义类型不会被随意替换
假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的 Integer.class ,这样便可以防止核心 API 库被随意篡改。可能你会想,如果我们在 classpath 路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心 API 包,需要访问权限,强制加载将会报出如下异常
java.lang.SecurityException: Prohibited package name: java.lang

2.3、自定义类加载器 通过继承java.lang.ClassLoader抽象类来实现。主要介绍一下核心方法
  • loadClass(java.lang.String)
该方法加载指定名称(包括包名)的二进制类型。JDK1.2 以后不要建议重写。负责构建双亲委派模型。
public Class loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } 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 { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader }if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); 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; } }

  • protected Class findClass(String name)
但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。
protected Class findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }

  • protected final Class defineClass(String name, byte[] b, int off, int len)
defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。
举个例子
package com.hyl.learnerJVM.load; import java.io.IOException; import java.io.InputStream; /** * 类加载器与 instanceof 关键词演示 * * @author hyl * @version v1.0: ClassLoaderTest.java, v 0.1 2020/8/13 13:30 $ */ public class ClassLoaderTest {public static void main(String[] args) throws Exception { ClassLoader myLoader = new ClassLoader() { @Override public Class loadClass(String name) throws ClassNotFoundException {try {String fileName = name.substring(name.lastIndexOf(".")+1)+".class"; InputStream is = getClass().getResourceAsStream(fileName); if (is == null){ return super.loadClass(name); } byte[] b =new byte[is.available()]; is.read(b); return defineClass(name,b,0,b.length); }catch (IOException e){ thrownew ClassNotFoundException(name); } } }; Object obj = myLoader.loadClass("com.hyl.learnerJVM.load.ClassLoaderTest").newInstance(); System.out.println( obj.getClass()); System.out.println( obj instanceof com.hyl.learnerJVM.load.ClassLoaderTest); ClassLoader myLoader2 = ClassLoader.getSystemClassLoader(); Object obj2 = myLoader2.loadClass("com.hyl.learnerJVM.load.ClassLoaderTest").newInstance(); System.out.println( obj2.getClass()); System.out.println( obj2 instanceof com.hyl.learnerJVM.load.ClassLoaderTest); Object obj3 =ClassLoaderTest.class.getClassLoader().loadClass("com.hyl.learnerJVM.load.ClassLoaderTest").newInstance(); System.out.println( obj3.getClass()); System.out.println( obj3 instanceof com.hyl.learnerJVM.load.ClassLoaderTest); } }

三、破坏双亲委派模型 Java世界里破坏双亲委派模型的三次情况:
3.1、JDK 1.2 之前 java.lang.ClassLoader已经被引用,但是主要是通过重写 loadClass()方法,这里还没有引入双亲委派模型。JDK1.2 以后 双亲委派模型 的逻辑写在了 loadClass()方法内,在自定义加载类时,建议使用findClass()方法。
3.2、SPI(Service Provider Interface ) 为了能加载其他厂商实现并部署在应用程序的 ClassPath 下的 SPI。这些 SPI 是由启动类加载器加载,但是启动类加载器无法加载其他厂商代码,所以引入了线程上下文类加载器(Thread Context ClassLoader)。使用线程上下文加载器去加载 SPI 服务代码,是一种父类加载器去请求子类加载器完成类加载的行为,就违背了双亲委派模型的一般性原则,但是也无可奈何。例如:JDNI、JDBC、JCE、JAXB和JBI都是使用这种方式。
在 JDK 6时,JDK 提供了 java.util.ServiceLoader类,以 META-INF/services 中的配置信息,辅以责任链模式,这才给 SPI 的加载提供了一种相对合理的解决方案。
3.3、用户“动态性”概念 代码热替换(Hot Sweep)、模块热部署(Hot Deployment)等。
OSGI
OSGI 实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块( OSGI 中称为 Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。
OSGI 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为复杂的网状结构,当收到类的加载请求时,OSGI 将按照下面的顺序经行类搜索:
  1. 将以 java.*开头的类,委派给父类加载器加载。
  2. 否则,将委派列表单内的类,委派给父类加载器加载。
  3. 否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载。
  4. 否则,查找当前 BundleClassPath,使用自己的类加载器加载。
  5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载。
  6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应的 Bundle 的类加载器加载。
  7. 否则,类查找失败。
1 和 2 还是遵从双亲委派模型,后面就没有了。
JDK9 ,开始了Java模块化系统(Java Platform Module System,JPMS)算是第四次破坏双亲委派模型,在加载时要先判断能够归属到某一个系统模块中,如果存在这种归属关系,就优先委派给负责那个模块的加载器完成加载。
破坏双亲委派模型不一定是贬义词,只要有明确的目的和充分的理由,突破旧有原则无疑也是一种创新。
参考
  • 《深入理解Java虚拟机》第三版,周志明著。
  • https://www.dazhuanlan.com/2019/11/17/5dd0248ad6d20/
  • https://zhuanlan.zhihu.com/p/73359363
  • https://docs.oracle.com/javase/tutorial/ext/basics/load.html
  • https://blog.csdn.net/javazejian/article/details/73413292
  • 【JVM|双亲委派模型(Parents Delegation Model)(JDK 8)】真正理解线程上下文类加载器(多案例分析)

    推荐阅读