本篇章源码基于 Java 8 版本,不同版本可能存在差异。🚀本篇章代码 Demo

# 类加载执行流程

  • 我们每编写一个 .java 文件的时候都储蓄着需要执行的程序与逻辑。
  • 这些 .java 文件经过 Java 编译器后会生成它对应的类加载文件也就是 .class 文件。
  • 此时 .class 文件中保存着 Java 代码转换后的虚拟机指令。
  • 当需要使用某个类时,虚拟机将会加载它的 .class 文件,并创建对应的 class 对象。
  • 并将 class 文件加载到虚拟机的内存中,这便是类加载的执行流程,流程图如下,其中验证、准备、解析统一归属于链接。
    piGHWl9.jpg
  • 加载:通过一个类的完全限定名查找此类的字节码文件,并利用字节码文件创建一个 Class 对象
  • 验证:确保 Class 文件的字节流中包含信息符合当前虚拟机的要求,并检测会不会对虚拟机产生危害,主要包括四种验证:
    • 文件格式验证
    • 元数据验证
    • 字节码验证
    • 符号引用验证
  • 准备:为类变量 (既 static 修饰的字段变量) 分配内存并设置该类变量的初始值为 0 (如: private static int num = 5 这里只是将 num 初始化为 0 ,至于 5 的值将会在初始化时赋值),这里不包含 final 修饰的 static ,因为 final 在编译的时候就会分配了,值得注意的是:这里不会实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中。
  • 解析:将常量池中的符号引用替换为直接引用的过程,符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标地址,相对偏移量或一个间接定位到目标的句柄,如:
    • 类解析
    • 接口解析
    • 字段解析
    • 类方法解析
    • 接口方法解析
  • 初始化:类加载的最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量,如:前面只初始化了默认值的 static ,变量将会在这个阶段进行赋值,成员变量也将会被初始化。
  • 这便是类加载的全部过程,而类加载器的任务是根据一个类的全限定名来读取此类二进制字节流到 JVM 中的,然后转换为一个与目标类对应的 java.lang.Class 对象实例。
  • 虚拟机提供了三种方式的类加载器,如下:

# 三种方式类加载器

# 引导 (Bootstrap) 类加载器

C/C++ 语言实现,启动类加载器,属于最高层, JVM 启动时创建,通常由于 os 相关的本地代码实现,是最根基的类加载器,没有对应的 Java 对象,因此在 Java 中只能用 null 来指代。

# Java8

  • 启动类加载器主要加载的是 JVM 自身需要的类,这个类加载使用 C++ 来实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib 路径下的核心类库或 -Xbootclasspath 参数指定的路径下的 jar 包加载到内存中,注意由于虚拟机是按照文件名识别加载 jar 包的,如果文件名不被虚拟机识别,即使将 jar 包放在 lib 目录下也没有任何作用,出于安全考虑 Bootstrap 启动类加载器只加载包名为 javajavaxsun 等开头的类。
  • 总结:
    • 引导类加载器使用 C/C++ 语言实现,嵌套在 JVM 内部。
    • 用于加载 Java 核心类库。
    • JVM 启动时创建,通常由于 os 相关的本地代码实现,是最根基的类加载器。
    • 没有对应的 Java 对象,因此在 Java 中只能用 null 来指代。
    • 不继承自 java.lang.ClassLoader ,没有父加载器。
    • 还可用于加载扩展类加载器和应用程序类加载器,并指定为它们的父类加载器。
    • 只加载包名为 java javax sun 开头的类文件。

JVM 启动时通过 Bootstrap ClassLoader 加载 rt.jar , 并初始化 sun.misc.Launcher 从而创建 Extension ClassLoaderApplication ClassLoader 的实例,下面代码是查看 Bootstrap ClassLoader 初始化了那些类库。

GetBootstrapClassLoader.java版本请选择JAVA 8
package top.rem.rain;
import sun.misc.Launcher;
import java.net.URL;
/**
 * @Author: LightRain
 * @Description: 查看引导类加载器到底加载了那些核心类库
 * @DateTime: 2023-12-03 00:21
 * @Version:1.0
 **/
public class GetBootstrapClassLoader {
  public static void main(String[] args) {
    URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
    for (URL urL : urLs) {
      System.out.println(urL.toExternalForm());
    }
  }
}
/*
  打印结果:
  file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/resources.jar
  file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/rt.jar
  file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/sunrsasign.jar
  file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/jsse.jar
  file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/jce.jar
  file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/charsets.jar
  file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/jfr.jar
  file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/classes
 */

# Java9

  • 自从 Java 9 引入了模块特性后,负责加载启动时的基础模块类有以下几种:
    • java.base
    • java.management
    • java.xml
  • 除了启动类加载器之外,其它的类加载器都是 java.lang.ClassLoader 的子类,因此有对应的 Java 对象。
  • 这些类加载器需要先由另一个类加载器,比如说启动类加载器,加载至 Java 虚拟机中方能执行类加载。

# 扩展 (Extension)& 平台 (Platform) 类加载器

# Java8 - 扩展 (Extension)

  • 扩展类加载器是 Sun 公司实现的,现已被 Oracle 收购, sun.misc.Launcher$ExtClassLoader 类由 Java 语言实现。
  • 它是 Launcher 的静态内部类,它负责加载 <JAVA_HOME>/lib/ext 目录下或者由系统变量 -Djava.ext.dirs 路径中的类库。
  • 负责加载一些扩展的系统类,如: Xml 加密 压缩 相关的功能类等。
  • 可以供开发者直接使用标准扩展类加载器来加载。
Launcher.java参考JAVA 8
package sun.misc;
/**
 * This class is used by the system to launch the main application.
 */
public class Launcher {
  static class ExtClassLoader extends URLClassLoader {
    //ExtClassLoader 类中获取路径的代码
    private static File[] getExtDirs() {
      // 加载 & lt;JAVA_HOME>/lib/ext 目录中的类库
      String s = System.getProperty("java.ext.dirs");
      File[] dirs;
      if (s != null) {
        StringTokenizer st =
                new StringTokenizer(s, File.pathSeparator);
        int count = st.countTokens();
        dirs = new File[count];
        for (int i = 0; i < count; i++) {
          dirs[i] = new File(st.nextToken());
        }
      } else {
        dirs = new File[0];
      }
      return dirs;
    }
  }
}

# Java9 - 平台 (Platform)

Java 9 替换为了平台类加载器,为何要替换? Java 8 主要加载 jre/lib 下的 ext 扩展 jar 包时使用,这样的操作并不推荐,而 Java 9 更新了模块化就更无需这样扩展的类加载器了。

  • 只要负责加载一些相对次要,但又通用的类。
  • 加载一些平台相关模块如:
    • java.scripting
    • java.compiler.*
    • java.corba.*

# 系统 (System) 类加载器 & 应用 (App) 类加载器

  • 系统类加载器 也被称为 应用程序加载器Sun 公司实现的 sun.misc.Launcher$AppClassLoader
  • 它负责加载系统类路径 java -classpath-D java.class.path 指定路径下的类库,也就是我们经常用到的 classpath 路径。
  • 开发者可以直接使用 系统类加载器 ,一般情况下该类加载是程序中默认的类加载器,通过 ClassLoader.getSystemClassLoader() 方法可以获取到该类加载器。
  • Java 日常应用程序开发中,类的加载几乎由上述 3 种类加载器相互配合执行的,在必要时我们还可以自定义类加载器,需要注意的是 Java 虚拟机对 class 文件采用的是按需加载的方式。
  • 就是说当需要使用该类时才会将它的 class 文件加载到内存中生成主要分为以下 class 对象,而且加载某个类的 class 文件时, Java 虚拟机采用的是 双亲委派模式 ,即把当前请求交由父类处理,它是一种任务委派模式。

# 双亲委派模式

# 双亲委派工作原理

双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器, 注意 :双亲委派模式中的父子关系并非通常的类继承关系,而是采用组合关系来复用父类加载器的相关代码。

piyddVf.jpg

  • 双亲委派模式是 Java 1.2 后引入的,其工作原理就是如果:一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,以此类推,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回。
  • 倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
  • 俗话说就是每个儿子都很懒,有活就先给父亲干,直到父亲说这事我也干不了时,儿子才会自己想办法去做。

# 双亲委派主要用途

  • 双亲委派的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层级关系,通过这种层级关系可以避免类的重复加载。
  • 当父类加载了该类时,其子类就没必要再加载了,其次考虑到安全因素 Java 核心 API 中定义类型不会被随意替换。
  • 假设通过网络传递了一个名为 java.lang.Integer 类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心 Java API 中发现这个名称的类已被加载,之后并不会重新加载传递过来的 java.lang.Integer 类,而是直接返回已加载过的 Integer.class 对象。
  • 这样做的原因还有一个就是防止核心 API 被随意篡改,如果:我们在 classpath 路径下自定义一个名为 java.lang.Student 类呢?该类在 java.lang 包下并不存在,经过双亲委派模式传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会被加载。
  • 这将会反向委托给子类加载器来加载,最终会通过系统类加载器加载该类,但这样做是不被允许的,因为 java.lang 是核心 API ,是需要访问权限的,强制加载将会抛出 java.lang.SecurityException: Prohibited package name: java.lang 的异常。

所以是无论如何都无法加载成功的,下面是用代码在 Java 中定义的类加载器和 双亲委派模式 的实现。

# 双亲委派关系图

piyBUq1.png

# ClassLoader 常用方法

# loadClass(Sting)

  • 该方法加载指定名称 (包括包名) 的二进制类型,该方法在 JDK 1.2 之后不在建议开发者重写但开发者可以直接调用该方法。
  • loadClass() 方法是 ClassLoader 类自己实现的,该方法中的逻辑就是 双亲委派模式 的实现。
  • 下面是 loadClass(String name, boolean resolve) 方法的源码,它是一个重载方法, resolve 参数代表是否生成 class 对象的同时进行解析相关操作。
ClassLoader.java参考JAVA 8
package java.lang;
public abstract class ClassLoader {
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先从缓存中查找类是否已被加载
            // First, check if the class has already been loaded -> 首先检查类是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 如果存在父类加载器优先由父类加载器加载类
                    if (parent != null) {
                        // 调用父类加载器的 loadClass 方法加载类
                        c = parent.loadClass(name, false);
                    } else {
                        // 如果没有父类则委托给引导类加载器去加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果抛出该异常说明父类加载器无法加载类
                    // ClassNotFoundException thrown if class not found -> 如果找不到类则抛出 ClassNotFoundException
                    //from the non-null parent class loader -> 从非 null 父类加载器
                }
                if (c == null) {
                    // If still not found, then invoke findClass in order -> 如果仍未找到,则按顺序调用 findClass
                    //to find the class -> 找到 class
                    long t1 = System.nanoTime();
                    // 如果都没有找到则通过自定义实现的 findClass 去查找并加载
                    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;
        }
    }
}
  • 当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,不存在则交给该类加载器的父类加载器去加载。
  • 若没有父类加载器则交给顶级启动类加载器去加载,最后倘若扔没有找到,则使用 findClass() 方法去加载。

loadClass 实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,我们可以直接使用 this.getClass().getClassLoader().loadClass('className') 这样就可以直接调用 ClassLoaderloadClass 方法获取到 class 对象了。

# findClass(Sting)

  • JDK 1.2 之前,在自定义类加载时,总会去继承 ClassLoader 类并重写 loadClass 方法,从而实现自定义的类加载。
  • JDK 1.2 之后不再建议调用者去覆盖 loadClass() 方法,而是建议把自定义的类加载逻辑写在 findClass() 方法中。
  • findClass() 方法是在 loadClass() 方法中被调用的,当 loadClass() 方法中父类加载器加载失败后,则会调用自己的 findClass() 方法来完成类加载。
  • 这样就可以保证自定义的类加载器也符合 双亲委派模式 了,但需要注意的是 ClassLoader 类中并没有实现 findClass() 方法的具体逻辑,取而代之的是抛出 ClassNotFoundException 异常。
  • 同时应该知道的是 findClass() 方法通常是和 defineClass() 方法一起使用的。
  • ClassLoader 类中的 findClass() 方法源码如下:
ClassLoader.java参考JAVA 8
package java.lang;
public abstract class ClassLoader {
  /**
   * 实现自定义类的加载器需要子类重写此方法
   * 默认抛出 ClassNotFoundException
   */
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

# defineClass(byte[] b, int off, int len)

  • defineClass() 方法是用来将 byte 字节流解析成 JVM 能够识别的 Class 对象 ( ClassLoader 中已实现该方法逻辑)
  • 通过这个方法不仅能够通过 class 文件实例化 class 对象,还可以通过其它方式实例化 class 对象,如:通过网络接收一个类的字节码,然后转换为 byte 字节流创建对应的 class 对象。
  • defineClass() 方法通常与 findClass() 方法一起使用,一般情况下,在自定义类加载器时会直接覆盖 ClassLoaderfindClass() 方法并编写加载规则。
  • 最后取得要加载类的字节码后转换成流,然后调用 defineClass() 方法生成类的 class 对象,下面是简单案例代码。
OverrideFindClass.java版本请选择JAVA 8
package top.rem.rain;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
/**
 * @Author: LightRain
 * @Description: 重写 FindClass 方法示例代码
 * @DateTime: 2023-12-03 00:13
 * @Version:1.0
 **/
public class OverrideFindClass extends ClassLoader{
  /**
   * 文件扩展名
   */
  private final static String FILE_EXTENSION = ".class";
  /**
   * .class 文件路径
   */
  private final String filePath;
  /**
   * 构造方法
   *
   * @param filePath .class 文件路径
   */
  public OverrideFindClass(String filePath) {
    this.filePath = filePath;
  }
  /**
   * 重写获取类的字节码并创建 class 对象的逻辑
   *
   * @param className 全限定类名称
   * @return Class<?> 任意 class 类型对象
   */
  @Override
  protected Class<?> findClass(String className) {
    // 获取字节码的字节数组
    byte[] data = this.getClassData(className);
    // 调用父类中的 defineClass 方法将字节流解析成 JVM 能够识别的 Class 对象并返回
    return this.defineClass(className, data, 0, data.length);
  }
  /**
   * 读取字节码流的方法
   *
   * @param className 对象全限定名称
   * @return byte [] 字节数组
   */
  private byte[] getClassData(String className) {
    // 创建 InputStream 对象获取字节输入流
    InputStream inputStream = null;
    // 定义一个字节数组
    byte[] data = null;
    // 创建 ByteArrayOutputStream 获取字节输出流
    ByteArrayOutputStream byteArrayOutputStream = null;
    try {
      // 将字符串中的 "." 全部替换为 “\\”
      // 获取全限定名称类路径如:top.rem.rain.NotEqualClassTest -> top\rem\rain\NotEqualClassTest
      className = className.replace('.', '\\');
      // 读取.class 文件到 InputStream 流中
      inputStream = Files.newInputStream(new File(filePath + className + FILE_EXTENSION).toPath());
      //new ByteArrayOutputStream 获取字节输出流对象
      byteArrayOutputStream = new ByteArrayOutputStream();
      int ch = 0;
      //inputStream 字节流读取到 - 1 为结束
      while (-1 != (ch = inputStream.read())) {
        // 将字节流中的数据写入到字节输出流中 inputStream -> byteArrayOutputStream
        byteArrayOutputStream.write(ch);
      }
      // 将字节输出流转换为字节数组并赋值到 data 中
      data = byteArrayOutputStream.toByteArray();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      try {
        assert inputStream != null;
        inputStream.close();
        assert byteArrayOutputStream != null;
        byteArrayOutputStream.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    return data;
  }
}

注意:如果直接调用 defineClass() 方法生成类的 class 对象,这个类的 class 对象并没有解析 (可以理解为链接阶段,解析是链接的最后一步),其解析操作需要等待初始化阶段进行。

# resolveClass(Class<?> c)

使用该方法可以使用类的 Class 对象创建完成并同时被解析,前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值,同时将字节码文件中的符号引用替换为直接引用。

上述 4 个方法都 ClassLoader 类中的比较重要的方法,也是我们可能会经常用到的方法。

# SercureClassLoader 扩展 - ClassLoader

# URLClassLoader&URLClassPath

  • 来看 SercureClassLoader 扩展了 ClassLoader 新增了几个与使用相关的代码源 (对代码源的的位置及证书的验证) 和权限定义类验证 (主要指对 class 源码的访问权限) 的方法。
  • 我们更多是与它的子类 URLClassLoader 有所关联, ClassLoader 是一个抽象类,很多方法是空方法并没有实现,如: findClass() findResource() 等方法。
  • URLClassLoader 类为这些方法提供了具体实现,新增了 URLClassPath 类协助取得 Class 字节码流等功能。
  • 在编写自定义类加载器时如果没有过于复杂的需求,可以直接继承 URLClassLoader 类,就可以避免自己编写 findClass() 方法及其获取字节码流的方法了。
    pisGWin.png
  • 从类图结构来看 URLClassLoader 中存在一个 URLClassPath 类,通过这个类就可以找到要加载的字节码流。
  • 就是说 URLCLassPath 类负责找到要加载的字节码后再读取字节流,最后通过 defineClass() 方法创建类的 class 对象。
  • URLClassLoader 类的结构图可以看出其构造方法都有一个必须传递的参数 URL[] ,该参数的元素是代表字节码文件的路径。
  • 在创建 URLClassLoader 对象时必须要指定这个类加载器要到哪个目录下寻找要加载的 class 文化。
  • 同时应该注意: URL[]URLCLassPath 类的必传参数,在创建 URLClassPath 对象时,会根据传递的 URL 数组中的路径判断是文件还是 jar 包。
  • 根据不同的路径创建 FileLoader 或者 JarLoader 或默认 Loader 类去加载相应路径下的 class 文件。
  • 而当 JVM 调用 findClass() 方法时就由 Loader FileLoader JarLoader 这三个加载器中的一个将 class 字节码流文件加载到内存中,最后利用字节码流创建类的 class 对象。
  • 注意事项: 如果我们在定义类加载器时选择继承 ClassLoader 类而非 URLClassLoader 时,必须自己实现 findClass() 方法的具体加载逻辑以及获取字节码流的逻辑。

了解完 URLClassLoader 后下面来看剩余的两个类加载器: SystemClassLoader(系统类加载器) 也被称为 AppClassLoader(应用程序加载器)ExtClassLoader(扩展类加载器) 扩展类加载器是 Java 8 中的, Java 9 之后将扩展改为了平台 PlatformClassLoader(平台类加载器) ,此处使用 Java 8 版本

# App(System)ClassLoader&ExtClassLoader

  • 这两个类都继承自 URLClassLoader 类, ExtClassLoaderAppClassLoadersun.misc.Launcher 中的静态内部类。
  • sun.misc.Launcher 主要被系统用于启动主应用程序, ExtClassLoaderAppClassLoader 都是由 sun.misc.Launcher 创建的,来看结构图如下。
    pisNypn.png
    pisNRmT.png
  • 我们发现 ExtClassLoader 并没有重写 loadClass() 方法,这足矣证明其遵循 双亲委派模式
  • AppClassLoader 重写了 loadClass() 方法,但最终调用的还是父类的 loadClass() 方法,因此也遵循 双亲委派模式 ,重写方法源码如下。
Launcher.java参考JAVA 8
package sun.misc;
public class Launcher {
    /**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {
        /**
         * Override loadClass so we can checkPackageAccess.
         */
        public Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException {
            int i = name.lastIndexOf('.');
            if (i != -1) {
                SecurityManager sm = System.getSecurityManager();
                if (sm != null) {
                    sm.checkPackageAccess(name.substring(0, i));
                }
            }
            if (ucp.knownToNotExist(name)) {
                // The class of the given name is not found in the parent
                // class loader as well as its local URLClassPath.
                // Check if this class has already been defined dynamically;
                // if so, return the loaded class; otherwise, skip the parent
                // delegation and findClass.
                Class<?> c = findLoadedClass(name);
                if (c != null) {
                    if (resolve) {
                        resolveClass(c);
                    }
                    return c;
                }
                throw new ClassNotFoundException(name);
            }
            // 此处返回依旧调用的是父类中的 loadClass 方法
            return (super.loadClass(name, resolve));
        }
    }
}
  • 无论是 ExtClassLoader 还是 AppClassLoader 都继承自 URLClassLoader 类,因此它们都遵守 双亲委派模式
  • 到此处我们对 CLassLoader URLClassLoader ExtClassLoader AppClassLoaderLauncher 类之间的关系有了比较清晰地了解。

下面我们将通过代码来阐明,上面的每个类加载器的父类到底是谁。

# 类加载器之间的关系

CustomClassLoader.java版本请选择JAVA 8
package top.rem.rain;
import java.io.*;
import java.nio.file.Files;
/**
 * @Author: LightRain
 * @Description: 自定义类加载器
 * @DateTime: 2023-12-02 15:22
 * @Version:1.0
 **/
public class CustomClassLoader extends ClassLoader {
  /**
   * 文件扩展名
   */
  private final static String FILE_EXTENSION = ".class";
  /**
   * .class 文件路径
   */
  private final String filePath;
  /**
   * 构造方法
   *
   * @param filePath .class 文件路径
   */
  public CustomClassLoader(String filePath) {
    this.filePath = filePath;
  }
  /**
   * 重写获取类的字节码并创建 class 对象的逻辑
   *
   * @param className 全限定类名称
   * @return Class<?> 任意 class 类型对象
   */
  @Override
  protected Class<?> findClass(String className) {
    // 获取字节码的字节数组
    byte[] data = this.getClassData(className);
    // 调用父类中的 defineClass 方法将字节流解析成 JVM 能够识别的 Class 对象并返回
    return this.defineClass(className, data, 0, data.length);
  }
  /**
   * 读取字节码流的方法
   *
   * @param className 对象全限定名称
   * @return byte [] 字节数组
   */
  private byte[] getClassData(String className) {
    // 创建 InputStream 对象获取字节输入流
    InputStream inputStream = null;
    // 定义一个字节数组
    byte[] data = null;
    // 创建 ByteArrayOutputStream 获取字节输出流
    ByteArrayOutputStream byteArrayOutputStream = null;
    try {
      // 将字符串中的 "." 全部替换为 “\\”
      // 获取全限定名称类路径如:top.rem.rain.NotEqualClassTest -> top\rem\rain\NotEqualClassTest
      className = className.replace('.', '\\');
      // 读取.class 文件到 InputStream 流中
      inputStream = Files.newInputStream(new File(filePath + className + FILE_EXTENSION).toPath());
      //new ByteArrayOutputStream 获取字节输出流对象
      byteArrayOutputStream = new ByteArrayOutputStream();
      int ch = 0;
      //inputStream 字节流读取到 - 1 为结束
      while (-1 != (ch = inputStream.read())) {
        // 将字节乳流中的数据写入到字节输出流中 inputStream -> byteArrayOutputStream
        byteArrayOutputStream.write(ch);
      }
      // 将字节输出流转换为字节数组并赋值到 data 中
      data = byteArrayOutputStream.toByteArray();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      try {
        assert inputStream != null;
        inputStream.close();
        assert byteArrayOutputStream != null;
        byteArrayOutputStream.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    return data;
  }
}
CustomClassLoaderTest.java版本请选择JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 自定义加载器测试类
 * @DateTime: 2023-12-02 15:32
 * @Version:1.0
 **/
public class CustomClassLoaderTest {
    public static void main(String[] args) {
        CustomClassLoader loader = new CustomClassLoader(CustomClassLoaderTest.class.getName());
        System.out.println("自定义类加载器的父加载器: "+loader.getParent());
        System.out.println("系统默认的AppClassLoader: "+ClassLoader.getSystemClassLoader());
        System.out.println("AppClassLoader的父类加载器: "+ClassLoader.getSystemClassLoader().getParent());
        System.out.println("ExtClassLoader的父类加载器: "+ClassLoader.getSystemClassLoader().getParent().getParent());
        /*
          执行结果:
            自定义类加载器的父加载器: sun.misc.Launcher$AppClassLoader@18b4aac2
            系统默认的 AppClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
            AppClassLoader 的父类加载器: sun.misc.Launcher$ExtClassLoader@4554617c
            ExtClassLoader 的父类加载器: null
         */
    }
}
  • 在上述代码中我们自定义了一个 CustomClassLoader 并继承了 ClassLoader 类,而非继承自 URLClassLoader
  • 因此需要自己编写 findClass() 方法的逻辑以及加载字节码逻辑,在这里我们仅需要知道 CustomClassLoader 是自定义加载器即可。
  • 接着在 main 方法之后通过 ClassLoader.getSystemClassLoader() 方法获取到系统默认类加载器,通过获取其父类加载器及父类的父类加载器,同时还获取了自定义类加载器的父类加载器,打印结果正如我们所预料的。
  • AppClassLoader 的父类加载器为 ExtClassLoader ,而 ExtClassLoader 并没有父类加载器。
  • 自定义类加载器默认情况下它的父类加载器都是 AppClassLoader ,我们来看 Lancher 构造器的源码。
Launcher.java参考JAVA 8
package sun.misc;
public class Launcher {
    
    public Launcher() {
        // Create the extension class loader -> 创建扩展类加载器
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            // 无法创建扩展类加载器将抛出此异常
            throw new InternalError(
                "Could not create extension class loader", e);
        }
        
        // Now create the class loader to use to launch the application -> 现在创建用于启动应用程序的类加载器
        try {
            // 在创建 AppClassLoader 时并把 extcl 作为父类加载器传递给 AppClassLoader
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }
        // 设置线程上下文类加载器,后面分析
        // Also set the context class loader for the primordial thread -> 还要为原始线程设置上下文类加载器
        Thread.currentThread().setContextClassLoader(loader);
        // Finally, install a security manager if requested -> 最后如果需要安装安全管理器
        String s = System.getProperty("java.security.manager");
        if (s != null) {
            //init FileSystem machinery before SecurityManager installation -> 安装 SecurityManager 之前的 init 文件系统机制
            sun.nio.fs.DefaultFileSystemProvider.create();
            SecurityManager sm = null;
            if ("".equals(s) || "default".equals(s)) {
                sm = new java.lang.SecurityManager();
            } else {
                try {
                    sm = (SecurityManager)loader.loadClass(s).newInstance();
                } catch (IllegalAccessException e) {
                } catch (InstantiationException e) {
                } catch (ClassNotFoundException e) {
                } catch (ClassCastException e) {
                }
            }
            if (sm != null) {
                System.setSecurityManager(sm);
            } else {
                throw new InternalError(
                    "Could not create SecurityManager: " + s);
            }
        }
    }
}
  • Lancher 初始化时首先会创建 ExtClassLoader 类加载器,然后再创建 AppClassLoader 并把 ExtClassLoader 传递给 AppClassLoader 作为父类加载器。
  • 后面还把 AppClassLoader 默认设置为线程上下文加载器,关于线程上下文类加载器后面分析。
  • 最后就是 ExtClassLoader 类加载器为什么是 null ??? 来看下面的源码中的创建过程就会明白了。
  • 在创建 ExtClassLoader 时强制设置了其父类加载器为 null
Launcher.java 参考JAVA 8
package sun.misc;
public class Launcher {
    public Launcher() {
        ClassLoader extcl;
        try {
            // 1. 创建 ExtClassLoader
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError("Could not create extension class loader", e);
        }
        // 省略此处其它没必要的代码...
    }
    /*
     * ExtClassLoader 继承自 URLClassLoader
     * 用于加载已安装扩展的类加载器
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {
        // 省略此处其它没必要的代码...
        public static ExtClassLoader getExtClassLoader() throws IOException {
            if (instance == null) {
                synchronized (ExtClassLoader.class) {
                    if (instance == null) {
                        // 2. 继续跟踪 createExtClassLoader 方法
                        instance = createExtClassLoader();
                    }
                }
            }
            // 5. 然后返回出来的 ExtClassLoader 类中的父加载器就是 null
            return instance;
        }
        private static ExtClassLoader createExtClassLoader() throws IOException {
            try {
                return AccessController.doPrivileged(
                        new PrivilegedExceptionAction<ExtClassLoader>() {
                            public ExtClassLoader run() throws IOException {
                                final File[] dirs = getExtDirs();
                                int len = dirs.length;
                                for (int i = 0; i < len; i++) {
                                    MetaIndex.registerDirectory(dirs[i]);
                                }
                                // 3. 此处返回创建了一个 ExtClassLoader 对象,继续跟踪
                                return new ExtClassLoader(dirs);
                            }
                        });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }
        /*
         * 为指定的目录创建一个新的 ExtClassLoader。
         * Creates a new ExtClassLoader for the specified directories.
         */
        public ExtClassLoader(File[] dirs) throws IOException {
            // 4. 此处调用父类构造 URLClassLoader 传递 null 作为 parent
            super(getExtURLs(dirs), null, factory);
            SharedSecrets.getJavaNetAccess().
                    getURLClassPath(this).initLookupCache(this);
        }
    }
}

上述 super 跟入进去后便是下面的 URLClassLoader 构造器源码。

URLClassLoader.java 参考JAVA 8
package java.net;
public class URLClassLoader extends SecureClassLoader implements Closeable {
    
    public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        acc = AccessController.getContext();
        ucp = new URLClassPath(urls, factory, acc);
    }
}
  • 仔细分析后很显然 ExtClassLoaer 的父类为 null ,而 AppClassLoader 的父加载器为 ExtClassLoader
  • 所有自定义类加载器默认情况下其父加载器都是 AppClassLoader注意:这里所指的父类加载器并不是 Java 继承关系中的那种父子关系。

# 类与类加载器

  • JVM 中表示两个 class 对象是否为同一个类对象时,存在两个必要条件:
    • 类的全限定名称需要保持一致。
    • 加载这个类的 ClassLoader 实例对象必须相同。
  • 就是说在 JVM 中即使这两个类对象 ( class 对象) 来源于同一个 Class 文件,被同一个虚拟机所加载,但只要加载它们的 ClassLoader 实例对象不同,那么这两个类对象也是不相等的。
  • 这是因为不同的 ClassLoader 实例对象都拥有不同的独立类名空间,所以加载的 class 对象也会存在不同的类名空间中,注意:前提是需要重写 loadClass() 方法
  • 就从前面的 双亲委派模式loadClass() 方法源码分析中可得知,在方法第一步会通过 Class<?> c = findLoadedClass(name) 从缓存中查找,类的完全限定名相同则不会再一次被加载。
  • 因此我们必须绕过缓存查询才能重新加载 class 对象,也可以直接调用 findClass() 方法,下面是避免从缓存中查找的代码示例如下。
NotEqualClassTest.java版本请选择JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 不相等对象测试
 * @DateTime: 2023-12-02 22:11
 * @Version:1.0
 **/
public class NotEqualClassTest {
    public static void main(String[] args){
        // 此处是.class 文件存在的项目的顶级目录路径
        String classPath = "D:\\项目\\gitee\\untitled\\out\\production\\untitled\\";
        // 创建两个不同的自定义类加载器
        CustomClassLoader classLoader1 = new CustomClassLoader(classPath);
        CustomClassLoader classLoader2 = new CustomClassLoader(classPath);
        // 通过 findClass 创建类的 Class 对象
        Class<?> aClass1 = classLoader1.findClass("top.rem.rain.NotEqualClassTest");
        Class<?> aClass2 = classLoader2.findClass("top.rem.rain.NotEqualClassTest");
        System.out.println("findClass -> aClass1:"+aClass1.hashCode());
        System.out.println("findClass -> aClass2:"+aClass2.hashCode());
        
        /*
          执行结果:
            findClass -> aClass1:1836019240
            findClass -> aClass2:325040804
            生成不同实例对象
         */
    }
}

如果调用父类的 loadClass() 方法,结果如下,除非重写 loadClass() 方法去掉缓存查找步骤,不过现在一般都不建议重写 loadClass() 方法。

EqualClassTest.java版本请选择JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 相等对象测试
 * @DateTime: 2023-12-03 00:55
 * @Version:1.0
 **/
public class EqualClassTest {
  public static void main(String[] args) throws ClassNotFoundException {
    // 此处是.class 文件存在的项目的顶级目录路径
    String classPath = "D:\\项目\\gitee\\untitled\\out\\production\\untitled\\";
    // 创建两个不同的自定义类加载器
    CustomClassLoader classLoader1 = new CustomClassLoader(classPath);
    CustomClassLoader classLoader2 = new CustomClassLoader(classPath);
    // 直接调用 loadClass 方法的输出结果,注意并没有重写 loadClass 方法
    Class<?> aClass1 = classLoader1.loadClass("top.rem.rain.EqualClassTest");
    Class<?> aClass2 = classLoader2.loadClass("top.rem.rain.EqualClassTest");
    System.out.println("findClass -> aClass1:" + aClass1.hashCode());
    System.out.println("findClass -> aClass2:" + aClass2.hashCode());
    System.out.println("Class -> aClass3:" + EqualClassTest.class.hashCode());
        /*
          直接调用 loadClass 方法的输出结果,注意并没有重写 loadClass 方法
          执行结果:
          findClass -> aClass1:685325104
          findClass -> aClass2:685325104
          Class -> aClass3:685325104
          生成的都是同一个实例对象
         */
  }
}

所以如果不想从缓存查询相同完全类名的 class 对象,那么只有 ClassLoader 的实例对象不同,同一个字节码文件创建的 class 对象自然也不会相同。

# class 文件显示 & 隐式加载概念

了解

  • 所谓的 class 文件的显示加载与隐式加载的方式是指 JVM 加载 class 文件到内存中的方式。
  • 显示加载指的是在代码中通过调用 ClassLoader 加载 class 对象,如直接调用 Class.forName(name)this.getClass().getClassLoader().loadClass() 加载 class 对象。
  • 隐式加载指的是不直接在代码中调用 ClassLoader 方法加载 class 对象,而是通过虚拟机自动加载到内存中,如在加载某个类的 class 文件时,该类的 class 文件中引用了另外一个类对象,此时额外引用的类将通过 JVM 自动加载到内存中。
  • 以上两种方式一般会混合使用。

# 实现自定义类加载器

  • 想实现自定义类加载器需要继承 ClassLoader 或者 URLClassLoader 类。
  • 继承 ClassLoader 则需要自己重写 findClass() 方法并编写加载逻辑。
  • 继承 URLClassLoader 则不用自己编写 findClass() 方法的加载逻辑以及 class 文件加载转换成字节码流的代码。
  • 为什么要编写自定义类加载器其意义何在?
    • class 文件不在 ClassPath 路径下,默认系统类加载器 (应用类加载器) 无法找到该 class 文件,在这种情况下我们需要自己实现一个自定义的 ClassLoader 来加载特定路径下的 class 文件生成 class 对象。
    • 当一个 class 文件是通过网络传输并且可能会进行相应加密操作时,需要先对 class 文件进行相应的解密后再加载到 JVM 内存中,这种情况下也需要自己编写自定义的 ClassLoader 并实现相应的逻辑。
    • 当需要实现热部署功能时 (一个 class 文件通过不同的类加载器产生不同 class 对象从而实现热部署功能) 也想要自己实现自定义 ClassLoader 的逻辑。

# 自定义 File 类加载器

我们继承 ClassLoader 实现自定义的特定路径下的文件类加载器并加载编译后的 Student.class 源码如下。

Student.java版本请选择JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 学生类对象
 * @DateTime: 2023-12-03 12:42
 * @Version:1.0
 **/
public class Student {
  @Override
  public String toString() {
    return "top.rem.rain.Student{ " +
            "name=" + "LightRain" +
            " age=" + "17" +
            " sex=" + "woman " +
            '}';
  }
}
FileClassLoader.java版本请选择JAVA 8
package top.rem.rain;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
/**
 * @Author: LightRain
 * @Description: 自定义 FileClassLoader
 * @DateTime: 2023-12-03 12:50
 * @Version:1.0
 **/
public class FileClassLoader extends ClassLoader{
    /**
     * 文件扩展名
     */
    private final static String FILE_EXTENSION = ".class";
    /**
     * .class 文件路径
     */
    private final String filePath;
    public FileClassLoader(String filePath) {
        this.filePath = filePath;
    }
    @Override
    protected Class<?> findClass(String className){
        // 获取字节码的字节数组
        byte[] data = this.getClassData(className);
        // 调用父类中的 defineClass 方法将字节流解析成 JVM 能够识别的 Class 对象并返回
        return this.defineClass(className, data, 0, data.length);
    }
    /**
     * 读取字节码流的方法
     *
     * @param className 对象全限定名称
     * @return byte [] 字节数组
     */
    private byte[] getClassData(String className) {
        // 创建 InputStream 对象获取字节输入流
        InputStream inputStream = null;
        // 定义一个字节数组
        byte[] data = null;
        // 创建 ByteArrayOutputStream 获取字节输出流
        ByteArrayOutputStream byteArrayOutputStream = null;
        try {
            // 将字符串中的 "." 全部替换为 “\\”
            // 获取全限定名称类路径如:top.rem.rain.NotEqualClassTest -> top\rem\rain\NotEqualClassTest
            className = className.replace('.', '\\');
            // 读取.class 文件到 InputStream 流中
            inputStream = Files.newInputStream(new File(filePath + className + FILE_EXTENSION).toPath());
            //new ByteArrayOutputStream 获取字节输出流对象
            byteArrayOutputStream = new ByteArrayOutputStream();
            int ch = 0;
            //inputStream 字节流读取到 - 1 为结束
            while (-1 != (ch = inputStream.read())) {
                // 将字节乳流中的数据写入到字节输出流中 inputStream -> byteArrayOutputStream
                byteArrayOutputStream.write(ch);
            }
            // 将字节输出流转换为字节数组并赋值到 data 中
            data = byteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                assert inputStream != null;
                inputStream.close();
                assert byteArrayOutputStream != null;
                byteArrayOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }
}
FileClassLoaderTest.java版本请选择JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: FileClassLoader 测试
 * @DateTime: 2023-12-03 12:56
 * @Version:1.0
 **/
public class FileClassLoaderTest {
    public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        String filePath  = "D:\\项目\\gitee\\untitled\\out\\production\\untitled\\";
        // 创建自定义文件类加载器
        FileClassLoader fileClassLoader = new FileClassLoader(filePath);
        // 加载指定的 class 文件
        Class<?> aClass = fileClassLoader.loadClass("top.rem.rain.Student");
        System.out.println("aClass.newInstance().toString() = " + aClass.newInstance().toString());
        /*
          执行结果:aClass.newInstance ().toString () = top.rem.rain.Student { name=LightRain age=17 sex=woman }
         */
    }
}
  • 我们通过 getClassData() 方法找到了 class 文件并转换为字节流重写了 findClass() 方法,利用 defineClass() 方法创建了类的 class 对象。
  • main 方法中调用 loadClass() 方法加载指定路径下的 class 文件,由于启动类加载器、扩展类加载器以及系统类加载器都无法在其路径下找到该类,因此最终将由自定义类加载器加载,即调用 findClass() 方法进行加载。

如果继承 URLClassLoader 类来实现自定义类加载器,代码将会更加简洁,代码如下。

FileUrlClassLoader.java版本请选择JAVA 8
package top.rem.rain;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandlerFactory;
/**
 * @Author: LightRain
 * @Description: 继承自 URLClassLoader 的自定义类加载器
 * @DateTime: 2023-12-03 14:37
 * @Version:1.0
 **/
public class FileUrlClassLoader extends URLClassLoader {
    public FileUrlClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }
    public FileUrlClassLoader(URL[] urls) {
        super(urls);
    }
    public FileUrlClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(urls, parent, factory);
    }
}
FileUrlClassLoaderTest.java版本请选择JAVA 8
package top.rem.rain;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
/**
 * @Author: LightRain
 * @Description: FileUrlClassLoader 测试
 * @DateTime: 2023-12-03 14:38
 * @Version:1.0
 **/
public class FileUrlClassLoaderTest {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        String filePath  = "D:\\项目\\gitee\\untitled\\out\\production\\untitled\\";
        // 创建自定义文件类加载器
        File file = new File(filePath);
        FileUrlClassLoader fileUrlClassLoader = new FileUrlClassLoader(new URL[]{file.toURI().toURL()});
        // 加载指定的 class 文件
        Class<?> aClass = fileUrlClassLoader.loadClass("top.rem.rain.Student");
        System.out.println("aClass.newInstance().toString() = " + aClass.newInstance().toString());
        
         /*
          执行结果:aClass.newInstance ().toString () = top.rem.rain.Student { name=LightRain age=17 sex=woman }
         */
    }
}

除了需要重写构造器外无需编写 findClass() 方法及其 class 文件字节码流的转换逻辑。

# 自定义网络类加载器

自定义网络类加载器,主要用于读取通过网络传递的 class 文件

NetClassLoader.java版本请选择JAVA 8
package top.rem.rain;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
/**
 * @Author: LightRain
 * @Description: 自定义网络类加载器
 * @DateTime: 2023-12-03 15:43
 * @Version:1.0
 **/
public class NetClassLoader extends ClassLoader{
    private String url;
    public NetClassLoader(String url) {
        this.url = url;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassDataFromNet(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }
    /**
     * 从网络获取 class 文件
     * @param className
     * @return
     */
    private byte[] getClassDataFromNet(String className) {
        String path = classNameToPath(className);
        try {
            URL url = new URL(path);
            ByteArrayOutputStream baos;
            try (InputStream ins = url.openStream()) {
                baos = new ByteArrayOutputStream();
                int bufferSize = 4096;
                byte[] buffer = new byte[bufferSize];
                int bytesNumRead = 0;
                // 读取类文件的字节
                while ((bytesNumRead = ins.read(buffer)) != -1) {
                    baos.write(buffer, 0, bytesNumRead);
                }
            }
            // 如果有加密操作需要在这里先进行解密
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    private String classNameToPath(String className) {
        // 得到类文件的 URL
        return url + "/" + className.replace('.', '/') + ".class";
    }
}
  • 主要是在获取字节码流时的区别,从网络直接获取到字节流再转成字节数组然后利用 defineClass() 方法创建 class 对象。
  • 如果继承自 URLClassLoader 类则和前面文件路径的实现是类似的,无需担心路径是 filePath 还是 url ,因为 URLClassLoader 内的 URLClassPath 对象会根据传递过来的 URL 数组中的路径去判断是文件还是 jar 包,然后根据不同的路径创建 FileLoaderJarLoader 或默认类 Loader 去读取对应的路径或 url 下的 class 文件。

# 实现热部署类加载器

  • 热部署就是利用同一个 class 文件,使用不同的类加载器在内存中创建出两个不同的 class 对象 (即使用不同的类加载器加载实例),由于 JVM 在加载类之前会检测请求的类是否已被加载过 (即在 loadClass() 方法中调用 findLoadedClass() 方法)。
  • 如果被加载过则直接从缓存获取,不会重新再加载, 注意: 同一个类加载器的实例和同一个 class 文件只能被加载器加载一次,多次加载将会报错。
  • 因此我们实现热部署必须让同一个 class 文件,可以根据不同的类加载器重复加载,以实现所谓的热部署。
  • 实际上前面实现的 FileClassLoaderFileUrlClassLoader 已具备这个功能了,但前提是直接调用 findClass() 方法,而不是调用 loadClass() 方法,因为我们重写了 findClass() 方法,在方法中我们并没有去查询缓存中是否已被加载。
  • 因此在 ClassLoader 中的 loadClass() 方法体中调用的 findLoadedClass() 方法进行了检测是否已被加载。
  • 因此我们直接调用 findClass() 方法就可以绕过这个问题,当然也可以重写 loadClass() ,只不过是不推荐这么干仅此而已。
  • 利用 FileClassLoader 类测试代码如下:
FileClassLoaderTest2.java版本请选择JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: FileClassLoader 测试 -- 热部署
 * @DateTime: 2023-12-03 12:56
 * @Version:1.0
 **/
public class FileClassLoaderTest2 {
    public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        String filePath  = "D:\\项目\\gitee\\untitled\\out\\production\\untitled\\";
        // 创建自定义文件类加载器
        FileClassLoader fileClassLoader1 = new FileClassLoader(filePath);
        FileClassLoader fileClassLoader2 = new FileClassLoader(filePath);
        // 加载指定的 class 文件,调用 loadClass ()
        Class<?> aClass1 = fileClassLoader1.loadClass("top.rem.rain.Student");
        Class<?> aClass2 = fileClassLoader2.loadClass("top.rem.rain.Student");
        System.out.println("loadClass -> aClass1:"+aClass1.hashCode());
        System.out.println("loadClass -> aClass2:"+aClass2.hashCode());
        System.out.println("--------------------------------");
        // 加载指定的 class 文件,直接调用 findClass (), 绕过检测机制,创建不同 class 对象。
        Class<?> aClass3 = fileClassLoader1.findClass("top.rem.rain.Student");
        Class<?> aClass4 = fileClassLoader2.findClass("top.rem.rain.Student");
        System.out.println("findClass -> aClass3:"+aClass3.hashCode());
        System.out.println("findClass -> aClass4:"+aClass4.hashCode());
        
        /*
          执行结果:
            loadClass -> aClass1:1163157884
            loadClass -> aClass2:1163157884
            --------------------------------
            findClass -> aClass3:325040804
            findClass -> aClass4:1173230247
         */
    }
}

# 线程上下文类加载器

线程上下文类加载器是双亲委派模式的破坏者

  • Java 应用中存在着很多服务提供者接口 ( Service Provider Interface 简称: SPI ),这些接口允许第三方为它们提供具体实现。
  • 常见的 SPIJDBCJNDI 等,这些 SPI 的接口属于 Java 核心库,一般存在 rt.jar 包中,由 Bootstrap 类加载器加载。
  • SPI 的第三方具体实现的代码则是作为 Java 应用所依赖的 jar 包被存放在 classpath 路径下。
  • 由于 SPI 接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但 SPI 的核心接口类是由引导类加载器来加载的。
  • Bootstrap 类加载器无法直接加载 SPI 的具体实现类,同时由于 双亲委派模式 的存在, Bootstrap 类加载器也无法反向委托给 AppClassLoader 加载器来加载 SPI 的具体实现类。
  • 这种情况下我们就需要一种特殊的类加载器来加载第三方类库,而线程上下文类加载器就是个很好的选择。
  • 线程上下文类加载器 ( ContextClassLoader ) 是从 Java 1.2 开始引入的,我们可以通过 java.lang.Thread 类中的 getContextClassLoader()setContextClassLoader(ClassLoader cl) 方法来获取和设置线程的上下文类加载器。
  • 如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是应用类加载器 ( AppClassLoader ),在线程中运行的代码可以通过此类加载器来加载类和资源,以 jdbc.jar 加载为例,如图所示。
    piyBG24.png
  • 从图中可以看出 rt.jar 核心包是由 Bootstrap 类加载器加载的,其内包含 SPI 核心接口类,由于 SPI 中的类经常需要调用外部实现类的方法,而 jdbc.jar 包含外部实现类 ( jdbc.jar 存在于 classpath 路径下) 无法通过 Bootstrap 类加载器加载,因此只能委托线程上下文类加载器把 jdbc.jar 中的实现类加载到内存以便 SPI 相关类使用。
  • 显然这种线程上下文类加载器的加载方式破坏了 双亲委派模式 ,它在执行过程中抛弃了 双亲委派模式 ,使程序可以逆向使用类加载器,当然这也使得 Java 类加载器变得更加灵活。

接下来看 DriverManager 类的源码, DriverManagerJava 核心 rt.jar 包中的类,该类用来管理不同数据库的实现驱动 (即 Driver ),它们都实现了 Java 核心包中的 java.sql.Driver 接口,如: mysql 驱动包中的 com.mysql.jdbc.Driver ,在这里只要看看如何加载外部实现类,在 DriverManager 初始化时将会执行如下代码。

DriverManager.java参考JAVA 8
package java.sql;
public class DriverManager {
    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    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() {
                // 加载外部的 Driver 的实现类
                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);
            }
        }
    }
}

DriverManager 类初始化执行了 loadInitialDrivers() 方法,在该方法中通过 ServiceLoader.load(Diver.class) 去加载外部实现的驱动类, ServiceLoader 类会去读取 mysqljdbc.jar 下的 META-INF 文件的内容如下:

pi6mUXj.png

com.mysql.jdbc.Driver 继承类如下:

Driver.java参考JAVA 8
package com.mysql.jdbc;
import java.sql.SQLException;
public class Driver extends com.mysql.cj.jdbc.Driver {
    public Driver() throws SQLException {
    }
    static {
        System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
    }
}

从注释中可以看出平常我们使用 com.mysql.jdbc.Driver 已经被丢弃了,取而代之的是 com.mysql.cj.jdbc.Driver ,也就是说官方不再建议我们使用如下代码注册 mysql 驱动。

RegisterDriver.java版本请选择JAVA 8
package top.rem.rain;
import java.sql.Connection;
import java.sql.SQLException;
/**
 * @Author: LightRain
 * @Description: 注册 MySQL 驱动
 * @DateTime: 2023-12-04 21:58
 * @Version:1.0
 **/
public class RegisterDriver {
  public Connection registerMysql() throws SQLException, ClassNotFoundException {
    // 不建议使用该方式注册驱动类
    Class.forName("com.mysql.jdbc.Driver");
    String url = "jdbc:mysql://localhost:3306/cckfs?characterEncoding=UTF-8";
    // 通过 java 库获取数据库连接
    Connection conn = java.sql.DriverManager.getConnection(url, "root", "123456");
    return conn;
  }
}

而是去掉注册步骤,如下即可。

RegisterDriver.java版本请选择JAVA 8
package top.rem.rain;
import java.sql.Connection;
import java.sql.SQLException;
/**
 * @Author: LightRain
 * @Description: 注册 MySQL 驱动
 * @DateTime: 2023-12-04 21:58
 * @Version:1.0
 **/
public class RegisterDriver {
  public Connection registerMysql() throws SQLException, ClassNotFoundException {
    // 不建议使用该方式注册驱动类
    String url = "jdbc:mysql://localhost:3306/cckfs?characterEncoding=UTF-8";
    // 通过 java 库获取数据库连接
    Connection conn = java.sql.DriverManager.getConnection(url, "root", "123456");
    return conn;
  }
}

这样 ServiceLoader 就会帮助我们处理一切,并最终通过 load() 方法加载,来看看具体方法实现。

ServiceLoader.java参考JAVA 8
package java.util;
public final class ServiceLoader<S> implements Iterable<S> {
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 通过线程上下文类加载器来加载
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
}
  • 很显然确实是通过线程上下文类加载器加载的,实际上核心包的 SPI 类对外部实现类的加载都是线程上下文类加载器执行的,通过这种方式实现了 Java 核心代码内部去调用外部实现类。
  • 我们知道线程上下文类加载器默认情况下就是 AppClassLoader ,那为什么不直接通过 getSystemClassLoader() 方法获取类加载器来加载 classpath 路径下的类呢?是可以的,但这种直接使用 getSystemClassLoader() 方法获取 AppClassLoader 加载类会有一个缺点,那就是代码部署到不同服务器上时会出现问题。
  • 如果代码部署到 Java Web 应用服务或者 EJB 之类的服务将会出现问题,以为这些服务器使用的线程上下文类加载器并非 AppClassLoader ,而是 Java Web 应用服务器自家的类加载器,类加载器不同,所以我们应该少用 getSystemClassLoader()
  • 总之不同服务使用的可能默认 ClassLoader 是不同的,但使用线程上下文类加载器总能获取到当前程序执行相同的 ClassLoader ,从而避免不必要的问题。