计算机系统应用教程网站

网站首页 > 技术文章 正文

探秘类加载器和类加载机制

btikc 2024-09-08 11:58:27 技术文章 13 ℃ 0 评论

什么是类加载器

类加载器(ClassLoader)就是在系统运行过程中动态的将字节码文件加载到 JVM 中的工具,基于这个工具的整套类加载流程,我们称作类加载机制。我们在 IDE 中编写的都是源代码文件,以后缀名 .java 的文件形式存在于磁盘上,通过编译后生成后缀名 .class 的字节码文件,ClassLoader 加载的就是这些字节码文件。

有哪些类加载器

Java 默认提供了三个 ClassLoader,分别是 AppClassLoader、ExtClassLoader、BootStrapClassLoader,依次后者分别是前者的「父加载器」。父加载器不是「父类」,三者之间没有继承关系,只是因为类加载的流程使三者之间形成了父子关系,下文会详细讲述。

BootStrapClassLoader#

BootStrapClassLoader 也叫「根加载器」,它是脱离 Java 语言,使用 C/C++ 编写的类加载器,所以当你尝试使用 ExtClassLoader 的实例调用 getParent() 方法获取其父加载器时会得到一个 null 值。

根加载器会默认加载系统变量 sun.boot.class.path 指定的类库(jar 文件和 .class 文件),默认是 $JRE_HOME/lib 下的类库,如 rt.jar、resources.jar 等,具体可以输出该环境变量的值来查看。

除了加载这些默认的类库外,也可以使用 JVM 参数 -Xbootclasspath/a 来追加额外需要让根加载器加载的类库。比如我们自定义一个 com.ganpengyu.boot.DateUtils 类来让根加载器加载。

我们将其制作成一个名为 gpy-boot 的 jar 包放到 /Users/yu/Desktop/lib 下,然后写一个测试类去尝试加载 DateUtils。

运行这个测试类:

可以看到输出为 true,也就是说加载 com.enjoy.boot.DateUtils 的类加载器在 Java 中无法获得其引用,而任何类都必须通过类加载器加载才能被使用,所以推断出这个类是被 BootStrapClassLoader 加载的,也证明了 -Xbootclasspath/a 参数确实可以追加需要被根加载器额外加载的类库。

总之,对于 BootStrapClassLoader 这个根加载器我们需要知道三点:

  1. 根加载器使用 C/C++ 编写,我们无法在 Java 中获得其实例
  2. 根加载器默认加载系统变量 sun.boot.class.path 指定的类库
  3. 可以使用 -Xbootclasspath/a 参数追加根加载器的默认加载类库

ExtClassLoader#

ExtClassLoader 也叫「扩展类加载器」,它是一个使用 Java 实现的类加载器(sun.misc.Launcher.ExtClassLoader),用于加载系统所需要的扩展类库。默认加载系统变量 java.ext.dirs 指定位置下的类库,通常是 $JRE_HOME/lib/ext 目录下的类库。

我们可以在启动时修改java.ext.dirs 变量的值来修改扩展类加载器的默认类库加载目录,但通常并不建议这样做。如果我们真的有需要扩展类加载器在启动时加载的类库,可以将其放置在默认的加载目录下。总之,对于 ExtClassLoader 这个扩展类加载器我们需要知道两点:

  1. 扩展类加载器是使用 Java 实现的类加载器,我们可以在程序中获得它的实例并使用。
  2. 通常不建议修改java.ext.dirs 参数的值来修改默认加载目录,如有需要,可以将要加载的类库放到这个默认目录下。

AppClassLoader#

AppClassLoader 也叫「应用类加载器」,它和 ExtClassLoader 一样,也是使用 Java 实现的类加载器(sun.misc.Launcher.AppClassLoader)。它的作用是加载应用程序 classpath 下所有的类库。这是我们最常打交道的类加载器,我们在程序中调用的很多 getClassLoader() 方法返回的都是它的实例。在我们自定义类加载器时如果没有特别指定,那么我们自定义的类加载器的默认父加载器也是这个应用类加载器。总之,对于 AppClassLoader 这个应用类加载器我们需要知道两点:

  1. 应用类加载器是使用 Java 实现的类加载器,负责加载应用程序 classpath 下的类库。
  2. 应用类加载器是和我们最常打交道的类加载器。
  3. 没有特别指定的情况下,自定义类加载器的父加载器就是应用类加载器。

自定义类加载器#

除了上述三种 Java 默认提供的类加载器外,我们还可以通过继承 java.lang.ClassLoader 来自定义一个类加载器。如果在创建自定义类加载器时没有指定父加载器,那么默认使用 AppClassLoader 作为父加载器。

类加载器的启动顺序

上文已经提到过 BootStrapClassLoader 是一个使用 C/C++ 编写的类加载器,它已经嵌入到了 JVM 的内核之中。当 JVM 启动时,BootStrapClassLoader 也会随之启动并加载核心类库。当核心类库加载完成后,BootStrapClassLoader 会创建 ExtClassLoader 和 AppClassLoader 的实例,两个 Java 实现的类加载器将会加载自己负责路径下的类库,这个过程我们可以在 sun.misc.Launcher 中窥见。

ExtClassLoader 的创建过程#

我们将 Launcher 类的构造方法源码精简展示如下:

可以看到当 Launcher 被初始化时就会依次创建 ExtClassLoader 和 AppClassLoader。我们进入 getExtClassLoader() 方法并跟踪创建流程,发现这里又调用了 ExtClassLoader 的构造方法,在这个构造方法里调用了父类的构造方法,这便是 ExtClassLoader 创建的关键步骤,注意这里传入父类构造器的第二个参数为 null。接着我们去查看这个父类构造方法,它位于 java.net.URLClassLoader 类中:

URLClassLoader(URL[] urls, ClassLoader parent,
 URLStreamHandlerFactory factory)

通过这个构造方法的签名和注释我们可以明确的知道,第二个参数 parent 表示的是当前要创建的类加载器的父加载器。结合前面我们提到的 ExtClassLoader 的父加载器是 JVM 内核中 C/C++ 开发的 BootStrapClassLoader,且无法在 Java 中获得这个类加载器的引用,同时每个类加载器又必然有一个父加载器,我们可以反证出,ExtClassLoader 的父加载器就是 BootStrapClassLoader。

AppClassLoader 的创建过程#

理清了 ExtClassLoader 的创建过程,我们来看 AppClassLoader 的创建过程就清晰很多了。跟踪 getAppClassLoader() 方法的调用过程,可以看到这个方法本身将 ExtClassLoader 的实例作为参数传入,最后还是调用了 java.net.URLClassLoader 的构造方法,将 ExtClassLoader 的实例作为父构造器 parent 参数值传入。所以这里我们又可以确定,AppClassLoader 的父构造器就是 ExtClassLoader。

怎么加载一个类

将一个 .class 字节码文件加载到 JVM 中成为一个 java.lang.Class 实例需要加载这个类的类加载器及其所有的父级加载器共同参与完成,这主要是遵循「双亲委派原则」。

双亲委派#

当我们要加载一个应用程序 classpath 下的自定义类时,AppClassLoader 会首先查看自己是否已经加载过这个类,如果已经加载过则直接返回类的实例,否则将加载任务委托给自己的父加载器 ExtClassLoader。同样,ExtClassLoader 也会先查看自己是否已经加载过这个类,如果已经加载过则直接返回类的实例,否则将加载任务委托给自己的父加载器 BootStrapClassLoader。

BootStrapClassLoader 收到类加载任务时,会首先检查自己是否已经加载过这个类,如果已经加载则直接返回类的实例,否则在自己负责的加载路径下搜索这个类并尝试加载。如果找到了这个类,则执行加载任务并返回类实例,否则将加载任务交给 ExtClassLoader 去执行。

ExtClassLoader 同样也在自己负责的加载路径下搜索这个类并尝试加载。如果找到了这个类,则执行加载任务并返回类实例,否则将加载任务交给 AppClassLoader 去执行。

由于自己的父加载器 ExtClassLoader 和 BootStrapClassLoader 都没能成功加载到这个类,所以最后由 AppClassLoader 来尝试加载。同样,AppClassLoader 会在 classpath 下所有的类库中查找这个类并尝试加载。如果最后还是没有找到这个类,则抛出 ClassNotFoundException 异常。

综上,当类加载器要加载一个类时,如果自己曾经没有加载过这个类,则层层向上委托给父加载器尝试加载。对于 AppClassLoader 而言,它上面有 ExtClassLoader 和 BootStrapClassLoader,所以我们称作「双亲委派」。但是如果我们是使用自定义类加载器来加载类,且这个自定义类加载器的默认父加载器是 AppClassLoader 时,它上面就有三个父加载器,这时再说「双亲」就不太合适了。当然,理解了加载一个类的整个流程,这些名字就无关痛痒了。

为什么需要双亲委派机制#

「双亲委派机制」最大的好处是避免自定义类和核心类库冲突。比如我们大量使用的 java.lang.String 类,如果我们自己写的一个 String 类被加载成功,那对于应用系统来说完全是毁灭性的破坏。我们可以尝试着写一个自定义的 String 类,将其包也设置为 java.lang:

我们将其制作成一个 jar 包,命名为 thief-jdk,然后写一个测试类尝试加载 java.lang.String 并使用接收一个 int 类型参数的构造方法创建实例。

运行测试程序

java -cp /Users/yu/Desktop/lib/thief/thief-jdk.jar:. Test

程序抛出 NoSuchMethodException 异常,因为 JVM 不能够加载我们自定义的 java.lang.String,而是从 BootStrapClassLoader 的缓存中返回了核心类库中的 java.lang.String 的实例,且核心类库中的 String 没有接收 int 类型参数的构造方法。同时我们也看到 Class 实例的类加载器是 null,这也说明了我们拿到的 java.lang.String 的实例确实是由 BootStrapClassLoader 加载的。

总之,「双亲委派」机制的作用就是确保类的唯一性,最直接的例子就是避免我们自定义类和核心类库冲突。

JVM 怎么判断两个类是相同的#

「双亲委派」机制用来保证类的唯一性,那么 JVM 通过什么条件来判断唯一性呢?其实很简单,只要两个类的全路径名称一致,且都是同一个类加载器加载,那么就判断这两个类是相同的。如果同一份字节码被不同的两个类加载器加载,那么它们就不会被 JVM 判断为同一个类。

Person 类:

setPerson(Object obj) 方法接收一个对象,并将其强制转换为 Person 类型赋值给变量 p。

测试类

CustomClassLoader 是一个自定义的类加载器,它将字节码文件加载为字符数组,然后调用 ClassLoader 的 defineClass() 方法创建类的实例,后文会详细讲解怎么自定义类加载器。在测试类中,我们创建了两个类加载器的实例,让他们分别去加载同一份字节码文件,即 Person 类的字节码。然后在实例一上调用 setPerson() 方法将实例二传入,将实例二强制转型为实例一。

运行程序会看到 JVM 抛出了 ClassCastException 异常,异常信息为 Person cannot be cast to Person。从这我们就可以知道,同一份字节码文件,如果使用的类加载器不同,那么 JVM 就会判断他们是不同的类型。

全盘负责#

「全盘负责」是类加载的另一个原则。它的意思是如果类 A 是被类加载器 X 加载的,那么在没有显示指定别的类加载器的情况下,类 A 引用的其他所有类都由类加载器 X 负责加载,加载过程遵循「双亲委派」原则。我们编写两个类来验证「全盘负责」原则。

Worker 类:

DateUtils 类

测试类

运行测试类

java -Xbootclasspath/a:/Users/yu/Desktop/lib/worker.jar Test

运行结果

Copy
true
true
2018-09-16 22:34:43

「全盘委托」原则实际是为「双亲委派」原则提供了保证。如果不遵守「全盘委托」原则,那么同一份字节码可能会被 JVM 加载出多个不同的实例,这就会导致应用系统中对该类引用的混乱.

喜欢的小伙伴,点个关注吧!

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表