题目
请解释Java中的类加载机制,并描述双亲委派模型的工作原理及其如何避免类重复加载
信息
- 类型:问答
- 难度:⭐⭐
考点
类加载机制,双亲委派模型,类加载器
快速回答
Java类加载机制分为加载、链接、初始化三个阶段:
- 加载:查找字节码并创建Class对象
- 链接:包含验证、准备、解析三个子阶段
- 初始化:执行静态代码块和静态变量赋值
双亲委派模型工作原理:
- 类加载器收到请求后先委派给父类加载器
- 父加载器无法完成时才自己尝试加载
- 加载顺序:启动类加载器 → 扩展类加载器 → 应用类加载器 → 自定义加载器
避免重复加载:
- 父加载器已加载的类,子加载器不会重复加载
- 不同类加载器加载的相同类被视为不同类
一、类加载机制核心流程
Java类加载分为三个阶段(详见java.lang.ClassLoader源码):
- 加载(Loading)
- 通过全限定名获取二进制字节流
- 将字节流转化为方法区运行时数据结构
- 在堆中生成Class对象作为访问入口
- 链接(Linking)
- 验证:检查文件格式、元数据、字节码等(-Xverify:none可关闭)
- 准备:为静态变量分配内存并设默认值(如int=0)
- 解析:将符号引用转为直接引用(可选阶段)
- 初始化(Initialization)
- 执行<clinit>()方法,包含静态变量赋值和静态代码块
- 虚拟机保证父类先初始化(接口除外)
二、双亲委派模型原理
类加载器层次结构:
// 类加载器委派逻辑(简化版)
protected Class<?> loadClass(String name, boolean resolve) {
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 委派给父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
// 3. 父加载器无法完成时自行加载
if (c == null) {
c = findClass(name); // 自定义加载器需重写此方法
}
}
return c;
}
}加载器类型:
- Bootstrap ClassLoader:加载JRE/lib核心库(C++实现)
- Extension ClassLoader:加载JRE/lib/ext扩展库
- Application ClassLoader:加载classpath下的类
- Custom ClassLoader:用户自定义(如Tomcat的WebappClassLoader)
三、避免类重复加载的机制
关键设计:
- 向上委派优先:子加载器委托父加载器尝试加载,确保核心类只由Bootstrap加载器加载一次
- 类标识唯一性:相同全限定名 + 相同类加载器 = 唯一类标识
- findLoadedClass检查:加载前先检查当前加载器是否已加载
示例场景:
// 尝试重复加载String类(将失败)
ClassLoader customLoader = new CustomClassLoader();
Class<?> cls1 = customLoader.loadClass("java.lang.String");
Class<?> cls2 = Class.forName("java.lang.String");
System.out.println(cls1 == cls2); // 输出true(实际是同一个类)四、常见问题与最佳实践
常见错误:
- 破坏双亲委派导致核心库被篡改(如自定义java.lang.String)
- 不同类加载器加载同一类造成ClassCastException
- 未正确重写findClass()导致无限递归
最佳实践:
- 自定义类加载器应重写findClass()而非loadClass()
- 需要隔离加载时使用线程上下文类加载器(如JDBC驱动加载)
- OSGi等模块化框架采用图结构委派替代双亲委派
五、扩展知识
- 打破双亲委派的场景:
- SPI机制(ServiceLoader使用线程上下文类加载器)
- 热部署(如Tomcat重写loadClass实现web应用隔离)
- 类卸载条件:
- 无实例存活
- Class对象无引用
- 加载器实例可回收
- JDK9模块化影响:类加载器增加模块层(Layer)概念,委派逻辑更复杂