双亲委派模型(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

在虚拟机内部,加载器之间存在一定的关系如下:

双亲委派模型(Parents Delegation Model)(JDK 8)_第1张图片

而这种层级关系,是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<Launcher.ExtClassLoader>() {
            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<Launcher.AppClassLoader>() {
        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){
                    throw  new 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,jvm)