类加载器加载的过程
🔴 双亲委派机制的执行流程:
-
委派阶段(自下而上):
- 当需要加载一个类时,首先由当前ClassLoader(通常是Application ClassLoader)接收请求
- 当前ClassLoader检查自己是否已经加载过该类,如果已加载则直接返回
- 如果未加载,则委派给父ClassLoader处理
- 逐级向上委派:Application ClassLoader → Extension ClassLoader → Bootstrap ClassLoader
- 直到委派到Bootstrap ClassLoader(顶层,没有父ClassLoader)
-
加载阶段(自上而下):
- Bootstrap ClassLoader首先尝试加载该类
- 负责加载核心类库(rt.jar、charsets.jar等)
- 如果该类属于核心类库范围,则加载成功并返回
- 如果不属于其负责范围,则加载失败
- 如果Bootstrap ClassLoader加载失败,则由Extension ClassLoader尝试加载
- 负责加载扩展类库(lib/ext目录下的jar包)
- 如果属于扩展类库范围,则加载成功并返回
- 如果不属于其负责范围,则加载失败
- 如果Extension ClassLoader也加载失败,则由Application ClassLoader尝试加载
- 负责加载用户类路径(classpath)下的类
- 通常能够加载成功,因为用户类都在classpath中
- Bootstrap ClassLoader首先尝试加载该类
-
关键特点:
- 安全性:确保核心类库不被用户类覆盖
- 唯一性:同一个类只会被一个ClassLoader加载一次
- 层次性:每个ClassLoader都有明确的职责范围
全盘负责机制
🔴 全盘负责(Full Responsibility):当一个类加载器负责加载某个类时,它不仅要加载该类本身,还要负责加载该类所依赖的所有其他类(包括其父类、接口、字段类型、方法参数类型、方法返回类型等)。
🟠 全盘负责的具体表现:
-
依赖类加载:
- 当加载类A时,如果A继承自类B,那么类加载器必须同时负责加载类B
- 如果A实现了接口C,那么类加载器必须同时负责加载接口C
- 如果A的字段类型是类D,那么类加载器必须同时负责加载类D
-
递归加载:
- 加载过程是递归的:加载A需要加载B,加载B又可能需要加载E
- 整个依赖链上的所有类都由同一个ClassLoader负责加载
-
一致性保证:
- 确保一个类的所有依赖类都由同一个ClassLoader加载
- 避免不同ClassLoader加载同一个类导致类型不一致的问题
🟡 全盘负责的重要性:
- 类型安全:防止同一个类被不同ClassLoader加载造成类型冲突
- 内存效率:避免重复加载相同的类
- 运行时稳定:确保类之间引用关系的正确性
🔴 当前入口ClassLoader无法加载涉及类的处理机制
🔴 核心问题:如果当前入口的ClassLoader无法加载当前类所涉及的类会怎么处理?
🟠 处理流程详解:
-
委派机制启动:
- 当前ClassLoader(入口ClassLoader)首先尝试加载目标类
- 如果无法加载,启动双亲委派机制
- 按照层次结构向上委派:Application → Extension → Bootstrap
-
逐级尝试加载:
-
Bootstrap ClassLoader尝试:
- 如果目标类属于核心类库(如java.lang.*),Bootstrap成功加载
- 如果不属于核心类库范围,Bootstrap返回null(表示加载失败)
-
Extension ClassLoader尝试:
- 如果Bootstrap失败,Extension尝试在ext目录加载
- 如果目标类属于扩展类库,Extension成功加载
- 如果不属于扩展类库范围,Extension返回null
-
Application ClassLoader尝试:
- 如果Extension也失败,Application尝试在classpath加载
- 如果目标类在用户类路径中,Application成功加载
- 如果不在classpath中,Application返回null
-
-
最终处理结果:
🔴 成功情况:
- 某个ClassLoader成功加载了类,返回Class对象
- 后续对该类的所有引用都使用这个ClassLoader加载的版本
🔴 失败情况:
- 所有ClassLoader都无法加载目标类
- 抛出
ClassNotFoundException异常 - 程序执行被中断
🟡 实际应用场景:
-
缺少依赖JAR包:
// 如果classpath中缺少某个依赖库 // 当代码引用该库中的类时,会触发ClassNotFoundException import com.example.ExternalClass; // 如果ExternalClass不存在 -
类路径配置错误:
// 如果JAR包在错误的路径下 // Application ClassLoader无法找到类文件 -
版本不匹配:
// 编译时使用的类版本与运行时不一致 // 可能导致ClassNotFoundException
是否可以打破双亲委派
🔴 答案:可以,但需要通过特定的方式,最常见的是重写loadClass()方法。
🟠 打破双亲委派的方法:
-
重写loadClass()方法(主要方式):
loadClass()方法是双亲委派机制的核心实现findClass()方法只是加载类的具体实现,不会影响委派机制- 通过重写
loadClass()方法可以完全绕过双亲委派
-
使用线程上下文类加载器(SPI机制):
- 通过
Thread.currentThread().setContextClassLoader()设置 - 主要用于JDBC驱动加载、日志框架等SPI场景
- 通过
-
继承ClassLoader类:
- 不是实现接口,而是继承ClassLoader类
- 可以自定义类加载逻辑
🟡 打破双亲委派的应用场景:
- 热部署和热替换:如OSGi框架、Tomcat的Web应用隔离
- 模块化加载:不同模块需要不同版本的同一个类
- SPI机制:JDBC驱动加载、日志框架等
- 类隔离:防止不同应用之间的类冲突
🟢 风险和注意事项:
- 安全性风险:可能绕过核心类库的保护机制
- 类冲突:同一类被多个ClassLoader加载可能导致类型转换异常
- 内存泄漏:自定义ClassLoader可能导致类无法被GC回收
- 调试困难:打破标准机制会增加问题排查的复杂度
为什么我们的类加载器要分层
🔴 简单理解:就像公司的组织架构一样,分层管理更安全更高效
🟠 主要原因:
-
安全性 - 保护核心类库
- 防止你写一个恶意的
java.lang.String类覆盖系统的String - 就像银行金库有多层防护一样
- 防止你写一个恶意的
-
职责分工 - 各司其职,便于排查问题
- Bootstrap:管JDK核心类(java.lang.*等)
- Extension:管扩展类库
- Application:管你的应用程序类
- 出现ClassNotFoundException时,能快速定位是哪一层的问题
- 就像不同部门管不同的事,出问题时知道找谁
浙公网安备 33010602011771号