什么是双亲委派?怎么破坏?

#Java#面试八股

什么是双亲委派?怎么破坏?

结论先行:双亲委派模型(parent delegation model)是指类加载器收到加载请求时,先委派给父加载器,父加载器无法完成才自己加载。它的价值是保证核心类库唯一——java.lang.String 永远由启动类加载器提供,用户无法塞一个同名类进来。破坏方式有三条:重写 loadClass() 走自己的顺序(Tomcat)、用线程上下文类加载器(TCCL)让父加载器反向调用子加载器(JDBC)、以及用网状委派替代树状(OSGi)。

一、委派链路

           ┌──────────────────────┐
 请求 ───→ │ Bootstrap ClassLoader│  ← jre/lib、JDK 9+ 的 jrt:/modules
           └──────────┬───────────┘
                      │ 找不到
           ┌──────────▼───────────┐
           │ Extension / Platform │  ← jre/lib/ext、JDK 9+ 的 jdk.* 模块
           └──────────┬───────────┘
                      │ 找不到
           ┌──────────▼───────────┐
           │ Application (System) │  ← -classpath、-cp
           └──────────┬───────────┘
                      │ 找不到
           ┌──────────▼───────────┐
           │ 自定义 ClassLoader   │  ← 插件目录、热部署、加密字节流
           └──────────────────────┘

每一层只认自己的路径,找不到就返回 null,由发起请求的那一层「自下而上」回调 findClass()。

二、loadClass 的骨架

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> c = findLoadedClass(name);          // 1. 已加载则直接返回
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false); // 2. 委派父加载器
                } else {
                    c = findBootstrapClassOrNull(name); // 3. 父为空则找启动类加载器
                }
            } catch (ClassNotFoundException ignored) {
            }
            if (c == null) {
                c = findClass(name);                  // 4. 父加载器都失败,自己找
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

模板方法把「找字节流」的活留给了 findClass()。所以自定义加载器应该重写 findClass() 而不是 loadClass(),这样委派语义仍然保留。

三、三条破坏路径

方式谁在用做法后果
重写 loadClassTomcat、Spring Boot 的可执行 JAR先在自己目录找,找不到再委派父加载器实现应用间类隔离、不同版本并存
线程上下文类加载器JDBC、JNDI、JAXPThread.currentThread().setContextClassLoader(...),父层代码通过 TCCL 反向加载解决 SPI 场景下父加载器看不见实现类的问题
网状委派OSGi、部分插件框架按 Bundle 依赖关系横向委派,而非固定父链支持模块热插拔,但依赖图复杂易出环

Tomcat 的 WebappClassLoader 就是典型:每个 Web 应用一个加载器,WEB-INF/classes 与 WEB-INF/lib 优先,javax.servlet 等容器 API 则委派给共享加载器,这样两个应用可以跑不同版本的 Spring。

四、TCCL 的代码形态

// 父加载器加载的框架代码,需要拿到子加载器里的实现类
public class SpiLoader {
    public static <T> T load(Class<T> api) throws Exception {
        ClassLoader tccl = Thread.currentThread().getContextClassLoader();
        Class<?> impl = Class.forName("com.example.MyImpl", true, tccl);
        return api.cast(impl.getDeclaredConstructor().newInstance());
    }

    public static void main(String[] args) throws Exception {
        Thread.currentThread().setContextClassLoader(
                new java.net.URLClassLoader(new java.net.URL[]{
                        new java.io.File("plugins/impl.jar").toURI().toURL()
                }));
        Runnable r = load(Runnable.class);
        r.run();
    }
}

DriverManager 之所以能加载各厂商驱动,靠的就是启动类加载器加载的 DriverManager 通过 TCCL 去扫 META-INF/services/java.sql.Driver。

五、JDK 9 模块化后的变化

变化点JDK 8JDK 9+
扩展类加载器ExtClassLoader改为 PlatformClassLoader
启动类加载器路径rt.jarjrt:/ 模块系统
委派顺序严格父优先先父后子,但模块内类优先由定义模块的加载器加载
核心包保护preDefineClass 检查 java. 前缀同上,并增加模块封装(--add-opens 才可反射)

六、常见追问

追问回答要点
双亲委派能防止什么防止核心类被同名类覆盖(安全)、防止同一个类被重复加载(一致性)
怎么自己写一个绕过委派的加载器重写 loadClass,去掉 parent.loadClass 直接 findClass;但 java.* 包会被 preDefineClass 拒绝
Class.forName 用哪个加载器默认用调用方的加载器,可显式传 ClassLoader;这也是 TCCL 存在的理由
Spring Boot 为什么能一个 JAR 起服务LaunchedURLClassLoader 从嵌套 JAR 读字节流,同时保留父委派加载 JDK 类
破坏双亲委派有风险吗有,可能出现同一个类被两个加载器各加载一份,instanceof 与强转失败,排查困难

小结

双亲委派是「父优先」的默认约定,保障核心类唯一;破坏它的动机都是「父加载器看不到子加载器的东西」或「需要版本隔离」。答题时给出委派链路 + 三条破坏路径 + 一个真实案例,就够了。

笔记加载中…