问题
双亲委派机制的核心思想是什么?
答案
核心思想
双亲委派机制的核心思想是:优先委派父类加载器加载类,只有父类无法加载时,子类加载器才尝试自己加载。
设计原则:
向上委派加载 → 向下逐级尝试
一句话总结:保证Java核心类库的类型安全和唯一性,防止核心API被篡改。
设计初衷
1. 保证类的唯一性
// 问题场景:如果没有双亲委派
// 不同类加载器可能加载多个java.lang.String
ClassLoader loader1 = new CustomClassLoader();
ClassLoader loader2 = new CustomClassLoader();
Class<?> string1 = loader1.loadClass("java.lang.String");
Class<?> string2 = loader2.loadClass("java.lang.String");
// 没有双亲委派:string1 != string2(灾难性后果)
// 有双亲委派:string1 == string2(由启动类加载器唯一加载)
保证效果:
- 同一个类在JVM中只有一份Class对象(由同一个类加载器加载)
- 核心类(如
String、Object)全局唯一 - 避免类型混乱和安全漏洞
2. 保护核心类库安全
// 恶意代码尝试替换String类
package java.lang;
public class String {
public String(String original) {
// 窃取密码、信用卡等敏感信息
hackServer(original);
}
}
双亲委派防护:
1. 自定义类加载器尝试加载"java.lang.String"
2. 委派给父类加载器(应用类加载器)
3. 继续委派给父类加载器(扩展类加载器)
4. 继续委派给启动类加载器
5. 启动类加载器成功加载官方的String类
6. 返回官方String类,恶意String类不会被加载
3. 避免类的重复加载
// 场景:多个类加载器需要加载相同的类
public class DuplicationDemo {
public static void main(String[] args) {
// 所有类加载器加载Object类
// 没有双亲委派:每个类加载器都会加载一份Object
// 有双亲委派:启动类加载器加载一次,所有子类共享
}
}
优势:
- 减少内存占用
- 提高类加载效率
- 统一类的定义
核心原则
原则1:向上委派优先
请求顺序:子类 → 父类 → 祖父类 → ... → 启动类加载器
// 伪代码
Class loadClass(String name) {
if (已加载) {
return 已加载的类;
}
if (有父类加载器) {
try {
return parent.loadClass(name); // 向上委派
} catch (ClassNotFoundException) {
// 父类加载失败,继续
}
}
return findClass(name); // 自己加载
}
原则2:向下逐级尝试
加载顺序:启动类加载器 → 扩展类加载器 → 应用类加载器 → 自定义类加载器
启动类加载器尝试加载
↓ 失败
扩展类加载器尝试加载
↓ 失败
应用类加载器尝试加载
↓ 失败
自定义类加载器尝试加载
↓ 失败
抛出ClassNotFoundException
原则3:父类优先生效
// 如果父类加载器能加载,子类加载器的版本不会生效
// classpath: myapp.jar(包含com.example.Utils v1.0)
// customPath: plugins/myapp.jar(包含com.example.Utils v2.0)
CustomClassLoader loader = new CustomClassLoader("plugins");
Class<?> utilsClass = loader.loadClass("com.example.Utils");
// 结果:加载的是v1.0(应用类加载器先加载)
// v2.0不会被加载(父类优先原则)
工作流程图
┌─────────────────────────────────────────────┐
│ 1. 应用调用ClassLoader.loadClass("User") │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ 2. 应用类加载器:检查是否已加载 │
│ → 未加载,委派给父类 │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ 3. 扩展类加载器:检查是否已加载 │
│ → 未加载,委派给父类 │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ 4. 启动类加载器:检查是否已加载 │
│ → 未加载,尝试从JAVA_HOME/lib加载 │
│ → 加载失败,返回null │
└─────────────────┬───────────────────────────┘
│ 加载失败
┌─────────────────▼───────────────────────────┐
│ 5. 扩展类加载器:尝试从ext目录加载 │
│ → 加载失败,抛出异常 │
└─────────────────┬───────────────────────────┘
│ 加载失败
┌─────────────────▼───────────────────────────┐
│ 6. 应用类加载器:从classpath加载 │
│ → 加载成功,返回Class对象 │
└──────────────────────────────────────────────┘
优势分析
优势1:安全性
// 防止核心类被篡改
// 即使自定义类加载器试图加载恶意的java.lang.Object
// 启动类加载器会先加载官方Object,恶意代码无效
保护范围:
java.*
javax.*
sun.*
com.sun.*
优势2:稳定性
// 核心类由启动类加载器加载,不会因为应用重启而重新加载
// 保证JVM运行期间核心类的稳定性
优势3:资源共享
// 同一个类只加载一次,所有子类加载器共享
// 节省内存,提高性能
// 示例
Class<?> stringClass1 = loader1.loadClass("java.lang.String");
Class<?> stringClass2 = loader2.loadClass("java.lang.String");
// stringClass1 == stringClass2(共享同一个Class对象)
劣势与局限
局限1:无法实现类的热替换
// 一旦类被父类加载器加载,无法替换为新版本
// 需要破坏双亲委派才能实现热部署
局限2:父类加载器无法访问子类资源
// 问题:JDBC驱动在classpath,DriverManager在核心库
// DriverManager(启动类加载器)无法加载MySQL驱动(应用类加载器)
// 解决:线程上下文类加载器(破坏双亲委派)
局限3:不适合模块化需求
// 不同模块需要使用不同版本的同一个库
// 双亲委派只能加载一个版本(父类加载器的版本)
// 解决:OSGi平行委派模型
实战示例
示例1:验证双亲委派
public class ParentDelegationVerifyDemo {
public static void main(String[] args) {
// 1. 查看String类的加载器
ClassLoader stringLoader = String.class.getClassLoader();
System.out.println("String类加载器: " + stringLoader);
// 输出:null(由启动类加载器加载)
// 2. 查看当前类的加载器
ClassLoader currentLoader =
ParentDelegationVerifyDemo.class.getClassLoader();
System.out.println("当前类加载器: " + currentLoader);
// 输出:sun.misc.Launcher$AppClassLoader@xxx
// 3. 查看加载器层次
ClassLoader loader = currentLoader;
int level = 1;
while (loader != null) {
System.out.println("层次" + level + ": " + loader);
loader = loader.getParent();
level++;
}
System.out.println("层次" + level + ": Bootstrap ClassLoader (null)");
// 输出:
// 层次1: sun.misc.Launcher$AppClassLoader@xxx
// 层次2: sun.misc.Launcher$ExtClassLoader@xxx
// 层次3: Bootstrap ClassLoader (null)
}
}
示例2:双亲委派保护核心类
public class SecurityDemo {
public static void main(String[] args) {
// 尝试加载自定义的String类
try {
CustomClassLoader loader = new CustomClassLoader();
Class<?> stringClass = loader.loadClass("java.lang.String");
// 结果:加载的是JDK的String类,不是自定义的
System.out.println(stringClass.getClassLoader());
// 输出:null(启动类加载器)
// 验证:创建对象
Object obj = stringClass.newInstance();
System.out.println(obj.getClass().getName());
// 输出:java.lang.String(官方类)
} catch (Exception e) {
e.printStackTrace();
}
}
}
与其他机制的对比
| 机制 | 委派方向 | 适用场景 | 代表技术 |
|---|---|---|---|
| 双亲委派 | 自下而上 | 通用类加载,保证安全 | JDK标准机制 |
| 打破双亲委派 | 自上而下 | SPI机制、热部署 | JDBC、Tomcat |
| 平行委派 | 平行关系 | 模块化系统 | OSGi |
哲学思想
双亲委派机制体现了以下设计哲学:
- 最小权限原则:子类加载器不越权加载核心类
- 单一职责原则:每个类加载器只负责特定范围的类
- 信任链原则:子类信任父类的加载结果
- 防御编程:从架构层面防止恶意代码
总结口诀
双亲委派很重要,
向上委派是王道。
父类优先保安全,
核心类库不会倒。
子类加载需等待,
父类失败才轮到。
类的唯一有保障,
系统稳定又可靠。
面试要点总结
- 核心思想:优先委派父类加载器,父类无法加载时子类才尝试
- 设计目的:保证类的唯一性、保护核心类库安全、避免重复加载
- 工作原理:向上委派 → 向下逐级尝试
- 三大优势:安全性、稳定性、资源共享
- 三大局限:无法热替换、父类无法访问子类资源、不适合模块化
- 典型应用:保护JDK核心类(如String、Object)不被篡改
- 破坏场景:JDBC SPI、Tomcat应用隔离、OSGi模块化、热部署
- 设计哲学:最小权限、单一职责、信任链、防御编程