Java类加载器剖析


Java 的类加载作为核心特性之一拥有众多的特性,理解和掌握类的加载方式和机制能够更好的了解程序的执行流程,本篇文章将详细介绍 Java 的类加载机制与其相关必备知识。

下面就让我们直接开始吧

一、文件编译

在开始之前先介绍一下 Java 文件的基本编译与执行命令以供后续使用。

1. Class编译

(1) 文件编译

通过 javac 命令将 .java 文件编译为 .class 文件,默认输出到当前目录。

javac MyBean.java  
(2) 编码处理

Java 文件中包含中文等编码如注释等时,在编译时需要指定编码格式,否则将会编译异常。

javac -encoding utf-8 MyBean.java  
(3) 包名处理

默认 javac 命令编译的文件是不包含包路径的,通过参数 -d 指定编译生成的文件将在同级目录创建与包名同层级文件夹。

其中 . 表示保存至当前目录下,可根据需要指定目录。

javac -d . MyBean.java  
(4) 依赖处理

如需要的编译的类中引用了其它类,在编译时则需要通过 -cp 指定依赖类的 class 文件。

javac -cp ./Denpend.class -d . MyBean.java  
(5) 编译版本

当编程环境中存在多个 JDK 版本时,可以通过输入完整路径从而指定 JDK 编译版本。

"C:\Program Files\Java\jdk1.8.0_202\bin\javac" -d <target_path> MyBean.java  

2. 文件执行

(1) Class运行

Class 文件运行较为简单,直接通过 java + 文件名 即可,需要注意的是若文件时编译设置了包名则运行时同样需要指定包名。

# 运行不含包名 class 文件
java MyBean  

# 运行含包名 class 文件
java xyz.ibudai.MyBean  
(2) Jar运行

运行 Jar 文件与 运行 Class 文件类似,只需添加 -jar 参数即可。

java -jar jar-name.jar

二、加载机制

1. 加载过程

所谓类加载过程,简单一句话而言即将编译完成的 class 文件通过特定的方式载入 JVM 虚拟机内存中,之后应该就可以在虚拟机内存中进行读取。

类加载器将字节码文件加载到内存中主要经历三个阶段 加载 -> 连接 -> 实例化

注意的是 .class 并不是一次性全部加载到内存中,而是在 Java 应用程序需要的时候才会加载。也就是说当 JVM 请求一个类进行加载的时候,类加载器就会尝试查找定位这个类,当查找对应的类之后将他的完全限定类定义加载到运行时数据区中。

2. 加载阶段

主要负责查找并且加载类的二进制数据文件,其实就是 class 文件,简单而言即将编译后的 class 文件以二进制的形式加载进 JVM 内存中。

3. 连接阶段

主要是确保类文件的正确性,比如 class 的版本,class 文件的魔术因子是否正确。

(Ⅰ) 验证(文件)

  • 验证文件格式,是否以魔数 0xCAFEBABE 开头。
  • 主、次版本号是否在当前 Java 虚拟机接受范围之内。
  • 常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)。
  • 文件中各个部分及文件本身是否有被删除的或附加的其他信息。

(Ⅱ) 验证(元数据)

  • 类是否有父类(除了 java.lang.Object 之外,所有的类都应当有父类)。
  • 类的父类是否继承了不允许被继承的类(被final修饰的类)。
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。

(Ⅲ) 验证(字节码)

  • 通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。

(Ⅳ) 准备

  • 准备阶段是正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段。
  • 当前阶段在进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。

(Ⅴ) 解析

解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程。

4. 初始化阶段

准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由 Java 虚拟机来主导控制。

如定义的 static int i = 123; 等类变量在准备阶段已经完成初始化,而类的普通成员变量如 int j; 仍未进行初始化,因此经过初始化阶段之后 int j; 则会初始化为 int j = 0;,类中其它普通成员变量同理。

三、类加载器

1. 基本类别

Java 中类加载器分为以下四类,各类加载器说明如下:

加载器 描述
BootstrapClassLoader 顶层加载器,主要负责加载核心的类库(java.lang.*等)。
ExtClassLoader 主要负责加载 jre/lib/ext 目录下的一些扩展的 JAR 包。
AppClassLoader 主要负责加载应用程序的主函数 main 等。
CustomClassLoader 自定义类加载器,即我们继承 ClassLoad 而自定义加载器,属于最底层加载器。

2. 命名空间

每一个类加载器实例都有各自的命名空间,命名空间是由该加载器及其所有父加载器所构成的,因此在每个类加载器中同一个 class 都是独一无二的。

在类加载器进行类加载的时候,首先会到加载记录表也就是缓存中,查看该类是否已经被加载过了,如果已经被加载过了就不会重复加载,否则将会认为其是首次加载。但是使用不同的类加载器,或者同一个类加载器的不同实例,去加载同一个 class 则会在堆内存和方法区产生多个 class 的对象。

3. 运行时包

JVM 运行时 class 会有一个运行时包,运行时的包是由类加载器的命名空间和类的全限定名称共同组成的,在判断类的属性或方法等作用域是也是基于运行时包进行的。

例如由系统类加载加载的 com.example.Test 类其对应的运行时包则为:

BootstrapclassLoader.ExtclassLoader.Appclassloader.com.example.Test

4. 初始类加载器

根据 JVM 规范的规定,在类的加载过程中,所有参与的类加载器即使没有亲自加载过该类也都会被标识为该类的初始类加载器。

在基于双亲委派的机制下,类依次经过了系统类加载器、扩展类加载器、根类加载器,则这些加载器都称为被加载类的初始类加载器,JVM 会在每一个类加载器维护的列表中添加该 class 类型。

String 类为例,根据双亲委派机制其依次经过了三个类加载器,因此对应的初始类加载如下:

5. 加载类型

类加载器负责从不同的源(例如文件系统、网络、内存等)加载类的字节码,如最常见的 .class 文件,将其转换为 Java 对象使得程序可以使用这些类。

类加载器同时还负责解析类的依赖关系,即查找并加载被当前类所依赖的其他类,以及确定每个类应该由哪个类加载器来加载。

Java 类加载机制具有以下特点:

  • 懒加载: 只有在需要使用类时才会加载该类,以节省内存和加载时间。
  • 双亲委派: 类加载器会按照层次结构来加载类,即先委托父类加载器加载,如果父类加载器无法加载则再交给自己来加载。
  • 缓存机制: 已经加载过的类会被缓存,避免重复加载同一类。
  • 破坏双亲委派机制: 允许用户自定义类加载器来加载类,从而可以实现一些自定义的类加载策略,例如热部署、插件化等。

四、双亲委派

双亲委派机制是类加载中非常重要的一类加载机制,通过该机制保证了类的安全完整性。

默认 JDK 中使用的即双亲委派机制,你可以在 java.lang 包下找到 ClassLoader 类,下面就从源码的角度分析双亲委派机制。

1. 加载方式

Java 中加载类可以共有 Class.forName()Classloader.loadClass() 两种方式,下面分别进行介绍。

(1) Class.forName()

通过 Class.forName() 方式加载不仅会将当前类加载进内存并且会初始化对象,可在需被加载的类中添加 static 静态块打印提示,当使用 Class.forName() 加载类时可以发现静态块代码将会被执行,说明此时创建了对象。

若想实现类加载但不创建对象可通过 Class.forName(className, false, classLoader) 方法加载,其中第二个参数用于指定是否创建对象,第三个参数指定类加载器。

public void initDemo1() {
    String className = "xyz.ibudai.bean.User";
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    try {
        Class<?> clazz1 = Class.forName(className);
        Class<?> clazz2 = Class.forName(className, false, classLoader);
        System.out.println("clazz1 loader: " + clazz1.getClassLoader());
        System.out.println("clazz2 loader: " + clazz2.getClassLoader());
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}
(2) Classloader.loadClass()

通过 loadClass() 方式加载并不会解析类只实现加载并不会创建相应对象,只有当对象被引用时才会初始化。

public void initDemo2() {
    String className = "xyz.ibudai.bean.User";
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    try {
        Class<?> clazz3 = classLoader.loadClass(className);
        System.out.println("clazz3 loader: " + clazz3.getClassLoader());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

需要注意一点 java.lang.String 等核心类库是在 JVM 启动时加载的,它们位于 Java 虚拟机的核心类库中,并由 Bootstrap ClassLoader 加载。因此在获取这些类的类加载器时,结果通常为 null ,需要注意的是 null 表示类加载器未知或者无法确定,而非没有类加载器。

2. 方法描述

在解析核心源码之前先过一遍下面这个表格,其中各方法在后面将会涉及到,在此简单描述其作用功能,后续不作详细描述。

方法 作用
getClassLoadingLock() 防止类被同时加载。
findLoadedClass() 判断类是否加载过,若加载过则返回加载器否则返回 null。
findBootstrapClassOrNull() 由顶级加载器加载类,代码由 C++ 实现,加载失败返回 null。
findClass() 根据完整类名读取 .class 文件并调用 defineClass() 查找对应的 java.lang.Class 对象。
defineClass() 将 byte 字节流转换为 java.lang.Class 对象,字节流可以来源 .class 文件等途径。
resolveClass() 在类加载过程中字节码文件被加载到 JVM 中并不会立即转换成 java.lang.Class 对象。只有在使用这个类时才会调用 resolveClass() 进行转化,才能够进行创建实例等操作。

3. 源码解读

在类加载实现中最为核心的即为 loadClass() 方法,其通过 synchronized 关键字保证类不会被重复加载,下面为 JDKloadClass() 的源码,在关键处我都提供了注释说明。

其中 parent 即为当前加载器的父级加载器。

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 判断类是否已经加载过
        Class<?> c = findLoadedClass(name);
        // 1.1 若不为空表明已加载则返回
        if (c == null) {
            try {
                // 2. 未加载 -> 判断是否存在父加载器
                if (parent != null) {
                    // 2.1 存在 -> 递归调用直至获取顶层父加载器
                    // (逐步向上委派到顶级类加载器进行类加载)
                    c = parent.loadClass(name);
                } else {
                    // 2.2 不存在(已达顶层加载器) -> 委托父类进行加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
            }

            // 3. 没有父加载器可以加载类 -> 由类自身实现加载
            if (c == null) {
                c = findClass(name);
            }
        }
        return c;
    }
}

五、自定义加载

自定义类加载器最常见的分为两种,遵循双亲委派机制与打破双亲委派机制,这里主要讨论后者。

根据上述的源码解析可以看出一个类的加载由 loadClass() 复制加载器的委派流向,而 findBootstrapClassOrNull()findClass() 负责具体的类文件加载逻辑。因此,若需要打破双亲委派机制,就需要重写 loadClass() 让其避免被顶级父加载器加载,同时需要重写 findClass() 如何将 .class 文件转为 JVM 可用的二进制数据。

1. 父类继承

新建自定义加载器类 CustomClassLoader 并继承 ClassLoader ,在类中定义构造方法与默认 class 文件存放路径等基本信息。构造函数中的 super()super(parent) 用于指定当前加载器的父级加载器,前者默认获取当前上下文的类加载器为父加载器,在绝大多数情况下为 AppClassLoader

在实际应用中通常只有特定类需要实现自定义加载,因此这里定义了集合 targetList 存放需要实现自定义加载的目标类,不在此集合中类则使用默认的加载方法。

public class CustomClassLoader extends ClassLoader {

    private final Path classDir;

    private static final Path DEFAULT_CLASS_DIR = Paths.get("E:\\Workspace\\Class");

    private List<String> targetList = new ArrayList<>();

    static {
        targetList.add("xyz.ibudai.bean.TestA");
        targetList.add("xyz.ibudai.bean.TestB");
    }

    /**
     * 使用默认的信息
     */
    public CustomClassLoader() {
        super();
        this.classDir = DEFAULT_CLASS_DIR;
    }

    /**
     * 指定文件目录与父类加载器
     */
    public CustomClassLoader(String path, ClassLoader parent) {
        super(parent);
        this.classDir = Paths.get(path);
    }
}

2. loadClass()

接下来也是自定义类加载的重点,在 JDK 中类的加载核心流程由 loadClass() 方法控制,想要打破默认的双亲委派机制就必须重写该方法。

重写的 loadClass() 其具象化的加载实现流程如下:

(Ⅰ) 检查当前类是否已经被加载过?

  • 若已加载,则返回结果。
  • 若未加载,则进入下一步。

(Ⅱ) 加载类是否在目标集合中?

  • 若是,则实现自定义 .class 文件读取加载。
  • 若否,则调用父加载器的 loadClass() 加载重试,即仍按双亲委派机制。

(Ⅲ) 判断自定义加载是否成功?

  • 若是,则返回结果。
  • 若否,则调用父加载器的 loadClass() 加载重试,即仍按双亲委派机制。

上述逻辑流程相对应的代码实现如下:

@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> c = findLoadedClass(name);
        if (c != null) {
            // 若已加载则返回
            return c;
        }
        if(targetList.contains(name)) {
            try {
                // 若在目标集合则自定义加载
                c = customFindClass(name);
            } catch (ClassNotFoundException ignored) {
                // 加载异常重新委派父类加载
                c = super.loadClass(name, resolve);
            }
        }
        if (c == null) {
            // 加载不成功重新委派父类加载
            c = super.loadClass(name, resolve);
        }
        return c;
    }
}

3. findClass()

在类的加载中 loadClass() 用于控制类的加载流程,而 findClass() 则用于控制具体的 .class 文件加载实现,即如何将 .class 装载进 JVM 中。这里我选择的是新建 customFindClass() 方法而并非重写 findClass() 目的在于防止当自定义类加载失败时重新调用父类的 loadClass() 时其在调用 findClass() 时发生混乱冲突。

customFindClass() 中实现了将编译后的类文件载入 JVM 中,即从本地读取编译后的 .class 文件转为字节数据并通过 defineClass() 查找对应的 java.lang.Class 对象。

其中 name 为类文件的完整类名,例如:xyz.ibudai.bean.TestA

private Class<?> customFindClass(String name) throws ClassNotFoundException {
    // 读取 class 的二进制数据
    byte[] classBytes = this.readClassBytes(name);
    if (classBytes == null || classBytes.length == 0) {
        throw new ClassNotFoundException("The class byte " + name + " is empty.");
    }
    // 调用 defineClass 方法定义 class
    return this.defineClass(name, classBytes, 0, classBytes.length);
}

/**
 * 将 class 文件转为字节数组以供后续内存载入
 */
private byte[] readClassBytes(String name) throws ClassNotFoundException {
    // 将包名分符转换为文件路径分隔符
    String classPath = name.replace(".", "/");
    Path classFullPath = classDir.resolve(Paths.get(classPath + ".class"));
    if (!classFullPath.toFile().exists()) {
        throw new ClassNotFoundException("Class file " + classFullPath + " doesn't exists.");
    }
    try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
        Files.copy(classFullPath, out);
        return out.toByteArray();
    } catch (IOException e) {
        throw new ClassNotFoundException("Read class " + classFullPath + " file to byte error.");
    }
}

4. 测试示例

完成上述的示例之后通过一个示例验证一下我们类加载效果。

新建测试类 TestA 并通过 java -d . TestA.java 命令将其编译为 .class 文件,将编译完成的带包名层级结构的 .class 文件移动至 CustomClassLoader 类中定义的默认目录下。

public class TestA {
    public void sayHello() {
        System.out.println("Hello world!");
    }
}

完成上述操作后编写相应的测试示例,分别通过默认的类加载与 CustomClassLoader 两种方式实现 TestA 类的加载,并分别输出了 TestA 的具体加载类信息。

相对应的测试示例代码如下:

@Test
public void loadTest() throws Exception {
    // 默认类加载器加载类
    Class<?> clazz1 = Class.forName("xyz.ibudai.bean.TestA");
    System.out.println("Loader-1: " + clazz1.getClassLoader());

    // 自定义类加载器加载类
    CustomClassLoader myLoader = new CustomClassLoader();
    Thread.currentThread().setContextClassLoader(myLoader);
    Class<?> clazz2 = myLoader.loadClass("xyz.ibudai.bean.TestA");
    System.out.println("Loader-2: " + clazz2.getClassLoader());
}

上述程序运行的结果如下,从结果可以看出通过 CustomClassLoader 我们成功实现了 TestA 的自定义加载。

Loader-1: sun.misc.Launcher$AppClassLoader@18b4aac2
Loader-2: xyz.ibudai.loader.CustomClassLoader@1b9e1916

六、动态加载

1. 基本介绍

Java 中实现类的动态加载或运行时加载通常有两种方式,第一种就是通过定义自己的类加载器打破双亲委派机制实现,第二种就是通过 JDK 中自带的 URLClassLoader 实现。

上面已经介绍了如何自定义加载器并打破双亲委派实现类的加载,下面着重介绍 URLClassLoader 如何实现类的动态加载。

2. 源码解读

URLClassLoaderJDK 中自带的动态类加载器,其提供在程序运行期间通过外部资源动态加载类文件的能力。

查看 URLClassLoader 的源码可看到其核心为继承 ClassLoader 并实现了 findClass()defineClass() 方法。同时我们可以看到其并没有重写 loadClass() 说明其委派机制仍是基于双亲委派实现,那它又是如何实现的呢?

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<>() {
                public Class<?> run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            // 调用 defineClass() 加载类
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        } catch (ClassFormatError e2) {
                            if (res.getDataError() != null) {
                                e2.addSuppressed(res.getDataError());
                            }
                            throw e2;
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

private Class<?> defineClass(String name, Resource res) throws IOException {
    // 略去代码
}

根据之前的介绍,双亲委派机制首先会通过层层委派到达顶级父加载器(通过为 Bootstrap ClassLoader),当到达顶级父加载器后,根据 loadClass() 代码可以得知其将会调用 findBootstrapClassOrNull() 尝试进行类加载。而 Bootstrap ClassLoader 将会从类空间中进行扫描类尝试读取加载,又因为 URLClassLoader 是通过外部资源进行的,很显然顶级父加载器无法在默认类空间中读取到对应的 .class 文件数据,因此 findBootstrapClassOrNull() 将会返回空。

同样,在上面解析双亲委派机制 loadClass() 源码时已经提到了当 findBootstrapClassOrNull() 结果为 null 时,将会调用自身的 findClass() 方法,这也是 URLClassLoader 为何通过重写 findClass()defineClass() 即实现了动态加载。

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name);
                } else {
                    // 双亲委派直至顶级 Bootstrap ClassLoader 尝试加载
                    // class文件为外部资源此时加载失败,c = null
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
            }

            // Bootstrap ClassLoader 尝试加载失败 c = null
            if (c == null) {
                // URLClassLoader 重写了 findClass()
                // 此处将执行 URLClassLoader#findClass() 从外部资源加载类
                c = findClass(name);
            }
        }
        return c;
    }
}

URLClassLoader 实现的不仅是动态加载,其进行加载时每个加载器都是由其自身的 loadClass() 加载实现,因此只需要通过定义不同的 URLClassLoader 即可同时实现类隔离,因为一个不同类加载器加载的类对应是不相同的,即可以理解为 JVM 对于一个类的定义是 加载器.类完整限定名,当加载器不同时即便类的完整限定名相同 JVM 仍认定其为不同的类。

但需要注意一点,在常用的 Maven 工程中,若在 pom.xml 文件中已经引入了 URLClassLoader 将要加载的类依赖时,根据双亲委派机制其最终的加载还是经由顶级父加载器的 findBootstrapClassOrNull() 实现,此时 URLClassLoader 的动态加载将失效,此类情景若想达到目的只能通过自定义类加载器破坏双亲委派机制实现。

3. 实现示例

基于上述理论,即可通过 URLClassLoader 实现从 URL 资源加载类,如下示例中即通过 URLClassLoader 加载 JDBC 驱动类。

public void loadDriver() {
    URL[] urls;
    try {
        String driverPath = "src/main/resources/mysql-driver.jar"
        File driver = new File(driverPath);
        // 驱动包不存在抛出异常
        if (!driver.exists()) {
            throw new FileNotFoundException();
        }
        // File 转 URL 资源格式
        list.add(driver.toURI().toURL());
        urls = list.toArray(new URL[0]);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

    ClassLoader parent = Thread.currentThread().getContextClassLoader();
    ClassLoader jarLoader = new URLClassLoader(urls, parent);
    try {
        // 加载驱动类
        String className = "com.mysql.jdbc.Driver"
        Class<Driver> c = jarLoader.loadClass(className);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

七、进阶操作

1. 类的卸载

Java 中并没有提供显式的方式实现类的卸载,想要卸载一个已经加载的类只能通过垃圾回收的方式实现。

一个类若想通过垃圾回收卸载则需要满足以下三个条件:

  • 当前类不存在任何实例,即所有对象实例都已被销毁。
  • 当前类不存在引用 (Reference)
  • 当前类的类加载已被垃圾回收 (GC)

2. 上下文加载器

在之前的示例中通过了 Thread.currentThread().getContextClassLoader() 获取了当前上下文的类加载,那么上下文类加载究竟有何用处?

Java 程序启动时会将核心类交由 Bootstrap Classloader 加载,但存在一部分服务提供者接口 (SPI) 只定义的规范接口,如最常见的 JDBC 其具体的实现都是由各大数据库厂商自行实现。而这部分接口实现类显然无法被系统类加载器加载(系统类加载只会加载核心包,不会加载第三方来源包),而默认的类加载机制 (双亲委派机制)并不支持父加载器向下委派,上下文加载器正是为此而生,通过子委派(由当前线程上下文加载器加载 SPI 实现类)的方式打破了双亲委派机制。

Java 应用运行时的初始线程的上下文类加载器是系统类加载器 (AppClassLoader),线程的上下文类加载器的获取与设置通过 Thread.currentThread().getContextClassLoader()Thread.currentThread().setContextClassLoader() 方法即可,一个线程若没有指定则默认继承其父线程上下文类加载器。

八、类隔离

在上面我们介绍了类的加载方式并且重点分析了双亲委派机制,下面介绍一下类加载的应用场景。

1. 基本介绍

与之前提到线程上下文加载器类似(子线程默认继承父线程加载器),一个类的引用类同样都将由该应用类的类加载器加载,即若在 ClassA 中引用了 ClassB ,则 ClassB 也将会由 ClassA 的类加载进行加载,这也正是能够类隔离的实现原理之一。

那么类隔离的作用是究竟是什么呢?正如上述所言,类的加载具有引用传递性,而一个模块中一个类只会加载一次,在类加载时通过 findLoadedClass() 判断是否加载,如是则跳过加载,也就是 JVM 虚拟机中每个类有且仅有一份。

2. 示例介绍

根据上述的介绍当程序需要依赖同一个类的多个版本时该方式将会产生大量的问题。

如工程中存在两个模块 module-amodule-b ,二者分别依赖了 module-c1.02.0 两个版本,但由于 module-amodule-b 同属一个工程都由 AppClassLoader 进行加载,最终在 JVM 仅会加载 module-c 的一个版本并非两个版本共同加载。

根据 Maven 中先定义先导入顺序,若 module-a 定义在前则最终加载的 module-c 则为 1.0 版本,此时若 module-b 引用了 2.0 中新特性在运行时则会抛出 ClassNotFoundException

Java 中同一个类由不同的类加载器加载对于 JVM 而言是两个不同的类,因此针对上述情况我们只要自定义类加载并将 module-amodule-b 分别通过两个不同的类加载(这里不同的类加载并不是指不同的类加载类别,只需由两个不同的实例对象即可)进行加载即可。如上述我们自定义实现了类加载 CustomClassLoader ,我们通过其两个实例 loader1loader2 分别对 module-amodule-b 进行加载类加载引用继承原理 module-c 将被加载分别加载两次,从而实现多版本的兼容隔离加载。

需要注意这里的 CustomClassLoader1CustomClassLoader2 所对应的自定义类加载器必须重写 loadClass() 方法并破坏其双亲委派机制,否则仍会逐层向上代理最终加载类的加载器都将为系统类加载器。


参考文档

  1. 《Java高并发编程详解-多线程与架构设计》

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录