JVM 类加载器详解
如果要确定两个类是否相同,必须满足以下三点:
- 同路径
- 同名
- 由同一个类加载器创建
即使是同路径同名的 class 类,如果被不同的类加载器加载那也是不同的
一、类加载器分类及作用
JVM 类加载器分为 四类,按层级从上到下依次为:
启动类加载器(Bootstrap ClassLoader) | 加载 JVM 核心类库(如 java.lang.* 、java.util.* ),唯一不继承 ClassLoader 的加载器 |
JAVA_HOME/jre/lib 下的核心 jar 包(如 rt.jar ) |
C/C++ |
扩展类加载器(Extension ClassLoader) | 加载 JVM 扩展类库(如 javax.* ) |
JAVA_HOME/jre/lib/ext 目录下的 jar 包 |
Java |
应用程序类加载器(Application ClassLoader) | 加载用户类路径(ClassPath)下的类(如项目代码、第三方依赖) | 由 -classpath 或 -cp 指定的路径 |
Java |
自定义类加载器(Custom ClassLoader) | 用户自定义类加载逻辑(如热部署、网络加载、加密类加载) | 由用户代码实现 | Java |
二、双亲委派机制
1. 核心流程
当类加载器收到加载请求时,按以下步骤处理:
- 向上委派: 不直接加载类,而是先委派父类加载器处理。
- 向下尝试: 若父类加载器无法完成加载(在自己的搜索范围内找不到类),子类加载器才会尝试加载。
2. 流程示例
具体实现依赖于**ClassLoader.loadClass()
**
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 先委派父加载器加载
if (parent != null) {
c = parent.loadClass(name, false); // 递归调用父加载器
} else {
// 父加载器为 null,表示到达 Bootstrap 类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器加载失败,继续向下执行
}
if (c == null) {
// 3. 父加载器未找到,由当前类加载器加载
c = findClass(name);
}
}
return c;
}
}
- 用户自定义类 MyClass 的加载过程:
Application ClassLoader
→ 委派给Extension ClassLoader
。Extension ClassLoader
→ 委派给Bootstrap ClassLoader
。Bootstrap ClassLoader
未找到MyClass
→ 返回Extension ClassLoader
。Extension ClassLoader
未找到MyClass
→ 返回Application ClassLoader
。Application ClassLoader
在 ClassPath 中找到并加载MyClass
。
3. 核心意义
- 避免重复加载:确保类在 JVM 中唯一(由父加载器优先加载)。对于避免重复加载,双亲委派通过委派链确保每个类由最顶层的父加载器优先尝试加载,如果已经加载过,就直接返回,不会再让子加载器处理。
- 保护核心类安全:防止用户自定义同名类(如
java.lang.String
)覆盖 JVM 核心类。类加载过程会由启动类加载器强制优先加载核心类,加载后就会直接返回,所以子类无法通过自定义同名类来覆盖核心类
三、打破双亲委派机制的场景
1. 经典场景
- SPI 服务发现:
JDBC 的
DriverManager
需要加载不同厂商的数据库驱动(如 MySQL、Oracle)。- 问题:核心类
DriverManager
(由Bootstrap ClassLoader
加载)需调用第三方驱动的实现类(由Application ClassLoader
加载),但双亲委派机制禁止父加载器访问子加载器的类。 - 解决方案:将类加载器切换为 线程上下文类加载器,将加载权临时交给子类加载器。
- 问题:核心类
2. 如何打破双亲委派?
- 重写
loadClass()
方法: 自定义类加载器不委派父加载器,直接加载类。 - 线程上下文类加载器:
通过
Thread.currentThread().setContextClassLoader()
临时切换类加载器。
以下是java发展历程中的三次打破双亲委派机制
第一次破坏:JDK 1.2 之前的“远古时代”
- 背景: 在 JDK 1.2 引入双亲委派模型之前,类加载器的实现没有统一规范。开发者通常直接重写
loadClass()
方法,直接实现类加载逻辑,未形成层级委派机制。- 问题:
- 类加载逻辑混乱,容易重复加载或覆盖核心类。
- 缺乏统一的委派规则,导致类加载器之间的协作困难。
- JDK 1.2 的改进:
- 正式提出双亲委派模型,规范类加载流程。
- 建议开发者仅重写
findClass()
:loadClass()
方法默认实现委派逻辑,而findClass()
仅负责具体类查找(如从自定义路径加载字节码)。开发者只需重写findClass()
即可实现扩展,无需破坏委派机制。- 意义: 通过约束开发者仅扩展
findClass()
,避免直接干预委派流程,从而维护双亲委派的核心规则。第二次破坏:模型自身的缺陷(基础类型回调用户代码)
背景: 双亲委派模型要求父加载器优先加载类,但某些场景下,父加载器加载的类需要调用子加载器加载的实现类,导致无法直接通过委派机制实现。
典型案例:
JDBC SPI(如
DriverManager
):
DriverManager
(由Bootstrap ClassLoader
加载)需要加载不同数据库厂商的Driver
实现类(由Application ClassLoader
加载)。- 按双亲委派规则,父加载器(
Bootstrap
)无法直接访问子加载器(Application
)的类,导致驱动无法加载。解决方案:
- 线程上下文类加载器(TCCL): 通过
Thread.currentThread().setContextClassLoader()
将类加载器切换为子加载器(如Application ClassLoader
),使父加载器代码可间接访问子加载器的类。意义: 通过打破双亲委派,解决了核心库(如 JDBC)需要动态扩展的难题,但需谨慎使用以避免安全风险。
第三次破坏:动态性需求(热替换、模块热部署)
背景: 现代应用对 动态性 的要求(如不重启服务更新代码)需要更灵活的类加载机制,而双亲委派的层级化模型无法满足。
典型案例:
Tomcat 热部署:
- 每个 Web 应用使用独立的
WebappClassLoader
,重启应用时直接替换类加载器,实现代码热替换。实现方式:
- 自定义类加载器:直接控制类加载逻辑,绕过双亲委派。
- 动态卸载与加载:通过销毁旧类加载器并创建新类加载器,实现代码更新。
意义: 牺牲双亲委派的部分安全性,换取系统的高灵活性和动态性,适应云原生、微服务等现代架构需求。
四 、自定义类加载器
使用场景
- 想加载非 classpath 随意路径中的类文件
- 通过接口来使用实现,希望解耦时,常用在框架设计和SPI思想
- 这些类希望予以隔离,不同应用的同名类都可以加载(其实就是违背双委机制的意义1),不冲突,常见于 tomcat 容器
步骤
- 继承 ClassLoader 父类
- 要遵从双亲委派机制,重写 findClass 方法(不是重写 loadClass 方法,否则不会走双亲委派机制)
- 读取类文件的字节码,调用父类的 defineClass 方法来加载类
- 使用者调用该类加载器的 loadClass 方法
fengdongnan的博客 文章被收录于专栏
记录fengdongnan的知识产出文档,欢迎大家来一起交流学习