什么是双亲委派?

深入理解双亲委派模型:设计、实现、本质与突破

双亲委派模型(Parent Delegation Model)是 JVM 类加载器协作加载类的核心规则,也是 Java 类加载机制中最核心的设计之一。它并非 JVM 强制规范,而是java.lang.ClassLoader默认实现的一种 "委派优先" 的类加载策略,其核心目标是保证类加载的唯一性、安全性,以及核心类库的优先加载

下面从「模型定义→核心价值→实现原理→执行流程→关键细节→模型突破」六个维度,彻底讲透双亲委派。

一、双亲委派的核心定义

当任意一个类加载器(ClassLoader)接收到加载类的请求时,它不会立即自己去加载 ,而是先将这个请求 "委派" 给它的父类加载器;父类加载器再向上委派,直到请求到达最顶层的启动类加载器(Bootstrap ClassLoader);只有当父类加载器在自己的加载范围内找不到该类(加载失败)时,子加载器才会尝试自己去加载这个类。

⚠️ 注意:这里的 "双亲" 并非 "父类 + 母类",而是 "父加载器"(逻辑上的层级关系,而非 Java 继承关系);且 "委派" 是单向的 "自上而下",加载失败后的 "回退" 是 "自下而上"。

二、为什么要设计双亲委派?(核心价值)

双亲委派的设计完全是为了解决类加载的 "安全性" 和 "唯一性" 问题,具体体现在三个层面:

1. 防止核心类库被篡改(安全)

Java 核心类库(如java.lang.Stringjava.lang.Object)由启动类加载器(Bootstrap)加载,且启动类加载器仅加载JAVA_HOME/jre/lib/rt.jar等核心 jar 包中的类。

如果没有双亲委派,自定义类加载器可以随意加载一个java.lang.String类(比如篡改equals方法),导致核心类库被恶意替换,JVM 的基础运行逻辑会被破坏。而双亲委派下,自定义类加载器加载java.lang.String的请求会先委派给启动类加载器,启动类加载器会优先加载核心库中的String类,自定义的String类永远不会被加载,从根本上保证了核心类的安全。

2. 避免类的重复加载(唯一性)

类的 "唯一性" 由「类加载器 + 类的全限定名」共同决定(JVM 中,两个类即使字节码完全相同,只要类加载器不同,就是两个不同的类)。

双亲委派下,同一个类的加载请求最终只会被最顶层的能加载它的类加载器处理,避免了不同类加载器重复加载同一个类,导致内存中出现多个相同类的元数据,引发类型转换异常(如ClassCastException)。

3. 保证类加载的层级一致性

父加载器加载的类是 "全局可见" 的,子加载器加载的类仅对自身及子加载器可见。双亲委派保证了 "基础类(核心类)由顶层加载器加载,业务类由应用层加载器加载",符合 JVM 的类加载层级设计,避免类依赖混乱。

三、双亲委派的实现原理(源码级解析)

双亲委派的核心逻辑封装在java.lang.ClassLoaderloadClass(String name, boolean resolve)方法中(JDK 8 为例),这是所有类加载器的核心入口方法。

1. 核心源码

java 复制代码
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    // 1. 加锁:保证类加载的线程安全(<clinit>()执行也是线程安全的)
    synchronized (getClassLoadingLock(name)) {
        // 2. 检查该类是否已经被加载过(缓存优先)
        Class<?> c = findLoadedClass(name);
        if (c == null) { // 未加载过,进入委派流程
            long t0 = System.nanoTime();
            try {
                // 3. 有父加载器则优先委派给父加载器
                if (parent != null) {
                    // 递归调用父加载器的loadClass方法
                    c = parent.loadClass(name, false);
                } else {
                    // 4. 无父加载器(启动类加载器),直接尝试由启动类加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器加载失败(找不到类),抛出异常,进入子加载器自己加载的逻辑
            }

            // 5. 父加载器未加载到,当前加载器自己加载
            if (c == null) {
                long t1 = System.nanoTime();
                // findClass:子类重写的核心方法,实现具体的加载逻辑(如读字节码、defineClass)
                c = findClass(name);

                // 性能统计(JVM内部使用)
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        // 6. 解析类(可选:resolve为true时,触发解析阶段)
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

2. 核心方法拆解

方法 作用
findLoadedClass() 检查类是否已加载(JVM 的类加载缓存),避免重复加载
parent.loadClass() 委派给父加载器加载,递归执行双亲委派逻辑
findBootstrapClassOrNull() 尝试由启动类加载器加载(返回 null 表示加载失败)
findClass() 自定义类加载器的核心扩展点,默认实现直接抛ClassNotFoundException,子类需重写(如读取字节码文件、网络字节流等)
defineClass() 将字节码数组转换为Class对象(JVM 底层实现,不可重写),是类加载的最终环节

3. 类加载器的 "父 - 子" 关系(非继承)

类加载器的parent属性是「逻辑父加载器」,而非 Java 继承关系:

  • 应用程序类加载器(AppClassLoader)的parent是扩展类加载器(ExtClassLoader);
  • 扩展类加载器的parent是启动类加载器(Bootstrap ClassLoader,C++ 实现,parent为 null);
  • 自定义类加载器的parent默认是应用程序类加载器(可通过构造函数指定)。

示例:

java 复制代码
public static void main(String[] args) {
    ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
    System.out.println(appClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
    System.out.println(appClassLoader.getParent()); // sun.misc.Launcher$ExtClassLoader@1540e19d
    System.out.println(appClassLoader.getParent().getParent()); // null(启动类加载器无Java对象)
}

四、双亲委派的完整执行流程(以加载业务类为例)

假设我们加载一个业务类com.example.MyService,触发应用程序类加载器(AppClassLoader)的加载请求,流程如下:

  1. AppClassLoader 接收到请求 → 调用loadClass("com.example.MyService")
  2. 检查缓存findLoadedClass()检查是否已加载,未加载则进入委派;
  3. 委派给父加载器 ExtClassLoader :调用ExtClassLoader.loadClass()
  4. ExtClassLoader 委派给 BootstrapClassLoader :ExtClassLoader 的parent是 null,调用findBootstrapClassOrNull("com.example.MyService")
  5. BootstrapClassLoader 加载失败:核心库中无该类,返回 null;
  6. ExtClassLoader 自己加载 :调用ExtClassLoader.findClass(),扩展库中无该类,抛出ClassNotFoundException
  7. AppClassLoader 自己加载 :捕获异常后,调用AppClassLoader.findClass(),从 classpath 中读取com/example/MyService.class字节码;
  8. 生成 Class 对象 :调用defineClass()将字节码转为Class对象,返回结果;
  9. 缓存并返回 :将Class对象缓存到 JVM,后续加载直接从缓存获取。

流程图简化:

复制代码
AppClassLoader → ExtClassLoader → BootstrapClassLoader(顶层)
                  ↑                ↓(加载失败)
                  ←                ExtClassLoader(加载失败)
                  ↓
AppClassLoader(自己加载成功)→ 返回Class对象

五、双亲委派的关键细节(易混淆点)

1. "父加载器" 不是 "父类"

自定义类加载器继承自ClassLoader,但parent属性是逻辑上的父加载器(如 AppClassLoader),而非继承关系中的父类。例如:

java 复制代码
public class CustomClassLoader extends ClassLoader {
    public CustomClassLoader() {
        // 父加载器默认是AppClassLoader
        super(); 
        // 也可指定父加载器:super(ClassLoader.getSystemClassLoader().getParent());
    }
}

2. 启动类加载器(Bootstrap)的特殊性

  • 由 C++ 实现,无对应的 Java 类(getParent()返回 null);
  • 仅加载rt.jar等核心 jar 包,且仅识别包名以java.javax.开头的类;
  • 无法被 Java 代码直接引用,只能通过findBootstrapClassOrNull()间接调用。

3. 接口的加载不触发父接口初始化

类初始化时会触发父类初始化,但接口初始化时不会触发父接口初始化(仅在使用父接口的静态字段时才初始化),这是双亲委派在初始化阶段的特殊规则。

4. 数组类的加载不遵循双亲委派

数组类(如String[])由 JVM 直接生成,不由类加载器加载;但数组的元素类型 (如String)仍遵循双亲委派加载。

六、双亲委派模型的 "突破"(为什么需要打破?)

双亲委派是默认策略,但并非强制,实际场景中为了满足灵活的类加载需求,会主动突破双亲委派,核心场景如下:

1. Tomcat 的类加载器(Web 应用隔离)

Tomcat 为每个 Web 应用创建独立的WebappClassLoader,其核心逻辑是:优先加载应用内的类,而非委派给父加载器(打破 "先委派父加载器" 的规则)。

原因:不同 Web 应用可能依赖同一类的不同版本(如应用 A 依赖 Spring 5,应用 B 依赖 Spring 6),如果遵循双亲委派,会由 AppClassLoader 加载其中一个版本,导致另一个应用冲突。Tomcat 的类加载顺序:

复制代码
WebappClassLoader(应用内class文件)→ WEB-INF/lib → CommonClassLoader → CatalinaClassLoader → ExtClassLoader → BootstrapClassLoader

2. JDBC 驱动加载(SPI 机制)

JDBC 的java.sql.Driver接口由 BootstrapClassLoader 加载,但驱动实现类(如 MySQL 的com.mysql.cj.jdbc.Driver)在 classpath 下(由 AppClassLoader 加载)。

如果遵循双亲委派,BootstrapClassLoader 加载Driver接口时,无法加载 AppClassLoader 中的实现类,因此需要通过线程上下文类加载器(Thread Context ClassLoader) 突破:

  • JVM 启动时,主线程的上下文类加载器是 AppClassLoader;
  • JDBC 通过Thread.currentThread().getContextClassLoader()获取 AppClassLoader,加载驱动实现类,绕过双亲委派的自上而下委派逻辑。

3. 热部署 / 动态加载(如 OSGi、JRebel)

热部署需要卸载已加载的类并重新加载,而双亲委派下,内置类加载器加载的类无法被卸载(类加载器本身不会被 GC)。因此自定义类加载器会重写loadClass(),不遵循委派逻辑,加载后通过销毁类加载器实现类卸载。

4. 模块化(Java 9+ Module)

Java 9 引入模块化系统(Module),双亲委派模型被弱化:模块的加载优先由模块层决定,而非单纯的父加载器委派,核心类库的加载逻辑也适配了模块化,但底层仍保留委派的核心思想。

七、总结:双亲委派的本质

双亲委派的本质是 **"自上而下委派,自下而上加载"** 的类加载策略,核心目标是保证核心类库的安全和类的唯一性。它是 JVM 类加载的 "默认规则",而非 "强制规范"------ 通过重写ClassLoaderloadClass()方法,可灵活调整或打破该规则,以满足不同场景的需求(如应用隔离、SPI 加载、热部署)。

理解双亲委派,不仅能解决类冲突、类加载失败等实际问题,更能深入理解 JVM 对类的管理逻辑 ------ 类的 "身份" 由「类加载器 + 全限定名」决定,这是 Java 动态扩展和安全机制的基础。

相关推荐
集大周杰伦1 小时前
基于 Python 的 ADS 自动化仿真框架与 API 使用指南
python·自动化·ads 自动化仿真·ads 程控·ads python
傻啦嘿哟1 小时前
Python高效实现Excel与TXT文本文件数据转换指南
开发语言·python·excel
七宝大爷1 小时前
第一个CUDA程序:从向量加法开始
android·java·开发语言
木心爱编程1 小时前
Qt C++ 插件开发指南:插件架构设计与动态加载实战
开发语言·c++·qt
有什么东东1 小时前
redis实现店铺类型查看
java·开发语言·redis
Henry Zhu1231 小时前
23种设计模式介绍以及C语言实现
c语言·开发语言·设计模式
AAIshangyanxiu1 小时前
基于R语言机器学习遥感数据处理与模型空间预测技术及实际项目案例分析
开发语言·机器学习·r语言·生态遥感·空间预测
我送炭你添花1 小时前
我送炭你献花:Pelco KBD300A 模拟器项目总览
python·功能测试·pyqt·运维开发