这个案例能帮你解释Java中类加载器的双亲委派模型吗

wen java案例 50

这个案例能帮你彻底搞懂Java类加载器的双亲委派模型

📚 目录导读

  1. 双亲委派模型的核心概念
  2. 为什么需要双亲委派?经典案例切入
  3. 自定义java.lang.String引发的思考
  4. Tomcat如何打破双亲委派实现隔离
  5. 双亲委派模型的工作流程详解
  6. 高频面试问答录
  7. 总结与最佳实践

双亲委派模型的核心概念

在Java生态中,类加载器(ClassLoader)负责将字节码加载到JVM内存中,双亲委派模型(Parents Delegation Model)是Java默认采用的类加载策略,其核心思想是:当一个类加载器需要加载某个类时,它首先将加载请求委派给父类加载器,只有当父类加载器无法完成加载时,子加载器才会尝试自行加载

这个案例能帮你解释Java中类加载器的双亲委派模型吗

JVM内置了三个层级的关键类加载器:

  • Bootstrap ClassLoader(启动类加载器):加载 $JAVA_HOME/jre/lib/rt.jar 等核心库
  • Extension ClassLoader(扩展类加载器):加载 $JAVA_HOME/jre/lib/ext 目录下的类
  • Application ClassLoader(应用类加载器):加载 classpath 中用户自定义的类

这种层次结构保证了Java核心API的稳定性和安全性。


为什么需要双亲委派?经典案例切入

核心问题场景

假设没有双亲委派,用户自定义了一个 java.lang.String 类,并在其中添加恶意代码,如果直接由应用类加载器加载,那么核心API将被污染,整个JVM的安全防线瞬间崩塌。

双亲委派的解决方案:当应用类加载器收到加载 java.lang.String 的请求时,它会向上委派给扩展类加载器,扩展类加载器继续向上委派给启动类加载器,启动类加载器在 rt.jar 中找到了标准的 String 类并成功加载,于是用户自定义的恶意 String 永远不会被加载,这就是双亲委派保护Java核心库不被篡改的根本机制。

通俗类比

可以把类加载器想象成一个家族企业:

  • 爷爷(Bootstrap)掌握核心技术(核心API)
  • 爸爸(Extension)拥有扩展业务
  • 儿子(Application)负责外部项目

任何项目需求(类加载请求),都先交给爷爷处理,爷爷能解决就不需要儿子出手;只有爷爷无法处理(找不到类)时,才下放给爸爸,最终落到儿子手里,这种“长老优先”的机制防止了“山寨技术”混入核心业务。


案例一:自定义java.lang.String引发的思考

实验场景

我们尝试编写一个 java.lang.String 类,并尝试通过 Class.forName("java.lang.String") 或直接打印 String.class.getClassLoader() 观察结果。

// 自定义的java.lang.String
package java.lang;
public class String {
    static {
        System.out.println("恶意String被加载!");
    }
}

实际结果

即使在 classpath 中放置了自定义 String,运行后你会发现:

  • 没有任何“恶意String被加载”的输出
  • String.class.getClassLoader() 返回 null(表示由Bootstrap ClassLoader加载)
  • 程序正常使用标准String功能

背后的机制

当JVM启动时,Bootstrap ClassLoader已经预加载了核心类,用户自定义的同名包路径类,由于双亲委派机制,永远不会有被应用类加载器加载的机会,JVM会直接使用高优先级的核心类版本。

重要启示

  • 无法通过自定义同名类替换JDK核心类(安全屏障)
  • 如果需要扩展核心类功能,应使用 -Xbootclasspath/p: 参数(不推荐,破坏安全)
  • 企业实践中,应避免使用 java.* 包名自定义类

案例二:Tomcat如何打破双亲委派实现隔离

矛盾点

Tomcat需要为部署在其中的多个Web应用提供互相隔离的类加载环境,如果严格遵循双亲委派,一个应用修改了某个类,会影响其他应用 —— 这显然不符合Servlet规范。

Tomcat的创新方案

Tomcat创建了自定义的 WebappClassLoader,它修改了加载顺序:

  1. 首先尝试自行加载 Web应用 /WEB-INF/classes/WEB-INF/lib/*.jar 中的类
  2. 如果找不到,再委派给父类加载器

这就是经典的 “先子后父”模式,严格来说是对双亲委派模型的破坏(违反了自底向上委派的约定),但在Java规范中允许通过 getParent() 设置父加载器并重写 loadClass() 方法实现。

代码层面实现

public class WebappClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 1. 检查是否已经加载
        Class<?> clazz = findLoadedClass(name);
        if (clazz != null) return clazz;
        try {
            // 2. 先尝试自行加载(打破委派)
            clazz = findClass(name);
            if (clazz != null) return clazz;
        } catch (ClassNotFoundException e) {
            // 忽略
        }
        // 3. 如果没找到,再委派给父加载器
        return super.loadClass(name);
    }
}

哪些场景需要打破双亲委派?

场景 典型方案 目的
Web应用隔离 Tomcat WebappClassLoader 应用间类隔离,互不干扰
热部署 OSGi框架、JDK9模块化 动态卸载和更新模块
数据库驱动加载 线程上下文类加载器 打破SPI加载限制

双亲委派模型的工作流程详解

源码级流程解读(基于JDK8的 ClassLoader.loadClass()

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 首先检查类是否已被加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 2. 如果父加载器存在,委派给父加载器
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 没有父加载器,则使用Bootstrap ClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器抛出异常,说明无法加载
            }
            if (c == null) {
                // 4. 父加载器无法加载,子加载器自己尝试
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

流程图解

客户端(类加载请求)
    ↓
Application ClassLoader(检查缓存)
    ↓ 没找到
委派给 Extension ClassLoader(检查缓存)
    ↓ 没找到
委派给 Bootstrap ClassLoader(检查缓存)
    ↓ 没找到,且rt.jar中无此类
返回 ClassNotFoundException ← 子加载器逐层尝试
    ↓
Extension ClassLoader 尝试从 ext 目录加载
    ↓ 失败
Application ClassLoader 尝试从 classpath 加载

关键知识点

  • 唯一性保证:同一个类全限定名在JVM中只能被同一个类加载器加载一次
  • 可见性:子加载器可以访问父加载器加载的类,反之不行
  • 缓存机制findLoadedClass() 检查的缓存是类加载器级别的

高频面试问答录

Q1:“如果父加载器加载了某些类,子加载器还能再加载同名类吗?”

A:不能。 双亲委派保证了类加载的唯一性,当父加载器成功加载某个类后,子加载器收到同名请求时,会在第一步 findLoadedClass() 时发现类已被加载(基于类的全限定名+加载器实例的二元组),直接返回父加载器加载的Class对象。

Q2:“破坏双亲委派一定是坏事吗?”

A:不一定。 虽然是默认机制,但有些场景需要主动破坏:

  • 应用服务器隔离:Tomcat、Jetty需要为不同Web应用提供独立的类空间
  • JDBC驱动加载:SPI机制需要线程上下文类加载器打破层级限制
  • 热部署:如Netty、Vert.x等需要动态替换类和JAR包

Q3:“如何查看一个类是由哪个类加载器加载的?”

A: 调用 Class.getClassLoader() 方法:

System.out.println(String.class.getClassLoader()); // null(Bootstrap)
System.out.println(ChromeDriver.class.getClassLoader()); // 应用类加载器
System.out.println(com.sun.crypto.provider.SunJCE.class.getClassLoader()); // 扩展类加载器

Q4:“同一个类可以被不同的类加载器加载多次吗?”

A:可以。 每个类加载器有自己的命名空间,例如Tomcat中两个Web应用部署了同名的 com.example.User 类,它们分别由各自的 WebappClassLoader 加载,两者在JVM中是不同的类,无法互相赋值(会抛出 ClassCastException),这正是隔离的原理。


总结与最佳实践

核心价值

双亲委派模型是Java安全体系的重要基石,它通过层级化的委托机制实现了:

  1. 安全性:核心API免受自定义代码污染
  2. 稳定性:避免类加载混乱导致的内存泄漏
  3. 标准化:统一类加载行为,便于JVM优化

实际开发建议

  • 不要尝试覆盖JDK核心类:即便在本地开发环境成功,在部署环境也会因双亲委派而失效
  • 理解SPI机制:JDBC、Java Naming等SPI接口需要线程上下文类加载器,这是官方认可的打破双亲委派方式
  • 容器化部署注意:在Docker/容器环境下,类加载器行为与标准JDK一致,但需注意基础镜像的JDK版本
  • 调试技巧:遇到 ClassNotFoundExceptionNoClassDefFoundError 时,首先检查类路径优先级,再考虑双亲委派是否导致类不可见

延伸思考

JDK 9引入的模块化系统(JPMS)虽然改变了类加载的组织方式,但双亲委派的思想仍然存在,模块化进一步细化了类的可见性控制,而底层委派逻辑依然是Bootstrap → Platform → Application的模式。

上一篇当前分类已是最后一篇

下一篇怎样用Java的工厂模式重构一个多支付方式的支付系统

抱歉,评论功能暂时关闭!