目录
一、回忆类加载过程
二、类加载器
1、定义与本质
2、JVM内置类加载体系
3、自定义类加载器
ClassLoader类中的两个关键方法:
核心原则:
4、类加载器加载的顺序
(1)核心:双亲委派模型下的委托与加载顺序
1)委托阶段(向上传递请求)
2)加载阶段(向下尝试加载)
注意:
(2)内置类加载器的层级加载范围顺序
(3)触发时机顺序
1) 触发类加载的常见时机
2)依赖类的加载顺序
三、双亲委派模型
1、什么是“双亲”
2、定义
3、执行流程
(1)步骤 1:触发加载请求
(2)步骤 2:向上委托
(3)步骤 3:顶层加载器自检查
(4)步骤 4:向下回传失败结果
(5)步骤 5:当前加载器自行加载
可视化流程:
4、底层实现(基于ClassLoader源码)
(1)loadClass()方法的核心源码(JDK8)
(2)核心方法
5、双亲委派模型的优点
(1)保证 Java 核心类的安全,防止核心 API 被篡改
(2)保证类的唯一性,避免重复加载
(3)明确类加载器的职责划分,提高加载效率
6、打破双亲委派模型
(1)场景一:Tomcat 等 Web 容器的类加载
问题背景
解决方案:打破双亲委派
(2)场景二:Java SPI(服务提供者接口)的加载
问题背景
解决方案:线程上下文类加载器(Thread Context ClassLoader)
(3)场景三:热部署 / 热加载
问题背景
解决方案:打破双亲委派的缓存机制
(4)场景四:Java 9 的模块化系统
一、回忆类加载过程
类加载器是类加载过程中加载过程中核心的实现,如果不熟悉类加载过程,可参考下面链接中的理解类加载过程回忆一下类加载的过程。
理解类加载过程https://blog.csdn.net/2201_75450136/article/details/155944812?spm=1001.2014.3001.5501
二、类加载器
1、定义与本质
类加载器(ClassLoader)是 Java 虚拟机(JVM)的核心组件之一,其核心职责是实现类加载过程中的 “加载” 阶段:根据类的全限定名(如java.lang.String、com.example.User),从磁盘、网络、jar 包等数据源中查找并读取对应的.class字节码文件,将其转换为 JVM 内存中的java.lang.Class对象(该对象是类在 JVM 中的唯一标识,后续对类的所有操作都通过这个对象进行)。
注意:
- 类加载器仅负责加载阶段,而链接(验证、准备、解析)和初始化阶段由 JVM 自身完成;
- 除了启动类加载器,其他类加载器本身也是 Java 类,遵循 “被其他类加载器加载” 的规则;
- JVM 中类的唯一性由 “类加载器 + 类的全限定名” 共同决定—— 即使两个类的全限定名相同,只要由不同的类加载器加载,JVM 就会将其视为两个完全不同的类,这也是类隔离的核心基础。
2、JVM内置类加载体系
JVM 提供了三层内置类加载器,分别负责加载不同来源的类,其层级关系和职责清晰划分:
| 类加载器类型 | 全称 / 别称 | 实现语言与归属 | 核心加载范围 | 关键特点 |
|---|---|---|---|---|
| 启动类加载器(Bootstrap ClassLoader) | 引导类加载器 | C/C++ 实现,属于 JVM 内核组件 | JVM 安装目录下的lib文件夹中核心类库(如rt.jar、charsets.jar),仅加载符合java.*包的核心类 | 1. 不是 Java 类,无法通过代码获取其实例(getClassLoader()返回null);2. 最高层级的类加载器 |
| 扩展类加载器(Extension ClassLoader) | 平台类加载器(Java 9+) | Java 实现(sun.misc.Launcher$ExtClassLoader),属于 JDK 类库 | JVM 安装目录下的lib/ext文件夹,或通过java.ext.dirs系统属性指定的目录中的类库 | 1. 是启动类加载器的子加载器;2. Java 9 后更名为平台类加载器,加载范围扩展到系统模块 |
| 应用类加载器(Application ClassLoader) | 系统类加载器(System ClassLoader) | Java 实现(sun.misc.Launcher$AppClassLoader),属于 JDK 类库 | 项目的类路径(Classpath,包括src编译后的.class 文件、第三方 jar 包如 Spring/MyBatis) | 1. 是扩展类加载器的子加载器;2. 开发者自定义类的默认加载器 |
平台类加载器
Java 9 引入模块化(Module)后,对类加载器体系做了微调:
- 移除了扩展类加载器,替换为平台类加载器(Platform ClassLoader),负责加载 JDK 的系统模块和扩展类;
- 启动类加载器负责加载核心模块(如
java.base);- 应用类加载器负责加载应用模块和 Classpath 中的类;
- 整体层级关系不变,仍遵循双亲委派的核心逻辑。
3、自定义类加载器
当内置类加载器无法满足特殊需求时(如加载加密的.class 文件、从网络下载字节码、动态生成类),可以通过继承ClassLoader类实现自定义类加载器。
ClassLoader类中的两个关键方法:
protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,且实现了双亲委派机制 。其中name为类的二进制名称,当resolve的值为 true,在加载时调用resolveClass(Class<?> c)方法解析该类。protected Class findClass(String name):是根据类的二进制名称来查找类,默认实现的是空方法。
核心原则:
- 不重写
loadClass()方法:避免破坏双亲委派模型,仅需重写findClass()方法(实现自定义的类查找逻辑); - 通过
defineClass()生成 Class 对象:将读取到的字节码数组转换为Class对象,这是 JVM 提供的底层方法,保证类的合法加载。
4、类加载器加载的顺序
类加载器的加载顺序并非单一的 “线性顺序”,而是结合了双亲委派模型的 “委托顺序”、类加载器的 “层级顺序” 以及触发类加载的 “时机顺序”三个维度。
(1)核心:双亲委派模型下的委托与加载顺序
这是类加载器最基础、最核心的加载顺序,也是单个类被加载时的核心流程。简单来说,顺序是:先向上委托父加载器,再向下由子加载器自行加载,具体可分为「委托阶段」和「加载阶段」两个步骤。
1)委托阶段(向上传递请求)
当任意一个类加载器收到类加载请求时,会按 “当前类加载器 → 父类加载器 → 启动类加载器”的顺序,将请求向上委托,直到传递到最顶层的启动类加载器。
示例:加载com.example.User时的委托顺序:
应用类加载器(AppClassLoader)→ 扩展类加载器(ExtClassLoader/PlatformClassLoader)→ 启动类加载器(BootstrapClassLoader)2)加载阶段(向下尝试加载)
当父加载器无法加载该类时,请求会向下回传,由子加载器依次尝试自行加载,直到某个加载器成功加载或全部失败(抛出ClassNotFoundException)。
示例:加载com.example.User时的加载顺序:
启动类加载器(检查核心类库,失败)→ 扩展类加载器(检查扩展目录,失败)→ 应用类加载器(检查Classpath,成功加载)注意:
- 委托阶段是“单向向上”的,加载阶段是“单向向下”的;
- 每个类加载器在委托前,会先检查自身是否已经加载过该类(缓存机制),避免重复加载。
(2)内置类加载器的层级加载范围顺序
从 JVM 内置类加载器的职责划分来看,它们的加载范围有明确的优先级顺序:启动类加载器优先加载核心类,其次是扩展 / 平台类加载器,最后是应用类加载器。这种顺序是为了保证核心类的唯一性和安全性。
| 加载优先级 | 类加载器类型 | 加载范围优先级 | 说明 |
|---|---|---|---|
| 最高 | 启动类加载器 | 核心类库(java.*) | 先加载 JVM 最核心的类 |
| 中间 | 扩展 / 平台类加载器 | 扩展类库 | 次加载系统扩展类 |
| 最低 | 应用类加载器 | 应用类和第三方库 | 最后加载开发者自定义的类 |
举例:当同时存在java.lang.String(核心类)、com.sun.nio.file.ExtendedFileSystem(扩展类)、com.example.User(应用类)时,加载顺序为:
java.lang.String(启动类加载器)→com.sun.nio.file.ExtendedFileSystem(扩展类加载器)→com.example.User(应用类加载器)。
(3)触发时机顺序
类加载器并非启动时就加载所有类,而是“懒加载”(按需加载),只有在特定时机才会触发类加载,不同触发时机的加载顺序遵循“使用即加载”的原则,且存在一些固定的先后依赖。
1) 触发类加载的常见时机
- 第一次创建类的实例(
new User()):触发类加载; - 第一次访问类的静态变量 / 静态方法(
User.num、User.test()):触发类加载; - 反射调用类(
Class.forName("com.example.User")):主动触发类加载; - 加载子类时:先加载父类(父类的加载顺序优先于子类);
- 执行
main()方法的类:作为程序入口,最先被加载; - SPI 服务加载(如
ServiceLoader.load(XXX.class)):触发实现类的加载。
2)依赖类的加载顺序
当一个类依赖其他类时,加载顺序为:被依赖的类优先于依赖类加载。
如:
public class Parent {} public class Child extends Parent { private static User user = new User(); }当第一次加载Child类时,加载顺序为:Parent(父类,由对应加载器加载)→User(依赖类)→Child(子类)。
三、双亲委派模型
双亲委派模型(Parent Delegation Model)是 Java 虚拟机(JVM)中类加载器的核心设计模式,其本质是一种 “向上委托、向下加载” 的层级委派机制,旨在通过严格的加载顺序保证 Java 核心类的安全与唯一性,同时简化类加载的职责划分。
1、什么是“双亲”
“双亲” 不是 “父母”,是 “委派关系”,很多人会误解 “双亲” 是指类加载器的继承关系,实际上:
- 双亲委派中的 “父类加载器”(Parent ClassLoader)是指 “委派的上级加载器”,而非 Java 中的类继承(
extends)关系。 - 大部分类加载器(如应用类加载器、扩展类加载器)通过组合方式持有父加载器的引用(
ClassLoader类中的parent成员变量),而非继承。 - 唯一的例外是启动类加载器(Bootstrap ClassLoader):它是用 C/C++ 实现的 JVM 内核组件,没有父加载器(
parent为null),是整个委派模型的顶层。
2、定义
当一个类加载器收到类加载请求时,它不会立即尝试自己加载,而是遵循以下步骤:
- 先委托父加载器:将加载请求向上传递给其父加载器处理;
- 逐层向上委派:这个委托过程会一直传递到最顶层的启动类加载器;
- 父加载器自检查:每个父加载器会先检查自己是否已经加载过该类,若已加载则直接返回
Class对象;若未加载,则在自己的加载范围内查找对应的.class文件; - 向下回传失败:如果父加载器在自己的加载范围内找不到该类,会将 “加载失败” 的结果回传给子加载器;
- 子加载器自行加载:只有当所有父加载器都加载失败后,当前类加载器才会尝试在自己的加载范围内查找并加载该类;
- 最终失败抛出异常:如果当前类加载器也无法加载,则抛出
ClassNotFoundException。
3、执行流程
以加载开发者自定义的com.example.User类为例,结合 JVM 内置的三层类加载器,完整执行流程如下:
(1)步骤 1:触发加载请求
当代码中执行new com.example.User()时,应用类加载器(AppClassLoader)收到加载com.example.User的请求。
(2)步骤 2:向上委托
- 应用类加载器首先检查自己是否已加载过
com.example.User(通过findLoadedClass()方法),若未加载,则将请求委托给其父加载器 ——扩展类加载器(ExtClassLoader/PlatformClassLoader)。 - 扩展类加载器同样先检查缓存,若未加载,将请求委托给其父加载器 ——启动类加载器(BootstrapClassLoader)。
(3)步骤 3:顶层加载器自检查
启动类加载器检查自己的加载范围(JVM 安装目录下的lib文件夹,如rt.jar),发现没有com.example.User类(核心类库只有java.*、javax.*等包的类),因此返回 “加载失败”。
(4)步骤 4:向下回传失败结果
- 扩展类加载器收到启动类加载器的失败结果后,检查自己的加载范围(
lib/ext文件夹或java.ext.dirs指定的目录),也找不到com.example.User,返回 “加载失败”。 - 应用类加载器收到扩展类加载器的失败结果后,开始自行加载。
(5)步骤 5:当前加载器自行加载
应用类加载器在类路径(Classpath)下(如项目的target/classes目录)查找com/example/User.class文件,找到后读取字节码并生成Class对象,完成加载。
可视化流程:
应用类加载器(收到请求) ↓(委托) 扩展类加载器 ↓(委托) 启动类加载器(检查核心类库,失败) ↓(回传失败) 扩展类加载器(检查扩展目录,失败) ↓(回传失败) 应用类加载器(检查Classpath,成功加载)4、底层实现(基于ClassLoader源码)
双亲委派模型的核心逻辑在java.lang.ClassLoader类的loadClass()方法中实现,这是 JVM 提供的默认实现,所有自定义类加载器都继承了这个逻辑。
(1)loadClass()方法的核心源码(JDK8)
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 1. 加锁:保证多线程下类加载的线程安全,避免重复加载 synchronized (getClassLoadingLock(name)) { // 2. 检查当前类加载器是否已加载该类(缓存优先) Class<?> c = findLoadedClass(name); if (c == null) { // 未加载过 long t0 = System.nanoTime(); try { // 3. 有父加载器则委托父加载器加载 if (parent != null) { c = parent.loadClass(name, false); } else { // 4. 无父加载器(如扩展类加载器),委托启动类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 父加载器加载失败,捕获异常,继续下一步 } // 5. 所有父加载器都失败,调用自身的findClass()方法加载 if (c == null) { long t1 = System.nanoTime(); c = findClass(name); // 性能统计(JVM内部用) sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } // 6. 若resolve为true,执行类的解析(链接阶段的解析步骤) if (resolve) { resolveClass(c); } return c; } }(2)核心方法
| 方法 | 作用 |
|---|---|
findLoadedClass(name) | 检查当前类加载器的缓存,避免重复加载 |
parent.loadClass(name) | 委托父加载器加载,实现向上委派 |
findBootstrapClassOrNull(name) | 委托启动类加载器加载(仅当parent为null时) |
findClass(name) | 父加载器失败后,当前加载器自行查找类(子类需重写) |
resolveClass(c) | 执行类的解析(链接阶段的最后一步) |
5、双亲委派模型的优点
设计双亲委派模型的根本目的是保障 Java 程序的安全性和稳定性,具体体现在以下三点:
(1)保证 Java 核心类的安全,防止核心 API 被篡改
这是最核心的优势。例如,若开发者试图自定义一个java.lang.String类(与 Java 核心类同名),按照双亲委派模型:
- 加载请求会先传递到启动类加载器;
- 启动类加载器已经加载了核心的
java.lang.String类(来自rt.jar),会直接返回该类的Class对象; - 自定义的
java.lang.String类永远不会被加载,从而避免了核心类被恶意替换或篡改的风险。
补充:JVM 还会对核心类的包名进行校验(如java.lang包的类只能由启动类加载器加载),即使绕过双亲委派,也会抛出SecurityException。
(2)保证类的唯一性,避免重复加载
由于每个类加载器都会先检查缓存,且父加载器优先加载,因此同一个类在 JVM 中只会被同一个类加载器加载一次。
例如,两个不同的类加载器的子加载器请求加载com.example.User,最终都会委托到应用类加载器加载,从而保证User类的Class对象在 JVM 中是唯一的,避免了类冲突。
(3)明确类加载器的职责划分,提高加载效率
JVM 的内置类加载器有明确的加载范围:
- 启动类加载器:负责核心类库;
- 扩展类加载器:负责系统扩展类;
- 应用类加载器:负责应用类和第三方库。
双亲委派模型让每个加载器只关注自己的职责范围,无需处理其他范围的类,从而提高了类加载的效率和可维护性。
6、打破双亲委派模型
虽然双亲委派模型是 JVM 的默认规则,但在某些场景下,由于其自身的局限性,需要被打破。这些场景也恰恰体现了 Java 类加载机制的灵活性。
(1)场景一:Tomcat 等 Web 容器的类加载
问题背景
Tomcat 作为 Web 容器,需要实现多 Web 应用的类隔离:
- 不同的 Web 应用可能依赖不同版本的第三方库(如应用 A 用 Spring 5,应用 B 用 Spring 6);
- 若遵循双亲委派,应用类加载器会只加载一次 Spring 类,导致版本冲突。
解决方案:打破双亲委派
Tomcat 自定义了类加载器体系(如WebappClassLoader),其加载顺序与双亲委派相反:
- 先加载当前 Web 应用
WEB-INF/classes目录下的类; - 再加载当前 Web 应用
WEB-INF/lib下的 jar 包中的类; - 最后才委托父加载器(Tomcat 的
CommonClassLoader)加载公共类和核心类。
核心目的:让每个 Web 应用拥有独立的类空间,实现类隔离。
(2)场景二:Java SPI(服务提供者接口)的加载
问题背景
SPI 是 Java 的一种服务发现机制(如 JDBC、JNDI、SLF4J),以 JDBC 为例:
java.sql.Driver接口由启动类加载器加载(属于核心类库rt.jar);- 具体的驱动实现(如 MySQL 的
com.mysql.cj.jdbc.Driver)在 Classpath 中,应由应用类加载器加载; - 按照双亲委派,启动类加载器无法委托子加载器加载,导致无法找到驱动实现。
解决方案:线程上下文类加载器(Thread Context ClassLoader)
JVM 提供了线程上下文类加载器,允许通过Thread.currentThread().getContextClassLoader()获取当前线程的类加载器(默认是应用类加载器),从而打破双亲委派的层级限制:
DriverManager(启动类加载器加载)通过线程上下文类加载器,获取应用类加载器;- 应用类加载器加载 Classpath 中的 JDBC 驱动实现类;
- 驱动类被实例化并注册到
DriverManager中。
这是一种“父加载器调用子加载器”的逆向委派,本质上打破了双亲委派的单向委托规则。
(3)场景三:热部署 / 热加载
问题背景
热部署(如 Spring Boot DevTools、JRebel)需要在不重启 JVM 的情况下,动态更新类的字节码。
解决方案:打破双亲委派的缓存机制
- 自定义类加载器加载新的类字节码;
- 每次热加载时,创建新的类加载器实例,加载更新后的类;
- 由于类的唯一性由 “类加载器 + 类名” 决定,新的类加载器会生成新的
Class对象,从而实现类的动态替换。
这种方式打破了 “类只被加载一次” 的缓存规则,本质上也是对双亲委派的灵活调整。
(4)场景四:Java 9 的模块化系统
Java 9 引入了模块化(Module)系统,对类加载器体系进行了微调:
- 移除了扩展类加载器,替换为平台类加载器;
- 允许模块指定 “导出” 和 “开放” 的包,类加载的范围由模块决定。
虽然整体仍遵循双亲委派的核心逻辑,但模块化系统让类加载的范围更精细,也在一定程度上突破了传统双亲委派的职责划分。