双亲委派模型是classloader.loadclass()的默认实现逻辑:先委托父加载器加载,父为null时由bootstrap加载;tomcat为支持多应用隔离和热部署而破坏该机制,采用先自加载再委派的反向策略。

双亲委派模型到底怎么工作的?
双亲委派不是强制规范,而是 ClassLoader.loadClass() 方法的默认实现逻辑:先委托父加载器尝试加载,父加载器为 null 时才由启动类加载器(Bootstrap)接手;所有非启动类加载器都继承自 java.lang.ClassLoader,其 loadClass() 默认会调用 parent.loadClass(),形成链式向上查询。
关键点在于:委托发生在 loadClass(),而非 findClass() —— 后者才是子类真正负责定位并定义类的地方,且默认抛出 NoClassDefFoundError。
- 启动类加载器(Bootstrap):C++ 实现,
lib/rt.jar等核心类,无 Java 对象引用,ClassLoader.getSystemClassLoader().getParent()返回null - 扩展类加载器(Extension):加载
lib/ext/下 JAR,由sun.misc.Launcher$ExtClassLoader实现 - 应用程序类加载器(AppClassLoader):加载
-classpath或CLASSPATH指定路径,是Thread.currentThread().getContextClassLoader()默认值
为什么 Tomcat 要破坏双亲委派?
因为 Web 容器必须支持:多个应用部署相同第三方库(如不同版本的 log4j),互不干扰;应用可覆盖容器提供的基础类(如自定义 javax.servlet.http.HttpServlet 子类);热部署时卸载旧类、加载新类——这些需求与“类由顶层统一加载”天然冲突。
Tomcat 的解决方案是:为每个 WebApp 创建独立的 WebAppClassLoader,它**重写了 loadClass(),优先调用 findClass() 自己加载 /WEB-INF/classes 和 /WEB-INF/lib/ 中的类,仅在加载失败后才委托父加载器**。这属于典型的“先自加载、再委派”,即反向双亲委派。
立即学习“Java免费学习笔记(深入)”;
注意:java.*、javax.*(除 javax.servlet.* 等少数例外)、org.apache.catalina.* 等包仍强制走父委派,避免容器自身类被污染。
Thread.currentThread().getContextClassLoader() 是谁?什么时候必须用它?
这个上下文类加载器(简称 TCCL)默认是 AppClassLoader,但框架(如 JNDI、JDBC、XML 解析器)在运行时往往需要加载“当前业务模块”的类,而它们本身由 Bootstrap 或 Extension 加载,无法直接访问应用类路径 —— 这时候就必须通过 TCCL 中转。
典型场景:
- JDBC 驱动注册:
DriverManager在 Bootstrap 中,但com.mysql.cj.jdbc.Driver在应用 classpath,靠 TCCL 加载 - SLF4J 绑定具体日志实现(如
logback-classic):桥接层在 core jar 中,实现类在应用依赖里 - Spring Boot 的
spring-boot-devtools热替换:通过自定义 TCCL 切换类加载实例
常见错误:在静态工具类中硬编码 MyUtil.class.getClassLoader(),结果拿到的是该工具类所在 jar 的加载器(可能是 Bootstrap 或 Extension),导致找不到业务类 —— 此时应改用 Thread.currentThread().getContextClassLoader()。
如何主动破坏双亲委派?写一个最小可验证例子
核心就是重写 loadClass(),跳过 super.loadClass() 的默认流程,改为先 findClass(),失败再调用父类逻辑。下面是一个极简自定义类加载器,从指定目录加载 .class 文件:
public class MyClassLoader extends ClassLoader {
private final File classDir;
public MyClassLoader(File classDir, ClassLoader parent) {
super(parent);
this.classDir = classDir;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 先查已加载的类(避免重复定义)
Class<?> c = findLoadedClass(name);
if (c != null) return c;
// 2. 【关键】不走 super.loadClass(),而是先自己找
try {
byte[] data = loadByteCode(name);
if (data != null) {
c = defineClass(name, data, 0, data.length);
if (resolve) resolveClass(c);
return c;
}
} catch (IOException ignored) {}
// 3. 自己找不到,才委托父加载器(即“破坏后仍保留兜底”)
return super.loadClass(name, resolve);
}
private byte[] loadByteCode(String name) throws IOException {
String path = classDir.getAbsolutePath() + "/" + name.replace('.', '/') + ".class";
File file = new File(path);
if (!file.exists()) return null;
return Files.readAllBytes(file.toPath());
}
}
使用时注意:不能用 new MyClassLoader(...).loadClass("X") 直接测试,因为此时父加载器是 AppClassLoader,它会提前加载掉 X(如果已在 classpath);应确保目标类不在任何上级 classpath 中,或显式设置父加载器为 null(慎用,会失去对 java.* 的访问)。
真正容易被忽略的是:破坏委派后,类的可见性、静态变量隔离、JNI 库路径、资源查找(getResource())等行为都会随之变化,不是换个加载器就完事——必须同步考虑类加载器作用域内的整个运行时契约。










