如何打破双亲委派模型?有哪些场景?

2025年 阅读约 9 分钟 面试指南 · Java面试 · JVM

深入解析打破双亲委派模型的三种方式:重写loadClass方法、线程上下文类加载器(SPI机制)、Tomcat类加载架构,附自定义类加载器实战和面试模拟。

一句话总结

打破双亲委派有三种方式:① 重写 loadClass()(改变加载顺序,如 Tomcat 的 WebappClassLoader 优先自己加载)→ ② 线程上下文类加载器(Thread.setContextClassLoader(),SPI 机制的核心,让 Bootstrap 加载的类能调用用户类)→ ③ SPI + ServiceLoader(JDK 内置的打破机制,核心 API 定义接口,第三方实现,通过线程上下文类加载器加载实现类)。典型场景:JDBC 驱动加载、Tomcat 应用隔离、OSGi 模块化、热部署。

初级理解

三种打破方式

方式原理典型场景
重写 loadClass()改变"先委托父加载器"的顺序Tomcat WebappClassLoader
线程上下文类加载器通过 Thread 传递 AppClassLoaderSPI(JDBC、JNDI)
SPI + ServiceLoader核心库定义接口,第三方实现JDBC 驱动、SLF4J

方式一:重写 loadClass()

// 默认 loadClass():先委托父加载器 protected Class<?> loadClass(String name, boolean resolve) { // ① 检查是否已加载 // ② 委托父加载器 // ③ 父加载器找不到 → 自己 findClass() } // 打破方式:重写 loadClass(),先自己加载 protected Class<?> loadClass(String name, boolean resolve) { // ① 检查是否已加载 // ② 先自己 findClass()(打破双亲委派!) // ③ 自己找不到 → 委托父加载器 } // 注意:JDK 核心类(java.*)仍必须由 Bootstrap 加载 // 否则会有安全问题

中级深入

方式二:线程上下文类加载器(SPI 核心)

// 问题:JDBC 的 DriverManager 在 rt.jar 中,由 Bootstrap 加载 // 但 MySQL 驱动在 classpath 中,由 AppClassLoader 加载 // Bootstrap 加载的类无法访问 AppClassLoader 加载的类! // 解决:线程上下文类加载器 // DriverManager 通过线程上下文获取 AppClassLoader 来加载驱动 // DriverManager 源码(简化) public class DriverManager { static { loadInitialDrivers(); // 使用 SPI 加载驱动 } private static void loadInitialDrivers() { // 获取线程上下文类加载器(默认是 AppClassLoader) ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class); for (Driver d : loader) { registerDriver(d); // 注册驱动 } } } // 线程上下文类加载器设置 Thread.currentThread().setContextClassLoader(myClassLoader); // 默认值:主线程继承 System ClassLoader(AppClassLoader)

SPI 机制完整流程

// SPI(Service Provider Interface): // 1. 核心库定义接口(如 java.sql.Driver) // 2. 第三方提供实现(如 com.mysql.cj.jdbc.Driver) // 3. META-INF/services/ 下配置文件指定实现类 // 4. ServiceLoader 通过线程上下文类加载器加载实现 // META-INF/services/java.sql.Driver 文件内容: // com.mysql.cj.jdbc.Driver // ServiceLoader 加载流程: ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class); // 内部: // 1. 获取线程上下文类加载器 // 2. 读取 META-INF/services/java.sql.Driver // 3. 加载并实例化文件中列出的所有实现类

高级拓展

Tomcat 类加载架构

// Tomcat 的类加载器层次(违反双亲委派) Bootstrap ClassLoader └── System ClassLoader └── Common ClassLoader(Tomcat 和 Web 应用共享) ├── Catalina ClassLoader(Tomcat 自身,隔离) ├── Shared ClassLoader(所有 Web 应用共享) └── Webapp ClassLoader(每个应用独立,打破双亲委派) // WebappClassLoader 的加载顺序(打破双亲委派): // 1. 先从自己缓存中查找 // 2. 如果没找到,先从 Bootstrap 加载(安全限制) // 3. 如果没找到,先从自己路径加载(打破!优先自己) // 4. 如果没找到,再委托父加载器 // 目的: // 1. 不同 Web 应用的类隔离(App1 的 User 类 ≠ App2 的 User 类) // 2. Web 应用可以覆盖父加载器的类(如覆盖 Tomcat 的 jar) // 3. 热部署:销毁旧的 WebappClassLoader,创建新的

自定义类加载器实现热部署

// 热部署原理:用新的类加载器重新加载类 public class HotDeployClassLoader extends ClassLoader { private String classPath; public HotDeployClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] bytes = loadClassData(name); return defineClass(name, bytes, 0, bytes.length); } private byte[] loadClassData(String name) { // 从 classPath 读取 .class 文件 String path = classPath + name.replace('.', '/') + ".class"; try (InputStream is = new FileInputStream(path)) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; int len; while ((len = is.read(buf)) != -1) bos.write(buf, 0, len); return bos.toByteArray(); } catch (IOException e) { throw new RuntimeException(e); } } } // 使用:每次修改类后,创建新的类加载器重新加载 // 旧的类加载器和旧类会被 GC 回收

实战场景

场景:同一个类不同版本共存

// 问题:应用依赖 A.jar(v1) 和 B.jar,B.jar 依赖 A.jar(v2) // 两个版本的 A.jar 如何共存? // 解决:用不同的类加载器加载 ClassLoader cl1 = new MyClassLoader("lib/v1/"); ClassLoader cl2 = new MyClassLoader("lib/v2/"); Class<?> classA_v1 = cl1.loadClass("com.example.A"); Class<?> classA_v2 = cl2.loadClass("com.example.A"); // classA_v1 != classA_v2(不同类加载器加载的同一个类 ≠ 同一个类) // 这就是 OSGi 和 Tomcat 实现类隔离的原理

面试模拟

Q:如何打破双亲委派模型?

A:三种方式:1) 重写 loadClass():改变加载顺序,先自己加载再委托父加载器(Tomcat);2) 线程上下文类加载器:通过 Thread.setContextClassLoader() 传递 AppClassLoader,让 Bootstrap 加载的类能调用用户类(SPI 核心);3) SPI + ServiceLoader:核心库定义接口,通过线程上下文类加载器加载第三方实现(JDBC)。

Q:SPI 机制是什么?为什么需要打破双亲委派?

A:SPI(Service Provider Interface)是 JDK 内置的服务发现机制。核心库定义接口(如 java.sql.Driver),第三方在 META-INF/services/ 下配置实现类,ServiceLoader 加载。需要打破双亲委派是因为:核心库由 Bootstrap 加载,无法访问 AppClassLoader 加载的第三方实现类,所以通过线程上下文类加载器来加载实现类。

Q:Tomcat 为什么要打破双亲委派?

A:1) 应用隔离:不同 Web 应用的类互不影响(各自 WebappClassLoader);2) 版本覆盖:Web 应用可以用自己的 jar 覆盖 Tomcat 的 jar;3) 热部署:销毁旧 WebappClassLoader 创建新的,实现不重启更新应用;4) 安全:防止应用之间的类互相访问。