vlambda博客
学习文章列表

JVM的双亲委派机制

前言

JVM可识别的文件是一个个的Class,而这些Class需要正确的运行起来就需要JVM的类加载器将这些Class文件加载到内存,并对数据进行校验、转换、解析、初始化并最终形成可以被JVM直接执行的指令。

而JVM的类加载器去加载Class文件的同时需要遵循一定的原则,那就是双亲委派的原则,所以本文就结合类加载器的源码去探索什么是所谓的双亲委派的原则。

一、类加载器是什么?

实现通过一个类的全限定名来获取描述该类的二进制流的动作的代码就叫做类加载器。类加载器主要分为以下几类:

  • Bootstrap: 加载lib.rt charset.jar 等核心内容,。
  • Extension: 加载扩展jar包,jre/lib/ext/*.jar
  • App: 加载classpath的内容
  • Custom Class Loader: 自定义类加载器

其中Bootstrap Class loader是JVM中最顶级的,由C++语言实现,获取Bootstrap类加载器会返回null。

二、双亲委派原则

一个类加载器收到类加载请求后不会立即先加载自己,而是先去让父级的加载器去检查缓存中,是否已经加载,层层迭代,到最顶层加载器都没有,会往下进行委派去加载指定的类。

双亲委派机制的好处:

  • 避免重复加载:资源浪费的问题,父类已经加载了,子类就不需要再次加载。

  • 保证了类加载的安全性:解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心 API,会带来安全隐患。比如Object 类。它存放在 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型顶端的启动类加载器加载, 因此 Object 类在程序的各种加载环境中都是同一个类。

注:父加载器不是类加载器的加载器,也不是类加载器的父类加载器。

下面就跟着ClassLoader的源码,一步步去发掘双亲委派机制的具体实现。


Launcher源码

Launcher是java程序的入口,Launcher的ClassLoader是BootstrapClassLoader,在Launcher创建的同时,还会创ExtClassLoader, AppClassLoaderJVM的双亲委派机制

ClassLoader源码

ClassLoader 是一个抽象类,像 ExtClassLoader,AppClassLoader 都是由该类派生出来,实现不同的类装载机制。在ClassLoader 中的loadClass是类装载的入口:JVM的双亲委派机制

如何自定义一个ClassLoader

实现ClassLoader的钩子函数findClass

public class MyClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String relativePath = name.replaceAll("\\.""/");
        File file = new File("/Users/leofee/projects/leofee/java-learning/out/production/java-learning/" + relativePath + ".class");

        if (!file.exists()) {
            // throws ClassNotFoundException
            return super.findClass(name);
        }

        try (ByteArrayOutputStream out = new ByteArrayOutputStream();
             FileInputStream in = new FileInputStream(file)) {

            byte[] bytes = new byte[1024 * 4];

            while (in.read(bytes) != -1) {
                out.write(bytes);
            }
            return defineClass(name, out.toByteArray(), 0, out.toByteArray().length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // throws ClassNotFoundException
        return super.findClass(name);
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader classLoader = new MyClassLoader();

        Class<?> hello = classLoader.loadClass("jvm.t02_class_loader.Hello");

        Hello instance = (Hello) hello.newInstance();
        instance.sayHello();
    }
}

如何破坏双亲委派原则

要想打破双亲委派的机制,目前暂已知的有两种:

  • 利用Thread.currentThread().getContextClassLoader();

  • 重写ClassLoader的loadClass方法。

在探索如何打破双亲委派机制之前,我们首先有个疑问,这大牛们设计好的双亲委派机制我们为什么要去打破呢?

其实双亲委派机制也有它本身的缺陷,但是这个缺陷一般情况下涉及不到,所以就不太关心,从双亲委派机制的整体来看,加载类的是从上至下进行加载的,所以在顶层类加载器中加载的类是无法访问子加载器中加载的类,就像BootStrap加载器中加载的都是rt.jar中的Class,rt.jar中的class一般作为用户调用的api,一般情况下也不会去访问子加载器中的类库,目前也无法访问到,因为BootStrapClassLoader是顶层的类加载器。

而有些特殊情况,目前都是提倡面向接口编程,目的是为了松耦合,接口提供商和实现类提供商也不一定就是一家公司,所以jdk提供了SPI(Service Provider Interface)的机制,最鲜明的例子就是数据库驱动java.sql.DriverDriver是JDK定义的数据库驱动规范接口,而Driver的实现类是由各家数据库提供商编写,而且JDK提供了DriverManager用于管理这些数据库的驱动。

在JDBC4.0之前,我们要加载数据库驱动,必须要先利用Class.forName("com.mysql.jdbc.Driver");将具体的数据库驱动实现类加载进来,Class.forName其实是利用了AppClassLoader进行加载,只要数据库驱动类在ClassPath中能找到即可。

JVM的双亲委派机制
在这里插入图片描述

在JDBC4.0之后,我们可以不需要再利用Class.forName手工加载数据库的驱动类,因为JDK中的SPI机制(在META-INF/services/目录下定义以接口全限定名的文件,文件的内容即接口的实现类全限定名), JDK会利用ServiceLoader.load方法去扫描这些实现类。下面我们就以Mysql数据库驱动为例,从获取数据库连接的源码去分析整个过程:

  1. 通常我们获取数据库的连接都是通过DriverManager.getConnection(url, username, password); 调用该方法首先会触发DriverManager的静态代码块,在loadInitialDrivers()中使用了ServiceLoader去查找那些Driver对应的SPI的描述文件。JVM的双亲委派机制

  2. 根据指定的接口Class(这里指的是Driver的接口),并通过Thread.currentThread().getContextClassLoader()获取线程上下文的ClassLoader, 然后实例化一个ServiceLoader,这里的获取到的ClassLoader其实就是AppClassLoader,关于线程上下文的ClassLoader就是在上面Launcher类中设置进去的。JVM的双亲委派机制

  3. ServiceLoader在内部类LazyIterator#nextSerivice()遍历的时候,使用第2步中的ClassLoader(实际就是AppClassLoader)去装载对应的Driver的实现类Class。JVM的双亲委派机制

  4. 当装载完成后,会触发MySql数据库驱动类com.mysql.jdbc.Driver的静态代码块:JVM的双亲委派机制

  5. 接着调用DriverManager.registerDriver将数据库驱动实现类添加DriverManager的驱动清单中。

  6. 致此,利用DriverManager.getConnection()去获取数据库连接时就能顺利成章的拿到对应的驱动类了。

纵观JDK提供的SPI机制,由于DriverManager是rt.jar中的类,是由BootStrapClass进行装载的,而DriverManager在BootStrapClassLoader装载的时候却去利用线程上下文中的ClassLoader去装载Mysql数据库的驱动类,这样就违背的了双亲委派的机制的原则。


上面说的是基于JDK提供的SPI机制,而关于第二种方式,我们从ClassLoader的源码可以看出,整个双亲委派的机制其实都是在ClassLoader#loadClass方法中定义的,所以想打破双亲委派的机制只需要重写loadClass方法即可,在自己的loadClass方法中定义装载类的机制。

总结

以上就是关于JVM中的双亲委派机制的原理,在什么情况下需要去打破大牛设计好的机制以及如何去打破双亲委派的机制。

参考文档:

https://blog.csdn.net/cy973071263/article/details/104129163