# 47. 双亲委派模型如何防止类冲突?哪些场景会破坏双亲委派?
# 标准答案
双亲委派模型(Parent Delegation Model)通过 逐级向上委托类加载,确保 核心 Java API 只能由 Bootstrap ClassLoader 加载,防止自定义类覆盖 JDK 关键类,避免类冲突。该模型的基本逻辑是:一个类加载器在尝试加载类时,先委托给父类加载器,只有当父类找不到时,子类加载器才会尝试加载。
然而,在 SPI 机制、类隔离、动态代理 等场景中,双亲委派模型可能会被破坏。
# 答案解析
# 1. 双亲委派模型如何防止类冲突?
核心机制:逐级委托加载
- 类加载器收到类加载请求后,优先交给 父加载器 处理。
- 一直向上委托,直到 Bootstrap ClassLoader(根加载器)。
- 如果父加载器无法找到该类,才会由当前加载器尝试加载。
防止类冲突的关键点
- 避免用户代码篡改 Java 核心类
- 例如
java.lang.String
、java.lang.Object
只能由 Bootstrap ClassLoader 加载,防止恶意篡改系统类。
- 例如
- 保证不同类加载器的类不会互相干扰
- 例如
javax.crypto
相关类必须由 JDK 自带的类加载器加载,否则可能被篡改导致安全漏洞。
- 例如
- 避免用户代码篡改 Java 核心类
类加载器层级
- Bootstrap ClassLoader(启动类加载器):加载
rt.jar
,包含java.lang.*
、java.util.*
等核心类。 - ExtClassLoader(扩展类加载器):加载
lib/ext/
目录下的扩展类,如javax.crypto
。 - AppClassLoader(应用类加载器):加载
classpath
下的应用代码,如com.example.Main
。 - 自定义 ClassLoader:如
Tomcat WebAppClassLoader
、SPI 机制 ClassLoader
,可以加载用户定义的类。
- Bootstrap ClassLoader(启动类加载器):加载
# 2. 哪些场景会破坏双亲委派模型?
尽管双亲委派模型有效防止类冲突,但在某些场景下,为了满足特定需求,会 人为打破双亲委派机制。常见的破坏场景包括:
Java SPI(Service Provider Interface)机制
- SPI 需要 动态加载第三方实现,如
JDBC
需要加载MySQL Driver
,但java.sql.Driver
由 Bootstrap ClassLoader 加载,而MySQL Driver
由 AppClassLoader 加载,导致父加载器无法找到MySQL
相关类。 - 解决方案:
Thread.currentThread().getContextClassLoader()
让 当前线程使用子 ClassLoader 加载 SPI 实现。
- SPI 需要 动态加载第三方实现,如
OSGi(模块化类加载)
- 在 OSGi 体系下,每个模块有自己的
ClassLoader
,模块间可以 部分共享类,但为了避免类冲突,OSGi 不遵循双亲委派,而是采用 自定义的类加载策略(如Import-Package
和Export-Package
)。 - 例如,
Tomcat
使用 不同 WebApp 各自独立的WebAppClassLoader
,确保不同应用的Servlet
不会互相干扰。
- 在 OSGi 体系下,每个模块有自己的
Tomcat、Spring 自定义类加载
- Tomcat 为了支持 不同 Web 应用相互隔离,每个
WebApp
都有独立的WebAppClassLoader
,避免WebA
加载的JAR
干扰WebB
。 - Spring 通过
ClassLoader
动态加载Bean
,如 AOP 代理、字节码增强,可能会破坏默认的委派机制。
- Tomcat 为了支持 不同 Web 应用相互隔离,每个
动态代理(JDK Proxy / CGLIB)
- JDK 动态代理使用
Proxy.newProxyInstance(ClassLoader loader, ...)
创建代理类,其中loader
可能是 自定义的 ClassLoader,可能与双亲委派机制不兼容。 - CGLIB 生成子类代理,类加载器可能不同于目标类的 ClassLoader,导致
ClassCastException
。
- JDK 动态代理使用
自定义
ClassLoader
(热更新、插件机制)- 为了支持 JVM 热部署,如
Tomcat
的reloadable
机制,会自定义ClassLoader
重新加载类,而不是走双亲委派模型。 IDEA
、Eclipse
也会使用 自定义ClassLoader
实现 热替换(HotSwap),避免 JVM 重新启动。
- 为了支持 JVM 热部署,如
# 3. 代码示例
破坏双亲委派:自定义 ClassLoader
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 这里可以自定义类加载逻辑,例如从网络或加密文件加载类
return super.findClass(name);
}
}
public class Test {
public static void main(String[] args) throws Exception {
MyClassLoader myLoader = new MyClassLoader();
Class<?> clazz = myLoader.loadClass("com.example.MyClass");
System.out.println("Class Loaded: " + clazz.getName());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
破坏 SPI 机制示例
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
1
JVM 默认使用 AppClassLoader 加载 MyService
,如果 MyService
由自定义 ClassLoader
加载,则需要手动指定:
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class, Thread.currentThread().getContextClassLoader());
1
# 最佳实践
- 尽量遵循双亲委派模型,避免类冲突。
- 使用
Thread Context ClassLoader
解决 SPI 加载问题,如JDBC Driver
加载。 - 合理设计类加载器层级,如 插件机制 可采用独立
ClassLoader
,避免影响主应用。 - 避免
ClassLoader
泄露,如ThreadLocal
持有ClassLoader
可能导致内存泄漏。
# 深入追问
- OSGi 如何实现类隔离?如何解决多个
ClassLoader
互相调用的问题? - Spring 如何使用
ClassLoader
进行Bean
代理? - Tomcat 如何实现 WebApp 的类加载隔离?
# 相关面试题
- 什么是 Java SPI?为什么 SPI 机制破坏了双亲委派?
- Tomcat 如何通过
WebAppClassLoader
实现类隔离? - 为什么
ClassLoader
可能会导致内存泄漏?如何避免?