ClassLoader体系概述
最近项目中需要运行时加载自定义 Jar 包,涉及到 Java 的 ClassLoader 体系。为了彻底搞清楚其中的原理(特别是 JDK 9 模块化后的变化),以及 Spring Boot 胖 Jar 中的自定义类加载机制,我整理了这份笔记,也方便以后查阅。
1.本文结构
-
ClassLoader 的类体系
展示 ClassLoader 的继承关系图,并解释为何存在两条继承线(JDK 内部实现线 vs 公开 API 线),以及各自职责。
-
两条继承线的分工 详细阐述 JDK 内部实现线(模块化感知、双亲委派)和公开 API 线(可扩展、URLClassLoader 体系)的分工与总结。
-
模块化下的双亲委派体系
通过堆栈分析说明 JDK 9+ 模块化系统如何改变线性委派模型;给出
AppClassLoader加载类的详细流程图,并以java.sql.Connection、String、com.example.MyService为例演示模块化路由过程。 -
自定义 ClassLoader(以 LaunchedClassLoader 为例) 结合 Spring Boot 源码,展示自定义类加载器的创建与委派链,重点分析其
loadClass方法的特殊处理逻辑(包前缀绕过双亲委派),并解释为何需要这样设计。
2.ClassLoader的类体系
可以看出,上图的继承体系分为两条线。
左边的PlatformClassLoader和AppClassLoader都继承了jdk.internal.loader.BuiltinClassLoader,jdk.internal.loader.BuiltinClassLoader继承了java.lang.ClassLoader。
右边的Spring Boot LaunchedClassLoader继承了java.net.URLClassLoader,java.net.URLClassLoader继承了java.security.SecureClassLoader,java.security.SecureClassLoader继承了java.lang.ClassLoader。
3. 两条继承线的分工
分支一:JDK 内部实现线
职责:
- 双亲委派:继承自 ClassLoader的模板方法loadClass,并且BuiltinClassLoader在委派前增加了模块化路由(findLoadedModule精准直达模块所属加载器)。
- 模块化感知:维护packageToModule映射,确保模块边界和包归属不被破坏。
- 类路径来源:PlatformClassLoader和AppClassLoader分别负责不同的类来源(模块路径 vs classpath),完成 JVM 自身的类加载体系。
分支二:公开 API 线(提供可扩展的类加载器)
职责:
- 提供开发者接口:任何想要自定义类路径的人,只需要继承URLClassLoader并传入一组URL,无需关心底层字节码读取细节。
- 不关心模块:URLClassLoader完全不涉及 JPMS 的模块系统,它只知道从给定的 URL列表里找类和资源(适用于传统的 classpath 或 Spring Boot 的 jar:nested场景)。
- 安全能力:通过父类SecureClassLoader绑定了CodeSource和ProtectionDomain,所以即使是自定义加载器,加载的类也带安全出身。
总结:一条是 JVM 的"内政官",负责系统稳定和模块秩序;另一条是"开放口岸",允许世界各地的代码以统一方式进入 Java 运行时。因此,当我们真正需要实现一个可加载外部 Jar 的自定义类加载器时,几乎无一例外地会继承 URLClassLoader(或它的子类,如 Spring Boot 的 LaunchedClassLoader)。
4.ClassLoader在模块化下的双亲委派体系
先看看SpringBoot胖jar加载org.springframework.boot.loader.launch.JarLauncher类的堆栈。 加载JarLauncher的是AppClassLoader。
如上面动图所展示的堆栈,在LauncherHelper的loadMainClass中,调用Class.forName加载Launcher类。之后进入native方法forName0,JVM再调用loadClass方法。那么JVM具体调用哪个ClassLoader的loadClass方法呢?这就看native方法中传入的loader具体是哪个对象,JVM会根据传入的对象,调用该对象的loadClass方法。
java
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller)
throws ClassNotFoundException;
通过堆栈看到,首先进入的是ClassLoader的loadClass,那是不是说明forName0传入的对象是ClassLoader对象呢?不是的,我们通过前面的堆栈可以看到传入的ClassLoader其实是AppClassLoader的对象。AppClassLoader是ClassLoader的子类,它并未覆写loadClass函数,所以调用loadClass函数时,实际上是调用它的父类ClassLoader的loadClass函数。后面看到堆栈两次进入BuiltinClassLoader的loadClassOrNull函数,也是这个道理。
在没有模块化的 JDK 8 及之前,委托链是线性的:
markdown
AppClassLoader
└─ parent → PlatformClassLoader
└─ parent → BootstrapClassLoader
JDK 9 引入模块系统后,BuiltinClassLoader 作为 PlatformClassLoader和AppClassLoader的共同父类,多了一个关键数据结构:
java
// 包名 → 该包所属的已解析模块
ConcurrentHashMap<String, LoadedModule> packageToModule;
这个映射表记录了哪个包归属于哪个模块。而模块系统要求:如果一个包已经被某个模块声明,那么该包下的所有类,必须由定义该模块的类加载器来加载。 这就打破了一味向上委托的线性模型,变成了先查模块映射,再按需委托的图状模型。
梳理一下AppClassLoader加载类的流程图,如下。
与传统的双亲委派相比,模块化的双亲委派,在委派parent类加载器加载之前,会先做模块化判断。
模块化路由举例
a. 加载 java.sql.Connection
假设应用代码触发 AppClassLoader.loadClass("java.sql.Connection"):
步骤 1:模块化判断
findLoadedModule("java.sql.Connection"):
提取包名 java.sql
- 查到它属于
java.sql模块 - 该模块被 JDK 内部定义,由 PlatformClassLoader 负责加载
- 发现
PlatformClassLoader≠ 当前对象(AppClassLoader) - 直接委托给 PlatformClassLoader.loadClass("java.sql.Connection")
步骤 2:PlatformClassLoader 处理
PlatformClassLoader 自己的 loadClass:
- 检查缓存 → 未找到
- 进入标准双亲委派:
super.loadClass→ClassLoader.loadClass - 委托 parent(Bootstrap) → Bootstrap 找不到
- 自己
findClass("java.sql.Connection")→ 找到,返回
这样,即使 AppClassLoader 和 PlatformClassLoader 之间还存在传统的父子链,模块化判断绕过了逐步向上委派的步骤,直接把请求路由到了正确的模块加载器。
b. 加载 String
触发 AppClassLoader.loadClass("java.lang.String"):
findLoadedModule("java.lang.String")→ 包java.lang→ 属于java.base模块java.base由 Bootstrap 定义- 委托给 Bootstrap → 直接返回已加载的
String.class
这里模块化判断甚至直接跳过了 PlatformClassLoader。
c. 加载自己应用里的 com.example.MyService
触发 AppClassLoader.loadClass("com.example.MyService"):
findLoadedModule("com.example.MyService")→ 包com.example- packageToModule 中没有该包的条目(你的应用代码不在模块路径上,而是在 classpath 上)
- 返回 null → 走标准双亲委派
ClassLoader.loadClass→ 委派 Platform → 委派 Bootstrap → 都找不到AppClassLoader.findClass("com.example.MyService")→ 从 classpath 找到
5. 自定义ClassLoader,以LaunchedClassLoader举例
在springboot的Launcher中有这样一段代码
java
private ClassLoader createClassLoader(URL[] urls) {
// 获取当前的类加载器,当前类是Launcher,类加载器是AppClassLoader
ClassLoader parent = getClass().getClassLoader();
// 创建一个LauncherClassLoader对象,将parent(即AppClassLoader)作为父加载器。
return new LaunchedClassLoader(isExploded(), getArchive(), urls, parent);
}
故LauncherClassLoader的实例委托链为:
text
BootstrapClassLoader (C++, null)
↑ parent
PlatformClassLoader
↑ parent
AppClassLoader
↑ parent
LaunchedClassLoader ← 自定义加载器
LauncherClassLoader的loadClass比较简单,代码如下
java
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith(JAR_MODE_PACKAGE_PREFIX) || name.equals(JAR_MODE_RUNNER_CLASS_NAME)) {
try {
Class<?> result = loadClassInLaunchedClassLoader(name);
if (resolve) {
resolveClass(result);
}
return result;
}
catch (ClassNotFoundException ex) {
// Ignore
}
}
return super.loadClass(name, resolve);
}
逐层逻辑
- 判断类名是否需要特殊处理 如果全限定类名以
JAR_MODE_PACKAGE_PREFIX(例如org.springframework.boot.loader.)开头, 或者类名严格等于JAR_MODE_RUNNER_CLASS_NAME,
则进入自定义加载流程。
否则,直接走标准双亲委派super.loadClass(name, resolve)。 - 自定义加载流程
调用loadClassInLaunchedClassLoader(name),该方法内部会使用LaunchedClassLoader的 URL 类路径(包含BOOT-INF/classes/和BOOT-INF/lib/*.jar的jar:nestedURL)去查找并定义类。 如果找到并成功加载,得到一个Class<?>对象result。 如果resolve参数为true,则调用resolveClass(result)完成该类的链接(解析符号引用)。 返回已加载的Class对象。 - 标准双亲委派
对于不匹配包前缀的类,或者自定义加载器找不到的类,调用super.loadClass(name, resolve)。 这会将请求交给父类ClassLoader的模板方法,执行"先委托父加载器,找不到再自己findClass"的经典流程。 这里的父加载器通常是AppClassLoader,因此会向上委托到PlatformClassLoader和BootstrapClassLoader去查找核心类库和 classpath 上的类。
为什么要有这段逻辑?
回顾 Spring Boot 的胖 JAR 结构:
text
app.jar
├── org/springframework/boot/loader/... ← 启动器自己的类
├── BOOT-INF/classes/ ← 你的应用类
└── BOOT-INF/lib/ ← 你的第三方依赖
启动器在运行时,创建了一个自定义的ClassLoader(例如LaunchedClassLoader),它知道如何从BOOT-INF/下面加载类。但是JVM 启动时,首先加载的是启动器自身的类(org.springframework.boot.loader.*),这些类是由 AppClassLoader从胖 JAR 的根目录加载的。
现在问题来了:自定义的ClassLoader继承自URLClassLoader,按照双亲委派模型,所有加载请求会先向上委派给AppClassLoader。如果应用代码中引用了某个同时存在于启动器包路径和BOOT-INF/classes/中的类,那么AppClassLoader会优先从根目录找到它,而不是从BOOT-INF/classes/加载。这会导致类版本混乱,尤其是当 Spring Boot 的启动器依赖和用户应用依赖存在重叠时。
此外,某些特定的"桥接"类(如JarModeRunner)必须由自定义加载器加载,以确保它们能访问到嵌套 JAR 内部的类。