写在前面的话:本文是在观看尚硅谷JVM教程后,整理的学习笔记。其观看地址如下:尚硅谷2020最新版宋红康JVM教程

一、什么是类加载器

在类加载过程中,加载阶段有一个动作是“通过一个类的全限定名称获取定义此类的二进制字节流”,虚拟机将这个动作交给应用程序,让其自行去决定怎么获取所需的类。而实现这个动作的代码就被称为类加载器。关于类加载过程,可以看我的上一篇博客《类加载过程》

1、类与加载器

类加载器除了用于实现类的加载外,还用于确定一个类在虚拟机中的唯一性,也就是说类加载器还用于判断两个类是否相等。
想要判断两个类对象是同一个类,需要两个必要条件:

① 类的完整类名包括包名都必须一致
② 加载这个类的类加载器必须是同一个类加载器加载的

这也意味着即使两个类是来自与同一个Class文件,被同一个虚拟机加载,但是只要加载它们的ClassLoader实例对象(类加载器)不同,那么这两个类对象就不相等。

2、类加载器的分类

目前,虚拟机只划分了两种类型的类加载器,一种是由C/C++语言实现的,属于虚拟机自身一部分,称为启动类加载器( Bootstrap ClassLoader )。另一种是使用Java语言实现的,不属于虚拟机自身的一部分,且全部继承于java.lang.ClassLoader抽象类的类加载器,也就是除启动类加载器外的所有类加载器都属于这一种类。

虽然从虚拟机的角度,类加载器只分为启动类加载器和其他类加载器这两种。
但从我们开发人员的角度,虚拟机实际上有有三种常见的类加载器,构成了三层类加载器的架构。
这三种类加载器分别为启动类加载器( Bootstrap ClassLoader )、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)。

这三种类加载器,再加上我们开发人员自己定义的用户自定义类加载器就构成了目前虚拟机的主体类加载结构,如下图所示:

在这里插入图片描述
需要注意的是,这里图示的关系并不是继承关系。
这更像一种上下层的层次关系。比如,加载一个类就像在文件夹中寻找某一个文件。现在有目录a/b/c/d.txt。现在找文件d.txt这个动作就好比加载一个类,找到相应的目录就是通过合适的类加载器去加载类。那么,我们会先在a这个文件夹里找(启动类加载器负责加载),但找不到(不合适)。于是往下一层,在b这个文件夹里找(扩展类加载器负责加载),但找不到(不合适)。于是再往下一层(应用程序类加载器负责加载),找到了(合适),于是就由这个应用程序类加载器加载。

虽然这几种类加载器不是继承关系。但实际上除了启动类加载器,每一种加载器都有自己的父类加载器。箭头指向的就是自己的父类加载器:

用户自定义加载器 - - -> 应用程序类加载器 ---> 扩展类加载器 ---> 启动类加载器

这个由顶层依次向下查找的过程就是双亲委派机制的原理,更具体的过程将在下面写到。

现在就来详细分析包括用户自定义类加载器在内的各种类加载器的详细作用。

3、启动类加载器

启动类加载器是虚拟机自带的类加载器,属于虚拟机的一部分。
这个类加载器特点和作用如下:

①由C/C++实现,并不继承于Java.lang.ClassLoader类。
②用来加载Java的核心类库(Java_HOME/jre/lib/rt.jarresource.jarsun.boot.class.path这几个路径下的内容,即这些路径下的类),用于提供JVM自身需要的类。
③负责加载扩展类加载器和应用程序类加载器,并作为他们的父类加载器。
④处于安全考虑,启动类加载器只加载包名为java、javax、sun等开头的类名。

4、扩展类加载器

①由Java实现,间接继承于Java.lang.ClassLoader类,在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现,不需要我们编写。
②父类加载器是启动类加载器。
③负责加载java.ext.dirs系统变量所定义的路径中的所有类库或者Java_HOME/jre/lib/ext目录下(扩展目录)的所有类库。如果我们创建的jar放在此目录下,扩展类加载器也会自动去加载。

5、应用程序类加载器

①由Java实现,间接继承于Java.lang.ClassLoader类。在类sun.misc.Launcher$AppClassLoader中以Java代码的形式实现,不需要我们编写。
②父类加载器是扩展类加载器。
③负责加载环境变量classpath或者系统属java.class.path指定的路径下的类库。
④该类加载器是Java程序中默认的类加载器,一般来说Java应用程序的类都由它来完成
⑤通过ClassLoader.getSystemClassLoader();可以直接获取到该类加载的实例对象。

下面是获取每个类加载器的代码示例:

public class ClassLoderTest {
    public static void main(String[] args) {

        //获取系统提供的最下层加载器:应用程序类加载器(AppClassLoader)
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);  //sun.misc.Launcher$AppClassLoader@18b4aac2

        //获取应用程序类加载器上层:扩展类加载器(ExtClassLoader)
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@1540e19d

        //获取扩展类加载器上层:获取不到了(无法获取到bootstrapClassLoader)
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);   //null

        //对于用户的自定义的类来说,默认是由应用程序类加载器来加载的
        ClassLoader userClassLoader = ClassLoderTest.class.getClassLoader();
        System.out.println(userClassLoader);    //sun.misc.Launcher$AppClassLoader@18b4aac2

        //String类是使用启动类加载器加载的,即Java的核心类库是由启动类加载器加载的,但无法被用户直接引用
        ClassLoader strClassLoader = String.class.getClassLoader();
        System.out.println(strClassLoader); //null
    }
}

结**果如下:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null

二、双亲委派机制

虚拟机对Class文件采用的是按需加载的方式,即用到某个类时才会把它的class文件加载到内存中生成class对象,在加载过程中,虚拟机采用的是双亲委派机制,是一种任务委派模式。
它的具体工作原理是:

(1)、当虚拟机加载一个类时,会调用到类加载器。如果这个类不是指定由用户自定义的类加载器来加载,那么默认由应用程序类来加载。
(2)双亲委派机制就是,当一个类加载器接收到类加载请求时,它首先并不会尝试自己去加载这个类,而是把这个请求委托给父类的加载器。如果父类加载器还有父加载器,就继续向上委托。这样一层层往上委托,所有的加载请求都会传送到最顶层的启动类加载器中。
(3)、如果父类加载器可以完成加载任务,那么加载请求完成。如果父类加载器无法完成任务,就会反馈回子加载器。此时子加载器才会尝试自己去加载这个类。这就是双亲委派机制。

具体的图示如下:
在这里插入图片描述
虚拟机实现双亲委派模型的做法比较简单,其代码全部在java.lang.ClassLoader中,主要是在其中的loadClass()方法中。如下:

    protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        //首先,检查请求加载的类是否已被加载
        Class c = findLoadedClass(name);
        if(c == null){  //未被加载
            try{
                if (parent != null){    //调用父加载器,不为空,调用其loadClass()⽅法处理
                    c = parent.LoadClass(name,false);
                }else { //父加载器为空,调用启动类加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            }catch (){
                //抛出异常说明⽗类加载器⽆法完成加载请求
            }
            if (c == null){ //尝试自己加载
                c = findClass(name);
            }
        }
        if (resolve){
            resolveClass(c);
        }
        return c;
    }

1、使用双亲委派机制的优点

(1)、避免类被重复加载:即Java中的类会跟加载它的类加载器具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父加载器已经加载了该类时,就没有必要子加载器再加载一次。
(2)、保护程序安全,防止核心API被篡改:比如,我们自定义一个叫java.lang.String的类。如果没有双亲委派机制,那么虚拟机在加载该类后,虚拟机无法分辨出使用的是哪个String类,不仅造成混乱,也可能造成虚拟机无法执行。但在双亲委派机制下,核心的API都会被委托给顶层的启动类加载器加载,用户自定义的核心类根本不会被加载,因为类加载器会发现该类已被加载过了,所以不会尝试去加载用户自定义的核心类。
(3)、创建了额外的命名空间:同名的类可以并存在Java虚拟机中,只需要使用不同的类加载器去加载它们即可。这相当于在虚拟机中创建了相互隔离的Java类空间。

2、破坏双亲委派机制

(1)、为什么要打破双亲委派模型
启动类加载器是最上层的类加载器,这也意味着它无法委托下层的类加载器去加载它所需要的的类。因为启动类加载器加载的是最基础的类,如String类等。在一般情况下,这些类都是被其他类继承或者调用的。但是也会有启动类加载器加载的基础类,去调用用户编写的类的情况。最典型的就是Java的SPI机制。
(2)、SPI机制
spi即Java内置的服务发现机制。简单来说,就是Java把要使用的服务定义成接口,而实现类由第三方来实现。最典型的spi就是JDBC。在JDK中,官方提供了一个数据库驱动接口,而接口的实现由数据库厂商来做的。
spi的实现只需3步即可,以JDBC为例:

① JDK定义好接口
②数据库厂商根据接口编写实现类后打包成jar包,并在jar包中创建一个META-INF/services目录,同时在这个目录下创建一个名字为JDK所需接口的全限定名称的文件,该文件的内容就是实现该接口的实现类的名称。
③虚拟机通过java.util.ServiceLoder这个工具类去把接口实现类加载到JVM中。具体的查找方法就是通过②中的目录找到接口实现类(该目录是约定俗成的)

那么JDBC中的spi与打破双亲委派模型有什么联系呢?

通过查看MySQL的Java连接驱动包可以看出JDK提供的数据库连接接口就是Java.sql.Driver

在这里插入图片描述
而这个接口是属于Java.sql包的。这个包是在Java_HOME/jre/lib/rt.jar,很明显这个包中的类是由启动类加载器加载的。而在这个包中不仅有Java.sql.Driver,还有其他跟数据库连接相关的接口,比如常用的管理驱动的类Java.sql.DriverManager类。而Java.sql.DriverManager类想要获得管理的数据库链接,就要加载第三方实现类com.mysql.cj.jdbc.Driver。但DriverManage类是由启动类加载器加载的,故启动类加载器会收到com.mysql.cj.jdbc.Driver的加载请求。

按照双亲委派原则,启动类加载器在接到请求后,应该先向上委托自己的父类去加载,但是启动类加载器既没有父类可以委托,自己也不能去完成加载,且不能向下委托下层类加载器去加载。所以想要完成加载请求,就必须打破双亲委派模型。

(3)、如何打破双亲委派模型
还是以JDBC为例,我们根据Java.sql.DriverManager类来分析。
在平时手动连接数据库中,我们这样写代码:

Class.forName("com.mysql.cj.jdbc.Driver");
Connection my_connection = my_connection = DriverManager.getConnection();

查看DriverManager的源码,可以发现其静态代码块:

static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

再看loadInitialDrivers();方法,有如下代码:

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                //工具类
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

可以发现有我们前面提到的工具类java.util.ServiceLoder,该工具类作用就是找到接口的实现类。这意味着,我们在使用DriverManager的时候,虚拟机已经去加载由第三方实现的java.sql.Driver接口的实现类了。

再往下看ServiceLoader.load()方法的源码,有:

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

我们会发现这样一句代码:

Thread.currentThread().getContextClassLoader();

这句代码的作用就是获取一个线程上下文类加载器(Thread Context ClassLoader),这个类加载器也是打破双亲委派机制的关键。

线程上下文类加载器(Thread Context ClassLoader):这个类加载器是Java为了打破双亲委派机制而引用的一个设计。这个类加载器并非具体指某一个类加载器,而是根据实际情况来确定的。这个类加载器可以根据java.lang.Thread类的setContext-ClassLoader()方法来指定设置,如果没有设置,那么会从父类线程继承一个,如果父类和其祖先类线程都没设置,那么它默认就是应用程序类加载器(Application ClassLoader)

由于线程上下文类加载器是被放在线程中,即线程运行时都会存在,所以任何时候都可以调用它来完成类加载动作。

那么在启动类加载器加载Java.sql.DriverManager类时,会通过使用线程上下文加载器(一般是应用程序类加载器)加载所需要的第三方的接口实现类。这本质上相当于,不遵守双亲委派中逐层向上委托并且不能向下委派的原则,直接指定了某一类具体的类加载器去加载所需要的的类。

这就是spi机制中打破双亲委派原则的基本方式,有点类似于作弊,主要是为了双亲委派机制的缺陷。