北屋教程网

专注编程知识分享,从入门到精通的编程学习平台

Java 类加载的双亲委派机制详解_java类加载为什么用双亲委派

一、引言

在 Java 开发中,我们每天都在写类、调用类、实例化对象,但你有没有想过:一个 .class 文件是如何被 JVM 加载进内存,并最终变成一个可以使用的 Java 对象的?

这个过程的核心,就是 类加载机制(Class Loading Mechanism),而其中最核心、最经典的设计,就是我们今天要讲的 —— 双亲委派机制(Parent Delegation Model)

理解双亲委派,不仅能帮助你深入 JVM 底层,更能帮你解决实际开发中遇到的类冲突、ClassNotFoundException、类隔离、热部署等问题。


二、什么是“双亲委派”?

“双亲委派”这个名字听起来有点奇怪 —— Java 类加载器难道有两个“父亲”?其实不是。“双亲”是英文 “Parent” 的直译,更准确的说法应该是 “父级委派机制”

它的定义非常简单:

当一个类加载器收到类加载请求时,它首先不会自己去加载这个类,而是把这个请求“委托”给它的父类加载器去完成。每一层都是如此,直到最顶层的启动类加载器。只有当父加载器无法加载该类时(即在其搜索路径中找不到),子加载器才会尝试自己加载。

这是一种“自底向上委托,自顶向下加载”的机制。


三、Java 类加载器的层级结构

在 JVM 中,默认存在三层类加载器,构成一棵“树”:

1. 启动类加载器(Bootstrap ClassLoader)

  • 由 C++ 实现,是 JVM 的一部分。
  • 负责加载 Java 核心类库,如 rt.jarjava.lang.*java.util.* 等。
  • 路径:$JAVA_HOME/jre/lib
  • 它是所有类加载器的“根”,没有父加载器。

2. 扩展类加载器(Extension ClassLoader)

  • 由 Java 实现(sun.misc.Launcher$ExtClassLoader)。
  • 负责加载扩展目录中的类:$JAVA_HOME/jre/lib/extjava.ext.dirs 指定路径。
  • 父加载器:Bootstrap ClassLoader。

3. 应用程序类加载器(Application ClassLoader / System ClassLoader)

  • sun.misc.Launcher$AppClassLoader 实现。
  • 负责加载我们自己写的类,即 ClassPath 下的类。
  • 父加载器:Extension ClassLoader。
  • 也是我们自定义类加载器默认的“父亲”。

开发者也可以继承 java.lang.ClassLoader 实现自定义类加载器,通常以 Application ClassLoader 为父。


四、双亲委派的工作流程(图解+伪代码)

伪代码简化版:

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
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器找不到,继续往下走
            }
            // 4. 父加载器都失败,自己加载
            if (c == null) {
                c = findClass(name); // 自定义加载逻辑
            }
        }
        if (resolve) {
            resolveClass(c); // 链接类
        }
        return c;
    }
}

流程图解:

用户自定义 ClassLoader
        ↓ 委托
Application ClassLoader
        ↓ 委托
Extension ClassLoader
        ↓ 委托
Bootstrap ClassLoader → 加载?→ 成功 → 返回
                             ↓
                           失败 → 逐级返回 → 子加载器自己加载

五、为什么要有双亲委派?三大核心优势

1. 避免类的重复加载

同一个类被不同 ClassLoader 加载,会被 JVM 视为完全不同的两个类(类的唯一性 = 全限定名 + 加载器)。双亲委派确保一个类只会被“最上层能加载它的加载器”加载一次,避免重复。

2. 保证核心类库的安全性

试想:如果你在项目里写了一个 java.lang.String 类,放在 ClassPath 下,会不会替换掉 JDK 的 String?

不会! 因为双亲委派机制会让 Bootstrap ClassLoader 优先加载官方的 String,你的“山寨版”根本没机会被加载 —— 有效防止核心 API 被污染或劫持。

3. 保证 Java 程序的稳定性和一致性

所有类加载都有统一入口和顺序,避免混乱。比如 Object 类无论在哪里使用,都是由 Bootstrap 加载的同一个类,确保 instanceof、反射、序列化等行为一致。


六、什么时候需要“打破”双亲委派?

虽然双亲委派是默认机制,但在某些高级场景下,必须“打破”它:

1. SPI(Service Provider Interface)机制 —— 最经典的例子:JDBC

  • java.sql.Driver 是 JDK 核心接口,由 Bootstrap 加载。
  • 但 MySQL、PostgreSQL 的驱动实现类在第三方 jar 中,由 AppClassLoader 加载。
  • Bootstrap 无法加载 AppClassLoader 的类 → 矛盾!

解决方案:使用 线程上下文类加载器(Thread.currentThread().getContextClassLoader()),让高层类加载器能“向下”加载低层类。

// 示例:JDBC 加载驱动时
Class.forName("com.mysql.cj.jdbc.Driver", true, 
              Thread.currentThread().getContextClassLoader());

2. OSGi、插件化架构、热部署系统

  • 如 Eclipse 插件、阿里 SOFAArk、Spring Boot DevTools。
  • 需要实现模块隔离、动态加载/卸载,每个模块有独立 ClassLoader。
  • 必须自定义类加载逻辑,不完全遵守双亲委派。

3. 自定义 ClassLoader 重写loadClass()

如果你继承 ClassLoader 并重写了 loadClass() 方法,改变了“先委托父类”的顺序,比如先自己加载,再委托父类 —— 就打破了双亲委派。

注意:除非明确知道自己在做什么,否则不建议随意打破双亲委派,容易引发类冲突或内存泄漏。


七、实战小例子:验证双亲委派

你可以写一个简单的测试类:

public class TestClassLoader {
    public static void main(String[] args) {
        System.out.println("String 类加载器:" + String.class.getClassLoader()); // null → Bootstrap
        System.out.println("自定义类加载器:" + TestClassLoader.class.getClassLoader()); // AppClassLoader
    }
}

输出:

String 类加载器:null
自定义类加载器:sun.misc.Launcher$AppClassLoader@xxxxxx

为什么 String.class.getClassLoader()null?因为 Bootstrap 是 C++ 实现的,Java 里拿不到引用,所以返回 null —— 但它确实是“最高领导”。


八、总结:一张图 + 三句话记住双亲委派

一张图:

[自定义ClassLoader] → 委托 → [AppClassLoader] → 委托 → [ExtClassLoader] → 委托 → [Bootstrap]
                                                              ↓
                                                          加载核心类
                                                              ↓ 失败?
[自定义ClassLoader] ← 返回 ← [AppClassLoader] ← 返回 ← [ExtClassLoader] ← 返回 ← 失败
                                                              ↓
                                                      自己尝试加载

三句话总结:

  1. 向上委托,向下加载 —— 先让“爸爸”试试,不行自己上。
  2. 安全第一,避免重复 —— 核心类不被污染,类不重复加载。
  3. 默认机制,可被打破 —— SPI、插件化、热部署等场景需灵活处理。
控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言