千家信息网

JVM双亲委派模型及SPI实现原理是什么

发表于:2024-12-12 作者:千家信息网编辑
千家信息网最后更新 2024年12月12日,这篇文章主要讲解了"JVM双亲委派模型及SPI实现原理是什么",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"JVM双亲委派模型及SPI实现原理是什么"吧
千家信息网最后更新 2024年12月12日JVM双亲委派模型及SPI实现原理是什么

这篇文章主要讲解了"JVM双亲委派模型及SPI实现原理是什么",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"JVM双亲委派模型及SPI实现原理是什么"吧!

1、双亲委派模型

我们知道类加载机制是将⼀个类从字节码⽂件转化为虚拟机可以直接使⽤类的过程,但是是谁来执⾏这个过程中的加载过程,它⼜是如何完成或者说保障了类加载的准确性和安全性呢?

答案就是类加载器以及双亲委派机制。

双亲委派模型的⼯作机制是:当类加载器接收到类加载的请求时,它不会⾃⼰去尝试加载这个类,⽽是把这个请求委派给⽗加载器去完成,只有当⽗类加载器反馈⾃⼰⽆法完成这个加载请求时,⼦加载器才会尝试⾃⼰去加载类。

我们可以从 JDK 源码中将它的⼯作机制⼀窥究竟。

ClassLoader#loadClass(String, boolean)

这是在 jdk1.8 的 java.lang.ClassLoader 类中的源码,这个⽅法就是⽤于加载指定的类。

/** * @author 庭前云落 * @Date 2020/8/22 21:29 * @description */public class ClassLoader {    protected Class loadClass(String name, boolean resolve) throws            ClassNotFoundException {        synchronized (getClassLoadingLock(name)) {            // First, check if the class has already been loaded            // ⾸先,检查该类是否已经被当前类加载器加载,若当前类加载未加载过该类,调⽤⽗类的加载类⽅法去加载该类(如果⽗类为null的话交给启动类加载器加载)            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 foun                    // from the non-null parent class loader }                    if (c == null) {                        // If still not found, then invoke findClass in orde                        // 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;            }        }    }

看完了上⾯的代码,我们知道这就是双亲委派模型代码层⾯的解释:

  1. 当类加载器接收到类加载的请求时,⾸先检查该类是否已经被当前类加载器加载;

  2. 若该类未被加载过,当前类加载器会将加载请求委托给⽗类加载器去完成;

  3. 若当前类加载器的⽗类加载器(或⽗类的⽗类……向上递归)为 null,会委托启动类加载器完成加 载;

  4. 若⽗类加载器⽆法完成类的加载,当前类加载器才会去尝试加载该类。

2、类加载器的分类及各自的职责

在 JVM 中预定义的类加载器有3种: 启动类加载器 (Bootstrap ClassLoader)、扩展类加载器(ExtensionClassLoader)、 应⽤类/系统类加载器(App/SystemClassLoader),另外还有⼀种是 用户自定义的类加载器 ,它们各⾃有各⾃的职责。

2.1.启动类加载器 Bootstrap ClassLoader

启动类加载器作为所有类加载器的"⽼祖宗",是由C++实现的,不继承于java.lang.ClassLoader 类。它在虚拟机启动时会由虚拟机的⼀段C++代码进⾏加载,所以它没有⽗类加载器,在加载完成后,它会负责去加载扩展类加载器和应⽤类加载器。

启动类加载器⽤于加载 Java 的核⼼类--位于 \lib 中,或者被 -Xbootclasspath 参数所指定的路径中,并且是虚拟机能够识别的类库(仅按照⽂件名识别,如 rt.jar、tools.jar ,名字不符合的类库即使放在lib⽬录中也不会被加载)。

2.2.拓展类加载器 Extension ClassLoader

拓展类加载器继承于 java.lang.ClassLoader 类,它的⽗类加载器是启动类加载器,⽽启动类加载器在 Java 中的显示就是 null。

引⾃ jdk1.8 ClassLoader#getParent() ⽅法的注释,这个⽅法是⽤于获取类加载器的⽗类加载器: Returns the parent class loader for delegation. Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class loader's parent is the bootstrap class loader.

拓展类加载器负责加载 \lib\ext ⽬录中的,或者被 java.ext.dirs 系统变量所指定的路径的所有类。

需要注意的是扩展类加载器仅⽀持加载被打包为 .jar 格式的字节码⽂件。

2.3.应⽤类/系统类加载器 App/System ClassLoader

应⽤类加载器继承于 java.lang.ClassLoader 类,它的⽗类加载器是扩展类加载器。

应⽤类加载器负责加载⽤户类路径 classpath 上所指定的类库。

如果应⽤程序中没有⾃定义的类加载器,⼀般情况下应⽤类加载器就是程序中默认的类加载器。

2.4.⾃定义类加载器 Custom ClassLoader

⾃定义类加载器继承于 java.lang.ClassLoader 类,它的⽗类加载器是应⽤类加载器。

这是普某户籍⾃定义的类加载器,可加载指定路径的字节码⽂件。

⾃定义类加载器需要继承 java.lang.ClassLoader 类并重写 findClass ⽅法(下⽂有说明为什么不重写 loadClass ⽅法)⽤于实现⾃定义的加载类逻辑。

3、双亲委派模型的好处

  1. 基于双亲委派模型规定的这种带有优先级的层次性关系,虚拟机运⾏程序时就能够避免类的重复加载。 当⽗类类加载器已经加载过类时,如果再有该类的加载请求传递到⼦类类加载器,⼦类类加载器执⾏ loadClass ⽅法,然后委托给⽗类类加载器尝试加载该类,但是⽗类类加载器执⾏ Class c = findLoadedClass(name); 检查该类是否已经被加载过这⼀阶段就会检查到该类已经被加载过,直接返回该类,⽽不会再次加载此类。

  2. 双亲委派模型能够避免核⼼类篡改。⼀般我们描述的核⼼类是 rt.jar、tools.jar 这些由启动类加载器加载的类,这些类库在⽇常开发中被⼴泛运⽤,如果被篡改,后果将不堪设想。 假设我们⾃定义了⼀个 java.lang.Integer 类,与好处1⼀样的流程,当加载类的请求传递到启动类加载器时,启动类加载器执行findLoadedClass(String) ⽅法发现 java.lang.Integer 已经被加载过,然后直接返回该类,加载该类的请求结束。虽然避免核⼼类被篡改这⼀点的原因与避免类的重复加载⼀致,但这还是能够作为双亲委派模型的好处之⼀的。

4、双亲委派模型的不足

这⾥所说的不⾜也可以理解为打破双亲委派模型,当双亲委派模型不满⾜⽤户需求时,⾃然是由于其不⾜之处也就促使⽤户将其打破这⾥描述的也就是打破双亲委派模型的三种

  1. 由于历史原因( ClassLoader 类在 JDK1.0 时就已经存在,⽽双亲委派模型是在 JDK1.2 之后才引⼊的),在未引⼊双亲委派模型时,⽤户自定义的类加载器需要继承 java.lang.ClassLoader 类并重写 loadClass() 方法,因为虚拟机在加载类时会调⽤ ClassLoader#loadClassInternal(String) ,而这个⽅法(源码如下)会调⽤自定义类加载重写的 loadClass() 方法。

而在引⼊双亲委派模型后, ClassLoader#loadClass ⽅法实际就是双亲委派模型的实现,如果重写了此⽅法,相当于打破了双亲委派模型。为了让⽤户⾃定义的类加载器也遵从双亲委派模型, JDK新增了 findClass 方法,⽤于实现⾃定义的类加载逻辑。

/** * @author 庭前云落 * @Date 2020/8/22 21:29 * @description */public class ClassLoader {    // This method is invoked by the virtual machine to load a class.    private Class loadClassInternal(String name) throws            ClassNotFoundException {        // For backward compatibility, explicitly lock on 'this' when        // the current class loader is not parallel capable.        if (parallelLockMap == null) {            synchronized (this) {                return loadClass(name);            }        } else {            return loadClass(name);        }    }// 其余⽅法省略......}
  1. 由于双亲委派模型规定的层次性关系,导致⼦类类加载器加载的类能访问⽗类类加载器加载的类,⽽⽗类类加载器加载的类⽆法访问⼦类类加载器加载的类。为了让上层类加载器加载的类能够访问下层类加载器加载的类,或者说让⽗类类加载器委托⼦类类加载器完成加载请求,JDK 引⼊了线程上下⽂类加载器,藉由它来打破双亲委派模型的屏障。

  2. 当⽤户需要程序的动态性,⽐如代码热替换、模块热部署等时,双亲委派模型就不再适⽤,类加载器会发展为更为复杂的⽹状结构。

5、线程上下文类加载器

上⾯说到双亲委派模型的不⾜时提到了线程上下⽂类加载器 Thread Context ClassLoader ,线程上下⽂类加载器是定义在 Thread 类中的⼀个 ClassLoader 类型的私有成员变量,它指向了当前线程的类加载器。

上⽂已经提到线程上下⽂类加载能够让⽗类类加载器委托⼦类类加载器完成加载请求,那么这是如何实现的呢?下⾯就来讨论⼀下。

5.1.SPI 在 JDBC 中的应用

我们知道 Java 提供了⼀些 SPI(Service Provider Interface) 接⼝,它允许服务商编写具体的代码逻辑来完成该接⼝的功能。

但是 Java 提供的 SPI 接⼝是在核⼼类库中,由启动类加载器加载的,⼚商实现的具体逻辑代码是在classpath 中,是由应⽤类加载器加载的,⽽启动类加载器加载的类⽆法访问应⽤类加载器加载的类,也就是说启动类加载器⽆法找到 SPI 实现类,单单依靠双亲委派模型就⽆法实现 SPI 的功能了,所以线程上下⽂类加载器应运⽽⽣。

在 Java 提供的 SPI 中我们最常⽤的可能就属 JDBC 了,下⾯我们就以 JDBC 为例来看⼀下线程上下⽂类加载器如何打破双亲委派模型。

回忆⼀下以前使⽤ JDBC 的场景,我们需要创建驱动,然后创建连接,就像下⾯的代码这样:

/** * @author 庭前云落 * @Date 2020/8/22 21:29 * @description */public class ThreadContextClassLoaderDemoOfJdbc {    public static void main(String[] args) throws Exception {        // 加载 Driver 的实现类        Class.forName("com.mysql.jdbc.Driver");        // 建⽴连接        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");    }}

在 JDK1.6 以后可以不⽤写 Class.forName("com.mysql.jdbc.Driver"); ,代码依旧能正常运⾏。这是因为⾃带的 jdbc4.0 版本已⽀持 SPI 服务加载机制,只要服务商的实现类在classpath 路径中,Java 程序会主动且⾃动去加载符合 SPI 规范的具体的驱动实现类,驱动的全限定类名在 META-INF.services ⽂件中。

所以,让我们把⽬光聚焦于建⽴连接的语句,这⾥调⽤了 DriverManager 类的静态⽅法getConnection 。

在调⽤此⽅法前,根据类加载机制的初始化时机,调⽤类的静态⽅法会触发类的初始化,当 DriverManager 类被初始化时,会执⾏它的静态代码块。

/** * @author 庭前云落 * @Date 2020/8/22 21:29 * @description */public class DriverManager {    static {        loadInitialDrivers();        println("JDBC DriverManager initialized");    }    private static void loadInitialDrivers() {        String drivers;        // 省略代码:⾸先读取系统属性 jdbc.drivers        // 通过 SPI 加载 classpath 中的驱动类        AccessController.doPrivileged(new PrivilegedAction() {            public Void run() {                // ServiceLoader 类是 SPI 加载的⼯具类                ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);                Iterator driversIterator = loadedDrivers.iterator();                try {                    while (driversIterator.hasNext()) {                        driversIterator.next();                    }                } catch (Throwable t) {                    // Do nothing                }                return null;            }        });        // 省略代码:使⽤应⽤类加载器继续加载系统属性 jdbc.drivers 中的驱动类    }}

从上⾯的代码中可以看到,程序时通过调⽤ ServiceLoader.load(Driver.class) ⽅法来完成⾃动加载。classpath 路径中具体的所有实现了 Driver.class 接⼝的⼚商实现类,⽽在 ServiceLoader.load() 方法中,就是获取了当前线程上下⽂类加载器,并将它传递下去,将它作为类加载器去实现加载逻辑的。

/** * @author 庭前云落 * @Date 2020/8/22 21:29 * @description */public final class ServiceLoader implements Iterable{    public static  ServiceLoader load(Class service) {        // 获取当前线程的线程上下⽂类加载器 AppClassLoader,⽤于加载 classpath 中的具体实现类        ClassLoader cl = Thread.currentThread().getContextClassLoader();        return ServiceLoader.load(service, cl);    }}

JDK 默认加载当前类的类加载器去加载当前类所依赖且未被加载的类,而ServiceLoader 类位于java.util 包下,⾃然是由启动类加载器完成加载,而厂商实现的具体驱动类是位于 classpath 下,启动类加载器⽆法加载 classpath 目录的类,而如果加载具体驱动类的类加载器变成了应⽤类加载器,那么就可以完成加载了。

通过跟踪代码,不难看出 ServiceLoader#load(Class) ⽅法创建了⼀个 LazyIterator 类同时返回了⼀个 ServiceLoader 对象,前者是⼀个懒加载的迭代器,同时它也是后者的⼀个成员变量,当对迭代器进行遍历时,就触发了⽬标接⼝实现类的加载。

/** * @author 庭前云落 * @Date 2020/8/22 21:29 * @description */private class LazyIterator implements Iterator {    public S next() {        if (acc == null) {            return nextService();        } else {            PrivilegedAction action = new PrivilegedAction() {                public S run() { return nextService(); }            };            return AccessController.doPrivileged(action, acc);        }    }}

在 DriverManager#loadInitialDrivers ⽅法,也就是 DriverManager 类的静态代码块所执⾏的⽅法中,有这样⼀段代码:

AccessController.doPrivileged(new PrivilegedAction() {        public Void run () {        ServiceLoader loadedDrivers =                ServiceLoader.load(Driver.class);        Iterator driversIterator =                loadedDrivers.iterator();        try {            while (driversIterator.hasNext()) {                driversIterator.next();            }        } catch (Throwable t) {            // Do nothing        }        return null;    } });

这段代码返回了⼀个 ServiceLoader 对象,在这个对象中有⼀个 LazyIterator 迭代器类,⽤于存放所有⼚商实现的具体驱动类,当我们对 LazyIterator 这个迭代器进⾏遍历时,就出发了类加载的逻辑。

private S nextService() {        if (!hasNextService())            throw new NoSuchElementException();        String cn = nextName;        nextName = null;        Class c = null;        try {            // 不⽤写 Class.forName("com.mysql.jdbc.Driver"); 的原因就是在此处会⾃动调⽤这个⽅法            c = Class.forName(cn, false, loader);        } catch (ClassNotFoundException x) {            fail(service, "Provider " + cn + " not found");        }        if (!service.isAssignableFrom(c)) {            fail(service, "Provider " + cn + " not a subtype");        }        try {            S p = service.cast(c.newInstance());            providers.put(cn, p);            return p;        } catch (Throwable x) {            fail(service, "Provider " + cn + " could not be instantiated", x);        }        throw new Error(); // This cannot happen    }

每次遍历都会调⽤ Class.forName(cn, false, loader) ⽅法对指定的类进⾏加载和实例化操作,这也是前⽂提到的在 jdk1.6 以后不⽤在写 Class.forName("com.mysql.jdbc.Driver"); 的原因。

在这个⽅法 Class.forName(cn, false, loader) 中,传⼊的参数 cn 是全路径类名,false 是指不进⾏初始化,loader 则是指定完成 cn 类加载的类加载器。

在这⾥的 loader 变量,我们回顾⼀下前⽂的描述,在 ServiceLoader.load(Driver.class) ⽅法中是不是获取了线程上下⽂类加载器并传递下去?

不记得?在回过头去看⼀遍!

⽽传⼊的线程上下⽂类加载器会作为参数传递给 ServiceLoader 类的构造方法

   private ServiceLoader(Class svc, ClassLoader cl) {        service = Objects.requireNonNull(svc, "Service interface cannot be null ");        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;        acc = (System.getSecurityManager() != null) ?                AccessController.getContext() : null;        reload();    }

⽽此处的 cl 变量就是调⽤ DriverManager 类静态⽅法的线程上下⽂类加载器,即应⽤类加载器。

也就是说,通过 DriverManager 类的静态⽅法,实现了由 ServiceLoader 类触发加载位于 classpath的⼚商实现的驱动类。前⽂已经说过, ServiceLoader 类位于 java.util 包中,是由启动类加载器加载的,⽽由启动类加载器加载的类竟然实现了"委派"应⽤类加载器去加载驱动类,这⽆疑是与双亲委派机制相悖的。⽽实现这个功能的,就是线程上下⽂类加载器。

感谢各位的阅读,以上就是"JVM双亲委派模型及SPI实现原理是什么"的内容了,经过本文的学习后,相信大家对JVM双亲委派模型及SPI实现原理是什么这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

0