一、双亲委派模型回顾
1. 标准双亲委派流程
java
复制
下载
// ClassLoader.loadClass() 方法核心逻辑
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;
}
}
2. 类加载器层次结构
text
复制
下载
Bootstrap ClassLoader (启动类加载器)
↑
Extension ClassLoader (扩展类加载器)
↑
Application ClassLoader (应用类加载器)
↑
Custom ClassLoader (自定义类加载器)
二、破坏双亲委派的主要场景
1. SPI机制(Service Provider Interface)
1.1 JDBC SPI破坏原理
java
复制
下载
// JDBC DriverManager的类加载机制
public class DriverManager {
static {
// 初始化时加载所有Driver
loadInitialDrivers();
}
private static void loadInitialDrivers() {
// 使用ServiceLoader加载驱动
ServiceLoader<Driver> loadedDrivers =
ServiceLoader.load(Driver.class);
// 这里的Driver.class是由启动类加载器加载的
// 但Driver实现类需要由应用类加载器加载
// 破坏了双亲委派:父加载器请求子加载器加载类
}
}
// ServiceLoader.load()方法
public static <S> ServiceLoader<S> load(Class<S> service) {
// 关键:使用线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(service, cl);
}
1.2 上下文类加载器(ContextClassLoader)机制
java
复制
下载
// 线程上下文类加载器的设置和获取
public class Thread implements Runnable {
private ClassLoader contextClassLoader;
public void setContextClassLoader(ClassLoader cl) {
// 可以在运行时动态设置
this.contextClassLoader = cl;
}
public ClassLoader getContextClassLoader() {
return contextClassLoader;
}
}
// JDBC使用示例
Connection conn = DriverManager.getConnection(url, user, password);
// 内部过程:
// 1. DriverManager由启动类加载器加载
// 2. 但mysql-connector-java.jar由应用类加载器加载
// 3. 通过Thread.currentThread().getContextClassLoader()打破双亲委派
2. Tomcat的类加载器设计
2.1 Tomcat类加载器架构
text
复制
下载
Bootstrap
↑
System
↑
Common
↗ ↖
Webapp1 Webapp2 ← 每个Web应用独立类加载器
↑ ↑
Jasper Jasper
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
2.2 Web应用隔离的实现
java
复制
下载
// Tomcat的WebappClassLoader
public class WebappClassLoader extends URLClassLoader {
@Override
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查本地缓存
Class<?> clazz = findLoadedClass0(name);
if (clazz != null) return clazz;
// 2. 检查JVM缓存
clazz = findLoadedClass(name);
if (clazz != null) return clazz;
// 3. 屏蔽J2SE和J2EE的核心类(不向上委托)
if (isJ2SEClass(name) || isJ2EEClass(name)) {
try {
clazz = getSystemClassLoader().loadClass(name);
if (clazz != null) return clazz;
} catch (ClassNotFoundException e) {
// 继续
}
}
// 4. 检查委托列表(某些包委托给父加载器)
if (isDelegatedPackage(name)) {
try {
clazz = getParent().loadClass(name);
if (clazz != null) return clazz;
} catch (ClassNotFoundException e) {
// 继续
}
}
// 5. 先在自己的Web应用内查找(打破双亲委派)
try {
clazz = findClass(name);
if (clazz != null) return clazz;
} catch (ClassNotFoundException e) {
// 继续
}
// 6. 最后委托给父类加载器
return super.loadClass(name, resolve);
}
}
// 判断是否J2SE类
private boolean isJ2SEClass(String name) {
return name.startsWith("java.") ||
name.startsWith("javax.") ||
name.startsWith("sun.");
}
}
2.3 Tomcat打破双亲委派的原因
java
复制
下载
// 场景1:不同Web应用使用不同版本的库
// WebApp1使用Spring 4.x,WebApp2使用Spring 5.x
// 如果不打破双亲委派,两者会共用同一个Spring类
// 场景2:热部署支持
// Tomcat需要在不重启容器的前提下重新加载Web应用
public class HotDeployClassLoader extends URLClassLoader {
// 每个版本使用新的ClassLoader实例
// 旧版本的ClassLoader可以被GC回收
}
// 场景3:JSP编译支持
// Jasper编译器需要动态生成Servlet类
public class JasperClassLoader extends URLClassLoader {
// JSP编译的类需要在Web应用类加载器中加载
// 而不是共享的Common ClassLoader
}
3. OSGi(Open Service Gateway Initiative)
3.1 OSGi的类加载模型
text
复制
下载
Bundle A ClassLoader
↗ ↖
Import Package Export Package
↑ ↓
Bundle B ClassLoader
3.2 OSGi的平级类加载器委托
java
复制
下载
// OSGi的BundleClassLoader实现
public class BundleClassLoader extends ClassLoader {
// OSGi不使用标准的双亲委派
// 而是使用图状依赖关系的类加载
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1. 查找本地Bundle
Class<?> clazz = findLocalClass(name);
if (clazz != null) return clazz;
// 2. 查找依赖的Bundle(平级查找,不是父级)
clazz = findDependentBundleClass(name);
if (clazz != null) return clazz;
// 3. 查找Fragment Bundle
clazz = findFragmentClass(name);
if (clazz != null) return clazz;
// 4. 查找DynamicImport
clazz = findDynamicImportClass(name);
if (clazz != null) return clazz;
// 5. 最后才委托给父类加载器
return super.loadClass(name, resolve);
}
private Class<?> findDependentBundleClass(String name) {
// 根据Import-Package声明查找依赖的Bundle
// 这是平级类加载器之间的委托,不是父子关系
for (Bundle dependency : getDependencies()) {
try {
return dependency.loadClass(name);
} catch (ClassNotFoundException e) {
// 继续查找
}
}
return null;
}
}
3.3 OSGi打破双亲委派的原因
java
复制
下载
// 原因1:模块化隔离
// 每个Bundle是独立的模块,有自己的类加载器
// 不同Bundle可以使用不同版本的库
// 原因2:动态性支持
// Bundle可以动态安装、更新、卸载
// 需要独立的类加载空间
// 原因3:服务注册与发现
// OSGi的服务模型需要类加载器隔离
public class ServiceTracker {
// 服务实现可以在运行时动态替换
// 需要类加载器隔离来支持热插拔
}
4. 热部署与动态代码更新
4.1 JRebel热部署实现
java
复制
下载
// JRebel的类重加载机制
public class JRebelClassLoader extends ClassLoader {
private Map<String, byte[]> reloadedClasses = new ConcurrentHashMap<>();
@Override
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1. 检查是否是重加载的类
byte[] reloadedBytes = reloadedClasses.get(name);
if (reloadedBytes != null) {
// 重新定义类,打破双亲委派
return defineClass(name, reloadedBytes, 0, reloadedBytes.length);
}
// 2. 标准双亲委派
return super.loadClass(name, resolve);
}
public void reloadClass(String name, byte[] newBytes) {
// 存储新的类字节码
reloadedClasses.put(name, newBytes);
// 需要打破双亲委派,让新的类定义生效
// 否则会加载到旧的缓存类
}
}
4.2 Spring Boot DevTools热重启
java
复制
下载
// 重启类加载器实现
public class RestartClassLoader extends URLClassLoader {
// 使用两个类加载器实现重启
// baseClassLoader: 加载不变的库(如第三方jar)
// restartClassLoader: 加载应用程序类
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1. 排除的包走父类加载器
if (isExcludedPackage(name)) {
return getParent().loadClass(name);
}
// 2. 应用类自己加载(打破双亲委派)
try {
Class<?> clazz = findClass(name);
if (clazz != null) return clazz;
} catch (ClassNotFoundException e) {
// 继续
}
// 3. 库类委托给baseClassLoader
return getBaseClassLoader().loadClass(name);
}
}
三、实现自定义类加载器打破双亲委派
1. 自定义类加载器实现
java
复制
下载
public class CustomClassLoader extends ClassLoader {
// 类文件路径
private String classPath;
public CustomClassLoader(String classPath) {
// 关键:不传递父加载器,或传递null
// 这样就不会委托给父加载器
super(null); // 打破双亲委派的关键
this.classPath = classPath;
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 先检查是否已加载
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 2. 完全跳过父类加载器,自己直接加载
// 这是打破双亲委派的关键
try {
c = findClass(name);
} catch (ClassNotFoundException e) {
// 3. 自己加载失败,才尝试父加载器
// 可以根据需要决定是否抛出异常
return super.loadClass(name, resolve);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 从指定路径加载类文件
byte[] data = loadClassData(name);
if (data == null) {
throw new ClassNotFoundException(name);
}
return defineClass(name, data, 0, data.length);
}
private byte[] loadClassData(String className) {
// 读取.class文件
String path = classPath + className.replace('.', '/') + ".class";
try (InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
} catch (Exception e) {
return null;
}
}
}
2. 实现场景:插件化架构
java
复制
下载
// 插件管理器
public class PluginManager {
private Map<String, PluginClassLoader> pluginLoaders = new HashMap<>();
public void loadPlugin(String pluginId, String pluginPath) {
// 为每个插件创建独立的类加载器
PluginClassLoader loader = new PluginClassLoader(pluginPath);
pluginLoaders.put(pluginId, loader);
// 加载插件入口类
Class<?> pluginClass = loader.loadClass("com.plugin.Main");
Plugin plugin = (Plugin) pluginClass.newInstance();
plugin.initialize();
}
// 插件类加载器
class PluginClassLoader extends URLClassLoader {
public PluginClassLoader(String pluginPath) {
// 关键:父加载器设置为null或特殊的类加载器
// 避免插件类污染主应用的类空间
super(new URL[0], null);
// 添加插件路径
addURL(new File(pluginPath).toURI().toURL());
}
@Override
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1. 检查是否是系统类(java.、javax.等)
if (name.startsWith("java.") || name.startsWith("javax.")) {
return getSystemClassLoader().loadClass(name);
}
// 2. 先在自己的插件路径查找(打破双亲委派)
try {
Class<?> c = findClass(name);
if (c != null) return c;
} catch (ClassNotFoundException e) {
// 继续
}
// 3. 共享的公共库类委托给共享类加载器
if (isSharedLibrary(name)) {
return getSharedClassLoader().loadClass(name);
}
// 4. 最后才使用标准双亲委派
return super.loadClass(name, resolve);
}
}
}
四、打破双亲委派的原理分析
1. 原理总结
java
复制
下载
// 打破双亲委派的本质是修改 loadClass() 方法的逻辑
public class BreakingParentDelegationClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 标准双亲委派的顺序:
// 1. findLoadedClass() // 检查是否已加载
// 2. parent.loadClass() // 委托父加载器 ← 通常在这里打破
// 3. findClass() // 自己加载
// 打破双亲委派的常见方式:
// 方式1:先自己加载,失败才委托父加载器(反向委托)
// 方式2:某些包自己加载,某些包委托父加载器(条件委托)
// 方式3:完全自己加载,不委托父加载器(隔离)
// 方式4:委托给非父类的其他加载器(网状委托)
}
}
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
2. 打破双亲委派的风险
java
复制
下载
class RisksOfBreakingParentDelegation {
// 风险1:类型安全破坏
void typeSafetyIssue() {
// 同一个类被不同类加载器加载,被认为是不同的类型
Class<?> class1 = loader1.loadClass("com.example.MyClass");
Class<?> class2 = loader2.loadClass("com.example.MyClass");
// class1 != class2,即使字节码完全相同
// 导致 ClassCastException
}
// 风险2:内存泄漏
void memoryLeakIssue() {
// 每个类加载器都有自己的命名空间
// 无法卸载类会导致 PermGen/Metaspace OOM
// 特别是热部署场景
}
// 风险3:资源浪费
void resourceWasteIssue() {
// 同一个类被多个类加载器加载多次
// 浪费内存和加载时间
}
// 风险4:类版本冲突
void versionConflictIssue() {
// 不同模块使用不同版本的库
// 通过不同类加载器隔离
// 但传递对象时可能遇到兼容性问题
}
}
3. 最佳实践
java
复制
下载
class BestPractices {
// 1. 明确打破双亲委派的目的
enum BreakingPurpose {
MODULE_ISOLATION, // 模块隔离
HOT_DEPLOYMENT, // 热部署
PLUGIN_ARCHITECTURE, // 插件架构
VERSION_ISOLATION, // 版本隔离
SECURITY_ISOLATION // 安全隔离
}
// 2. 控制破坏范围
class ControlledBreakingClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) {
// 只对特定包打破双亲委派
if (shouldBreakDelegation(name)) {
return loadClassWithoutDelegation(name);
}
// 其他包使用标准双亲委派
return super.loadClass(name, resolve);
}
private boolean shouldBreakDelegation(String className) {
// 明确哪些包需要特殊处理
return className.startsWith("com.plugin.") ||
className.startsWith("com.dynamic.");
}
}
// 3. 提供类加载器通信机制
interface ClassLoaderBridge {
// 不同类加载器间传递对象的机制
Object transferObject(Object obj, ClassLoader targetLoader);
// 类型转换检查
boolean isCompatible(Class<?> sourceType, Class<?> targetType);
}
}
五、Java 9+模块化系统的影响
1. 模块化系统下的类加载
java
复制
下载
// Java 9+的模块化类加载
public class ModuleClassLoader extends ClassLoader {
// 模块化系统引入了新的类加载规则
// 1. 模块声明依赖关系
// 2. 类加载基于模块依赖图
// 3. 强封装性:非导出包对外不可见
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 在模块化系统中:
// 1. 类加载器委托关系被模块依赖关系替代
// 2. 模块路径替代了类路径
// 3. 服务加载通过模块声明实现
// 模块化实际上没有打破双亲委派
// 而是引入了更精细的模块隔离机制
}
}
2. 模块化与双亲委派的兼容
java
复制
下载
// 模块化系统兼容双亲委派
class ModuleCompatibility {
// Java 9+ 中双亲委派仍然存在
// 但增加了模块化约束:
// 1. 启动类加载器 → 加载java.base等核心模块
// 2. 平台类加载器 → 加载其他平台模块
// 3. 应用类加载器 → 加载应用模块
// 模块化系统的特点:
// - 模块间的类可见性由module-info.java控制
// - 服务发现通过provides/uses声明
// - 打破了传统的"全有或全无"的类可见性
}
六、总结
打破双亲委派的核心场景:
-
SPI机制:父加载器需要调用子加载器加载实现类
-
应用服务器:Web应用隔离、热部署、版本隔离
-
OSGi模块化:Bundle间的平级类加载委托
-
热部署工具:动态代码更新和重加载
-
插件化架构:插件隔离和动态加载
设计原则:
-
明确目的:只有在必要时才打破双亲委派
-
控制范围:只对特定包或场景打破委派
-
考虑兼容性:处理类型安全和版本冲突
-
资源管理:注意类加载器的生命周期和内存泄漏
-
利用新特性:Java 9+的模块化提供更优雅的解决方案
双亲委派模型的破坏是Java生态灵活性的体现,但需要谨慎使用。理解其原理和场景,才能在适当的时候做出正确的设计选择。