【JVM】深入分析JVM类加载(三)-ClassLoader是如何加载类的?

1,426 阅读10分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

一、开篇

本文主要讲解ClassLoader是如何加载和管理类,为方便大家顺畅地理解本文,先介绍本文的思路:

(1) ClassLoader是什么?用来做什么的?常见的BootstrapClassloader、ExtClassLoader、AppClassLoader、SystemClassLoader、他们各是什么?有什么区别?

(2) ClassLoader双亲委派模型是什么?为何有双亲委派模型?

(3) 从源码分析双亲委派模型。

本文主要讲解 JDK8 的ClassLoader机制,其他版本可能有所差异。

二、ClassLoader初探

ClassLoader是什么?

ClassLoader即类加载器,ClassLoader根据一个类的全限定名(例如: java.util.ArrayList)来获取描述该类的二进制字节流,然后将字节流加载到 JVM,解析类信息,生成java.lang.Class对象。

如何确定两个类是否相等?

如何确定两个类是否"相等"?一个ClassLoader实例加一个Class文件唯一确认一个类。也就是,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance() 方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况。

通过下面的例子,我们先体验一下ClassLoader如何加载类以及比较加载自相同class文件的两个类。

首先创建类ClassA

package com.skyme.jvm.classloader;

public class ClassA {
}

再通过ClassLoaderTest来做试验:

package com.skyme.jvm.classloader;

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {

    /**
     * 自定义一个ClassLoader
     */
    public static class MyClassLoader extends ClassLoader {

        /**
         * 自定义获取Class文件的方式
         */
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            // 注意:此处自定义的loadClass(name),没有调用父类ClassLoader的loadClass(name)方法
            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);
            }
        }
    }

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        // 步骤1. 创建MyClassLoader对象
        MyClassLoader myClassLoader = new MyClassLoader();
        // 步骤2. 使用MyClassLoader加载"com.skyme.jvm.classloader.ClassA"
        Class<?> classA = myClassLoader.loadClass("com.skyme.jvm.classloader.ClassA");
        // 步骤3. 使用classA创建实例对象
        Object classAObj = classA.newInstance();
        // 步骤4. 打印classAObj对应的Class
        System.out.println("步骤4 classAObj对应的Class: " + classAObj.getClass());

        // 步骤5. 打印Class的ClassLoader
        System.out.println("步骤5.1 MyClassLoader加载的com.skyme.jvm.classloader.ClassA对应的ClassLoader: " + classAObj.getClass().getClassLoader());
        System.out.println("步骤5.2 xxx.class方式加载的com.skyme.jvm.classloader.ClassA 对应的ClassLoader: " +
                com.skyme.jvm.classloader.ClassA.class.getClassLoader());

        // 步骤6. 对比class
        System.out.println("步骤6.1 MyClassLoader加载的com.skyme.jvm.classloader.ClassA与xxx.class方式加载的com.skyme.jvm.classloader.ClassA是否相等: " +
                (classA.equals(com.skyme.jvm.classloader.ClassA.class)));

        System.out.println("步骤6.2 classAObj是否是xxx.class方式加载的com.skyme.jvm.classloader.ClassA的实例:" +
                (classAObj instanceof com.skyme.jvm.classloader.ClassA));
    }
}

上面例子运行结果:

步骤4 classAObj对应的Class: class com.skyme.jvm.classloader.ClassA
步骤5.1 MyClassLoader加载的com.skyme.jvm.classloader.ClassA对应的ClassLoader: com.skyme.jvm.classloader.ClassLoaderTest$MyClassLoader@330bedb4
步骤5.2 xxx.class方式加载的com.skyme.jvm.classloader.ClassA 对应的ClassLoader: sun.misc.Launcher$AppClassLoader@14ae5a5
步骤6.1 MyClassLoader加载的com.skyme.jvm.classloader.ClassA与xxx.class方式加载的com.skyme.jvm.classloader.ClassA是否相等: false
步骤6.2 classAObj是否是xxx.class方式加载的com.skyme.jvm.classloader.ClassA的实例:false

我们自定义了一个MyClassLoader,通过覆写loadClass()方法,实现了根据类名加载com.skyme.jvm.classloader.ClassA

通过运行结果里步骤5的结果可看出,com.skyme.jvm.classloader.ClassA.class方式获取的Class对象,该Class对象所属ClassLoader是AppClassLoader,而myClassLoader.loadClass("com.skyme.jvm.classloader.ClassA")方式获取Class对象所属ClassLoader是MyClassLoader。

通过步骤6的结果发现,都是com.skyme.jvm.classloader.ClassA类对应的Class对象,但是通过Class的equals()方法进行对比,他们结果并不相等,且通过instanceof关键字判断发现:MyClassLoader加载的Class对象创建的实例对象classAObj,并不属于com.skyme.jvm.classloader.ClassA(因为这个Class对象是AppClassLoader加载的)。

三、ClassLoader分类

下面通过思维导图整理了ClassLoader的分类。

ClassLoader-思维导图

站在Java开发人员的角度,了解一下BootstrapClassLoader、ExtClassLoader、AppClassLoader,还有自定义ClassLoader。

1 BootstrapClassLoader

主要用以加载核心类库,例如: java.util.List、java.lang.String。BootstrapClass是用C++语言实现,是虚拟机自身的一部分,且无法被Java程序直接引用。

加载的文件

1.BootstrapClassLoader会加载<JAVA_HOME>\lib目录下的类库

2.-Xbootclasspath参数所指定路径的类库

2 ExtClassLoader

BootstrapClassLoader加载核心类库,ExtClassLoader加载的是一些扩展类库,例如:DNS相关的dnsns.jar,对应源码在sun.misc.Launcher.ExtClassLoader。在JDK9之后,ExtClassLoader被PlatformClassLoader取代。

加载的文件

1.BootstrapClassLoader会加载<JAVA_HOME>\lib\ext目录下的类库

2.java.ext.dirs系统变量所指定路径的类库

ExtClassLoader常见错误用法

1.-Djava.ext.dirs中没有加上<JAVA_HOME>\lib\ext目录

-Djava.ext.dirs会覆盖Java本身的ext设置,java.ext.dirs指定的目录由ExtClassLoader加载器加载,如果您的程序没有指定该系统属性(-Djava.ext.dirs=d:\libs)那么该加载器默认加载<JAVA_HOME>\lib\ext目录下的所有文件。但如果你手动指定系统属性且忘了把<JAVA_HOME>\lib\ext路径给加上,那么ExtClassLoader不会去加载<JAVA_HOME>\lib\ext下面的jar文件,这意味着你将失去一些功能,例如java自带的加解密算法实现,所以如果要设置java.ext.dirs,一般情况,java.ext.dirs也得加上<JAVA_HOME>\lib\ext,示例:-Djava.ext.dirs="lib\ext;%JAVA_HOME%\jre\lib\ext"。

2.-java.ext.dirs指定应用自身类库

实际工作中,看到许多用项目中-java.ext.dirs指定了应用自身类库(应用自身代码、依赖的jar包等),导致用户的包被ExtClassLoader加载,但是很少场景适合用-Djava.ext.dirs去指定类库,建议应用自身类库用-classpath指定,然后由AppClassLoader加载。之前一些项目-java.ext.dirs指定了应用自身类库,然后项目通过Javaagent方式接入skywalking-agent时,就出现找不到类的情况,导致项目无法启动,同事硕总的这篇文章对这个案例做过分析插件的类隔离设计-奇葩问题的解决

3 AppClassLoader

AppClassLoadr是我们接触最多的ClassLoader,我们应用自身类库(应用自身代码、依赖的jar包等)一般都是通过AppClassLoader加载,当然也有特别的情况,如: SpringBoot应用打包成fatjar方式被启动时,应用中的类库是通过SpringBoot的自定义ClassLoader(即LaunchedURLClassLoader)加载的。

对应源码在sun.misc.Launcher$AppClassLoader。AppClassLoader也称为系统类加载器,因为java.lang.ClassLoader#getSystemClassLoader默认返回的是AppClassLoader。

加载的文件

1.-cp或-classpath指定的路径

2.jar包中MANIFEST.MF文件的Class-Path参数指定的路径

3.CLASSPATH系统环境变量指定的路径

4 自定义ClassLoader

用户也可以自定义ClassLoader,只要继承抽象类 java.lang.ClassLoader,覆写loadClass()方法或者findClass()方法。

自定义的ClassLoader给我们提供了强大的扩展性,我们可以实现:

1.通过各种源获取Class文件

除了获取磁盘上的Class文件,我们还可以在loadClass()或者findClass()方法中获取其他来源的Class文件,如:通过网络获取Class文件。

2.源码加密保护

我们不想自己的Class文件被其他人获取后只能反编译获取源码,则可以在源码编译打包时,先用特定算法对Class文件进行加密,在JVM加载Class文件时再用自定义的ClassLoader进行解密。

3.类隔离

不同的自定义ClassLoader装载的类可相互直接隔离,例如,外置的Tomcat容器就是通过不同自定义ClassLoader的实例去加载不同web应用的类,最终实现了不同web应用间类相互隔离,不会有冲突。Arthas和skywalking的java agent通过自定义的ClassLoader实现了agent的类与宿主应用的类相互隔离,保障agent的类不会污染宿主应用的类。

4.类热加载

通过自定义的ClassLoader,可以在应用运行期间,动态加载有变化的类。热加载工具spring-boot-devtools就是通过自定义的Restart classloader来加载项目主类和有变化的类,实现了类的热加载。

此外,自定义的ClassLoader还有其他妙用,大家可以自己继续补充。

四、双亲委派模型

1 简介

双亲委派机制,对应的英文是parents delegation model,这里的parent翻译为"双亲"容易有歧义,其实ClassLoader的parent只会有1个,所以翻译为"父委派机制"更贴切一些。

双亲委派模型,简单来讲,就是ClassLoader要加载类时,会先委派给父ClassLoader去加载,父ClassLoader又会委托他自己的父ClassLoader去加载,如果上面的父ClassLoader加载不到,则再交给下一层的父ClassLoader加载,所有父ClassLoader加载不到类,才会交由当前ClassLoader加载。

2 为何有双亲委派模型?

1.防止Java核心类库被开发者的类库篡改。

2.类复用,减少重复加载。例如,java.lang.Object类其实只要由BootstrapClassLoader加载一次就可以,其他ClassLoader不需要再重复加载。

3 双亲委派的类加载过程

下图展示了双亲委派模型下类加载的过程。

image-20221224125933087

上图中AppClassLoader、ExtClassLoader、BootstrapClassLoader的委派机制,大家都比较熟悉。

此外,有两个关注点需要注意:

1.如果ClassLoader类加载成功,则该ClassLoader会缓存该类,下次该ClassLoader就不用再执行类加载动作,直接读缓存返回即可。

2.对于自定义类加载器并非都是会委派给AppClassLoader去加载,即UserClassLoader1如果按上图的委派给AppClassLoader执行加载是有前提的:

(1) UserClassLoader1的parent ClassLoader设置为AppClassLoader

(2) 其次UserClassLoader1没有改动ClassLoader中loadClass()方法里双亲委派的逻辑。

我们下面来根据ClassLoader源码来说明这两个前提。

4 从ClassLoader源码分析双亲委派模型

双亲委派机制的核心逻辑在java.lang.ClassLoader#loadClass(java.lang.String, boolean)

/** 父ClassLoader */
private final ClassLoader parent;

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 步骤1. 首先,校验类是否已经加载过了,如果加载过,则直接读缓存返回。
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            // 步骤2 委派parent ClassLoader去加载
            try {
                // 2.1 如果parent ClassLoader不为空,则委派给parent ClassLoader去执行loadClass()方法加载类
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else { // 2.2 如果parent ClassLoader为空,例如ExtClassLoader的父ClassLoader就是为空,则此时parent ClassLoader就取BootstrapClassLoader。将委派给BootstrapClassLoader去加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) { // 如果抛出ClassNotFoundException,则说明类没有找到
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            // 步骤3. 如果通过上面的父类委派方式,还是没有找到。则调用findClass()方法去加载类
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);
                ..........
            }
        }
        ..........
        return c;
    }
}

// findClass()方法用以定义当前ClassLoader获取类的方式
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

从上面的源码,几处关键信息,

1.loadClass()中定义了双亲委派模型。

loadClass()定义了双亲委派模型,如果我们不想破坏双亲委派模型,则不建议覆写loadClass()方法,而是覆写findClass()方法自定义获取类的方式即可。

来个思考题,如果自定义的ClassLoader(类名为UserClassLoader2)覆写了loadClass()方法,并且不调用父类java.lang.ClassLoader#loadClass(java.lang.String, boolean),会发生什么?

答案是:

自定义的ClassLoader将不遵循双亲委派模型,因为不调用父类java.lang.ClassLoader#loadClass(java.lang.String, boolean)就没有委派父类加载类的过程了。此时自定义的ClassLoader会自己直接去加载对应类。

对应的加载流程为:

image-20221224224720577

2.ClassLoader的双亲委派机制,其实就是委派给parent ClassLoader去加载。

如果遵循双亲委派机制,那么当parent ClassLoader不为空,就委派给parent ClassLoader去执行loadClass()方法加载类;当parent ClassLoader为空,例如ExtClassLoader的父ClassLoader就是为空,则此时parent ClassLoader就取BootstrapClassLoader,将委派给BootstrapClassLoader去加载类。

来个思考题,如果自定义的ClassLoader(类名为UserClassLoader3)未覆写loadClass()方法,而是parent ClassLoader设置为ExtClassLoader,会发生什么?

答案是:

此时UserClassLoader3会遵循双亲委派机制,但是,UserClassLoader3加载类时,不会委派给AppClassLoader加载,而是先委派给ExtClassLoader。

对应的加载流程为:

image-20221224225410985

五、小结

本文讲解了ClassLoader的分类,ClassLoader加载类的过程,以及通过源码分析了双亲委派模型的原理和注意事项。

六、参考

《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》