Java双亲委派机制【类加载的核心内容】

双亲委派机制(Java 类加载核心机制)

双亲委派机制是 Java 类加载器(ClassLoader) 加载类时遵循的核心规则,核心思想是:一个类加载器要加载某个类时,不会先自己尝试加载,而是先委派给它的 "父类加载器" 去加载;只有当父类加载器无法加载(找不到该类)时,才会由当前类加载器自己尝试加载

这里的 "双亲" 并非指 Java 中的继承关系(不是 extends),而是一种「委派层级关系」,目的是保证类加载的 唯一性安全性

一、先搞懂:Java 的类加载器层级(委派的 "父子" 关系)

Java 中默认有 3 层核心类加载器(自上而下,父→子),还有自定义类加载器(最底层),层级如下:

类加载器 作用(加载范围) 特点
启动类加载器(Bootstrap ClassLoader) 加载 JDK 核心类库(如 rt.jar 中的 java.lang.Stringjava.util.ArrayList 等) 由 C/C++ 实现,无 Java 对象(getClassLoader() 返回 null
扩展类加载器(Extension ClassLoader) 加载 JDK 扩展类库(如 jre/lib/ext 目录下的类) Java 实现,父类是启动类加载器(逻辑上)
应用程序类加载器(Application ClassLoader) 加载用户编写的类(classpath 下的类,比如项目 src 目录编译后的类) Java 实现,默认的系统类加载器,父类是扩展类加载器
自定义类加载器(Custom ClassLoader) 开发者自定义加载逻辑(如加载加密类、网络上的类) 继承 ClassLoader,父类是应用程序类加载器

注意:层级是 "逻辑委派关系",不是 Java 中的继承关系(比如应用程序类加载器的父类是扩展类加载器,通过 getParent() 方法获取,而非 extends)。

二、双亲委派的核心流程("先找爹,爹不行自己上")

当某个类加载器(比如应用程序类加载器)要加载一个类(如 com.example.User)时,流程如下:

  1. 委派父类加载:当前类加载器不直接加载,先把 "加载请求" 委派给它的父类加载器(应用程序类加载器 → 扩展类加载器);
  2. 父类继续委派 :父类加载器也不直接加载,继续向上委派,直到最顶层的 启动类加载器
  3. 顶层尝试加载 :启动类加载器检查自己的加载范围(JDK 核心类库),如果能找到这个类,就直接加载并返回该类的 Class 对象;如果找不到,就 "向下回退" 加载请求;
  4. 逐层回退尝试:扩展类加载器接收回退的请求,检查自己的加载范围(扩展类库),能加载就返回,不能就继续回退;
  5. 自身最终加载 :如果所有父类加载器都无法加载,最后由最初发起请求的类加载器(应用程序类加载器)自己尝试加载;如果还是找不到,就抛出 ClassNotFoundException 异常。

用通俗的话讲:"儿子要找东西,先让爸爸找,爸爸让爷爷找,爷爷找不到再让爸爸找,爸爸还找不到,儿子自己找,再找不到就说没这东西"

三、为什么需要双亲委派机制?(核心作用)

1. 保证类的唯一性(避免重复加载)

如果没有双亲委派,多个类加载器可能会加载同一个类(比如用户自己写了一个 java.lang.String),导致 JVM 中出现多个相同全限定名的 Class 对象,破坏类的唯一性(JVM 判断两个类是否相同,需要 "类全限定名 + 加载它的类加载器" 都相同)。

比如:启动类加载器已经加载了 JDK 核心的 java.lang.String,如果应用程序类加载器再加载用户自定义的 java.lang.String,就会出现两个 String 类,导致程序逻辑混乱。而双亲委派机制会让 "先找父类加载",启动类加载器已经加载过,就不会重复加载。

2. 保证 Java 核心类的安全性(防止恶意篡改)

双亲委派机制能保护 JDK 核心类库不被恶意类冒充。比如:

  • 开发者无法自定义一个 java.lang.String 来替换 JDK 原生的 String:因为当加载这个自定义 String 时,会先委派给启动类加载器,启动类加载器已经加载了 JDK 的 String,就不会再加载自定义的 String
  • 更极端的,开发者无法自定义 java.lang.System 等核心类,避免恶意代码篡改核心类的逻辑(比如修改 System.exit() 的行为)。

四、例外情况:打破双亲委派机制

双亲委派是默认规则,但不是强制的,开发者可以通过自定义类加载器打破它(重写 ClassLoaderloadClass() 方法,不遵循委派逻辑)。常见场景:

  1. Tomcat 等 Web 服务器 :Tomcat 需要为每个 Web 应用隔离类加载(比如不同应用的 com.example.User 要独立加载),所以 Tomcat 的类加载器会先自己尝试加载 Web 应用的类,再委派给父类加载器(和双亲委派顺序相反,称为 "反向委派");
  2. OSGi 框架:OSGi 支持模块化热部署,每个模块有自己的类加载器,加载逻辑灵活,不严格遵循双亲委派;
  3. JDK 9 之后的模块系统(JPMS):模块系统对类加载有新的规则,部分场景会打破传统的双亲委派。

要彻底搞懂双亲委派机制,我们必须把「理论原理」「JDK 源码实现」「自定义代码验证」三者结合起来 ------ 从源码看规则本质,从自定义代码验证规则作用,再从打破规则的场景理解其灵活性。

Java 中所有类加载器都间接继承 java.lang.ClassLoader,这个抽象类定义了类加载的核心流程,关键是 3 个方法,分工明确:

方法名 作用 是否需要重写
loadClass(String name) 类加载的「入口方法」,实现双亲委派的核心逻辑(先委派父类,再自己加载) 不建议重写(打破双亲委派时才重写)
findClass(String name) 「查找类文件」的逻辑(比如从文件、网络、加密文件中读取字节码) 自定义类加载器必须重写
defineClass(String name, byte[] b, int off, int len) 把字节码数组转换成 JVM 能识别的 Class 对象(禁止重写,JVM 底层实现) 绝对不能重写

核心流程关系loadClass(委派逻辑)→ 父类加载失败 → 调用 findClass(找字节码)→ 调用 defineClass(转成 Class 对象)

这三个方法的分工,是理解双亲委派和自定义类加载器的关键!

五、源码解析:双亲委派的核心实现(JDK 8 为例)

双亲委派不是 "约定俗成",而是 ClassLoader 类的 loadClass 方法硬编码的逻辑。我们直接看 JDK 源码(关键部分加注释):

java

运行

java 复制代码
public abstract class ClassLoader {
    // 父类加载器(逻辑委派关系,不是继承关系)
    private final ClassLoader parent;

    // 类加载的入口方法:加载指定全限定名的类
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false); // 第二个参数 resolve:是否解析类(默认不解析)
    }

    // 核心实现方法
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) { // 加锁:保证类加载的线程安全(避免并发重复加载)
            // 步骤 1:检查该类是否已经被当前类加载器加载过(缓存)
            Class<?> c = findLoadedClass(name);
            if (c == null) { // 没加载过
                long t0 = System.nanoTime();
                try {
                    if (parent != null) { // 有父类加载器:委派给父类加载
                        c = parent.loadClass(name, false);
                    } else { // 没有父类加载器(启动类加载器):调用底层 native 方法查找
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 父类加载器抛出异常:说明父类找不到该类
                }

                // 步骤 2:父类加载器加载失败(c 还是 null),当前类加载器自己找
                if (c == null) {
                    long t1 = System.nanoTime();
                    // 调用 findClass:自定义类加载器必须重写这个方法(默认抛出异常)
                    c = findClass(name);

                    // 记录统计信息(忽略)
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                }
            }

            // 步骤 3:如果需要解析类(resolve=true),则解析
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    // 查找类文件:默认实现直接抛出异常,需要自定义类加载器重写
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

    // 把字节码转成 Class 对象:native 方法,禁止重写
    protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError {
        return defineClass(name, b, off, len, null);
    }
}

源码核心逻辑提炼(对应双亲委派理论):

  1. 缓存优先:先检查当前类加载器是否已经加载过该类(避免重复加载);
  2. 向上委派 :没加载过则委派给父类加载器(parent.loadClass),直到启动类加载器;
  3. 向下回退 :父类加载器找不到(抛 ClassNotFoundException),当前类加载器调用 findClass 自己找;
  4. 线程安全 :用 synchronized 锁保证同一类不会被并发加载。

关键细节

  • 启动类加载器(Bootstrap)是 C/C++ 实现的,没有对应的 Java 对象,所以 parentnull 时,会调用 findBootstrapClassOrNull(native 方法)让启动类加载器尝试加载;
  • 自定义类加载器的核心是重写 findClass(实现自己的查找逻辑),而不是 loadClass(否则会打破双亲委派)。

六、代码实现 1:验证默认双亲委派机制(看现象)

我们通过两个小实验,验证双亲委派的「唯一性」和「安全性」。

实验 1:查看类的加载器层级

写一个普通类,打印加载它的类加载器及其父类加载器:

java

运行

java 复制代码
// 普通类:com.example.Demo
package com.example;

public class Demo {
    public static void main(String[] args) {
        // 1. 获取加载 Demo 类的类加载器
        ClassLoader classLoader = Demo.class.getClassLoader();
        System.out.println("Demo 的类加载器:" + classLoader); // 应用程序类加载器

        // 2. 查看父类加载器(扩展类加载器)
        ClassLoader parentLoader = classLoader.getParent();
        System.out.println("父类加载器:" + parentLoader);

        // 3. 查看扩展类加载器的父类(启动类加载器,返回 null)
        ClassLoader grandParentLoader = parentLoader.getParent();
        System.out.println("祖父类加载器:" + grandParentLoader);

        // 4. 查看 JDK 核心类的加载器(启动类加载器)
        ClassLoader stringLoader = String.class.getClassLoader();
        System.out.println("String 的类加载器:" + stringLoader);
    }
}

运行结果:

plaintext

复制代码
Demo 的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2(应用程序类加载器)
父类加载器:sun.misc.Launcher$ExtClassLoader@6d03e736(扩展类加载器)
祖父类加载器:null(启动类加载器,无 Java 对象)
String 的类加载器:null(启动类加载器加载)

结果分析:

  • 符合我们之前讲的层级关系:应用程序 → 扩展 → 启动(父类委派);
  • 核心类(String)由启动类加载器加载,验证了「核心类优先加载」的规则。

实验 2:验证「核心类不能被篡改」(双亲委派的安全性)

尝试自定义一个 java.lang.String 类,看看能否替换 JDK 原生的 String

java

运行

java 复制代码
// 自定义类:java.lang.String(故意和核心类全限定名相同)
package java.lang;

public class String {
    public String() {
        System.out.println("恶意 String 类被加载!");
    }

    public static void main(String[] args) {
        new String();
    }
}

运行结果:

plaintext

复制代码
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
	at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
	...

结果分析:

  • 无法运行!因为 java.lang 是 JDK 核心包,双亲委派机制会让启动类加载器先尝试加载,但 JVM 禁止自定义核心包下的类(preDefineClass 方法校验);
  • 即使去掉包名限制(假设能编译),启动类加载器已经加载了原生 String,也不会再加载自定义的 String,保证了核心类的安全性。

七、代码实现 2:自定义类加载器(遵循双亲委派)

自定义类加载器的核心是「重写 findClass 方法」(实现自己的类查找逻辑),而不重写 loadClass(保留双亲委派逻辑)。

场景:加载指定路径(比如 D:/customClass/)下的 class 文件(不是 classpath 下的类)。

步骤 1:编写一个待加载的普通类

java

运行

复制代码
// 类:com.example.User(编译后放到 D:/customClass/ 目录下)
package com.example;

public class User {
    public void sayHello() {
        System.out.println("User: Hello, 双亲委派!");
    }
}

步骤 2:编译 User 类

javac 编译 User.java,得到 User.class,并按包结构放到指定路径:

plaintext

复制代码
D:/customClass/com/example/User.class

步骤 3:自定义类加载器(遵循双亲委派)

java

运行

java 复制代码
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

// 自定义类加载器:加载 D:/customClass/ 下的类
public class CustomClassLoader extends ClassLoader {
    // 自定义类的根路径
    private static final String CLASS_PATH = "D:/customClass/";

    @Override
    protected Class<?> findClass(String fullyQualifiedName) throws ClassNotFoundException {
        try {
            // 1. 把全限定名转成文件路径(com.example.User → com/example/User.class)
            String filePath = CLASS_PATH + fullyQualifiedName.replace(".", "/") + ".class";
            
            // 2. 读取 class 文件的字节码
            byte[] classBytes = loadClassBytes(filePath);
            
            // 3. 调用父类的 defineClass 方法,把字节码转成 Class 对象(禁止自己重写)
            return defineClass(fullyQualifiedName, classBytes, 0, classBytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException("类找不到:" + fullyQualifiedName, e);
        }
    }

    // 读取 class 文件的字节码
    private byte[] loadClassBytes(String filePath) throws IOException {
        FileInputStream fis = new FileInputStream(filePath);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = fis.read(buffer)) != -1) {
            bos.write(buffer, 0, len);
        }
        fis.close();
        return bos.toByteArray();
    }
}

步骤 4:测试自定义类加载器

java

运行

java 复制代码
public class TestCustomClassLoader {
    public static void main(String[] args) throws Exception {
        // 1. 创建自定义类加载器实例
        CustomClassLoader customLoader = new CustomClassLoader();
        
        // 2. 加载 D:/customClass/ 下的 com.example.User 类
        Class<?> userClass = customLoader.loadClass("com.example.User");
        
        // 3. 验证类加载器(应该是我们的 CustomClassLoader)
        System.out.println("User 类的加载器:" + userClass.getClassLoader());
        
        // 4. 反射调用 sayHello 方法
        Object user = userClass.newInstance();
        userClass.getMethod("sayHello").invoke(user);

        // 5. 验证双亲委派:如果 classpath 下也有 com.example.User,会优先加载哪个?
        Class<?> systemUserClass = Class.forName("com.example.User");
        System.out.println("系统类加载器加载的 User:" + systemUserClass.getClassLoader());
    }
}

运行结果:

plaintext

复制代码
User 类的加载器:CustomClassLoader@6d03e736
User: Hello, 双亲委派!
系统类加载器加载的 User:sun.misc.Launcher$AppClassLoader@18b4aac2

关键分析:

  1. 遵循双亲委派customLoader.loadClass 会先委派给父类(应用程序类加载器),父类在 classpath 下找不到 com.example.User(因为我们把它放到了 D:/customClass/),才会调用 CustomClassLoaderfindClass 方法自己加载;
  2. 类的唯一性 :如果 classpath 下也有 com.example.User,应用程序类加载器会先加载,自定义类加载器不会再加载(双亲委派的作用);
  3. 自定义逻辑findClass 方法实现了 "从指定路径读取 class 文件",这是自定义类加载器的核心价值(比如加载加密的 class 文件、网络上的 class 文件)。

八、代码实现 3:打破双亲委派机制

双亲委派是默认规则,但可以通过「重写 loadClass 方法」打破它。核心思路:修改 loadClass 的委派顺序,让当前类加载器先自己尝试加载,再委派给父类。

场景:模拟 Tomcat 的类加载逻辑(先加载 Web 应用内的类,再委派父类)

步骤 1:修改自定义类加载器(重写 loadClass

java

运行

java 复制代码
public class BreakDelegateClassLoader extends ClassLoader {
    private static final String CLASS_PATH = "D:/customClass/";
    // 只对 com.example 包下的类打破委派,其他类仍遵循(避免核心类加载异常)
    private static final String PACKAGE_PREFIX = "com.example.";

    @Override
    protected Class<?> loadClass(String fullyQualifiedName, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(fullyQualifiedName)) {
            // 1. 检查是否已加载
            Class<?> c = findLoadedClass(fullyQualifiedName);
            if (c == null) {
                try {
                    // 2. 打破委派:对 com.example 包下的类,先自己加载
                    if (fullyQualifiedName.startsWith(PACKAGE_PREFIX)) {
                        c = findClass(fullyQualifiedName); // 自己找
                    } else {
                        // 3. 其他类(如 java.lang.String)仍遵循双亲委派,委派给父类
                        c = super.loadClass(fullyQualifiedName, false);
                    }
                } catch (ClassNotFoundException e) {
                    // 4. 自己加载失败,再委派给父类(兜底)
                    c = super.loadClass(fullyQualifiedName, false);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    // 复用之前的 findClass 和 loadClassBytes 方法
    @Override
    protected Class<?> findClass(String fullyQualifiedName) throws ClassNotFoundException {
        try {
            String filePath = CLASS_PATH + fullyQualifiedName.replace(".", "/") + ".class";
            byte[] classBytes = loadClassBytes(filePath);
            return defineClass(fullyQualifiedName, classBytes, 0, classBytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(fullyQualifiedName, e);
        }
    }

    private byte[] loadClassBytes(String filePath) throws IOException {
        // 同之前的实现
        FileInputStream fis = new FileInputStream(filePath);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = fis.read(buffer)) != -1) {
            bos.write(buffer, 0, len);
        }
        fis.close();
        return bos.toByteArray();
    }
}

步骤 2:测试打破双亲委派的效果

java

运行

java 复制代码
public class TestBreakDelegate {
    public static void main(String[] args) throws Exception {
        // 1. 创建打破委派的类加载器
        BreakDelegateClassLoader breakLoader = new BreakDelegateClassLoader();
        
        // 2. 加载 com.example.User(自己先加载)
        Class<?> userClass = breakLoader.loadClass("com.example.User");
        System.out.println("打破委派后,User 的加载器:" + userClass.getClassLoader());
        
        // 3. 加载核心类 java.lang.String(仍遵循委派,启动类加载器加载)
        Class<?> stringClass = breakLoader.loadClass("java.lang.String");
        System.out.println("String 的加载器:" + stringClass.getClassLoader());
    }
}

运行结果:

plaintext

复制代码
打破委派后,User 的加载器:BreakDelegateClassLoader@6d03e736
String 的加载器:null

关键分析:

  1. 打破逻辑 :对 com.example 包下的类,先调用 findClass 自己加载,父类加载器不再优先;
  2. 核心类保护 :对非自定义包的类(如 java.lang.String),仍遵循双亲委派,避免破坏 JVM 核心逻辑;
  3. 实际意义 :Tomcat 就是这样实现的 ------ 每个 Web 应用有自己的类加载器,先加载应用内的类(WEB-INF/classes),再委派给父类加载器,实现不同应用的类隔离(比如两个应用的 com.example.User 互不干扰)。

九、关键问题答疑(彻底扫清盲区)

1. 为什么 JVM 判断两个类是否相同,需要 "类全限定名 + 类加载器"?

比如:用 CustomClassLoader 和系统类加载器分别加载 com.example.User,得到的两个 Class 对象是不同的:

java

运行

复制代码
CustomClassLoader customLoader = new CustomClassLoader();
Class<?> user1 = customLoader.loadClass("com.example.User");
Class<?> user2 = Class.forName("com.example.User"); // 系统类加载器加载
System.out.println(user1 == user2); // false
  • 原因:双亲委派机制保证了 "同一个类只被一个类加载器加载",但如果打破委派,多个类加载器可能加载同一个类;
  • 影响:如果两个 User 类的实例互相赋值,会抛出 ClassCastException(类型转换异常)。

2. 启动类加载器为什么 getParent() 返回 null

  • 启动类加载器(Bootstrap)是 JVM 的一部分,由 C/C++ 实现,没有对应的 Java 对象实例;
  • ClassLoader 类中,parentnull 时,会调用 findBootstrapClassOrNull 这个 native 方法,让 JVM 底层的启动类加载器尝试加载。

3. JDK 9 之后的模块系统(JPMS)对双亲委派有影响吗?

  • 有!JDK 9 引入模块系统后,类加载逻辑更复杂:模块会明确声明依赖,类加载器会先从模块路径(module path)加载,再从类路径(classpath)加载;
  • 但核心思想不变:仍优先加载核心模块的类,避免重复加载和恶意篡改,只是层级和查找顺序有调整。

4. 自定义类加载器必须重写 findClass 吗?

  • 是的!因为 ClassLoader 的默认 findClass 方法直接抛出 ClassNotFoundException
  • 重写 findClass 是 "遵循双亲委派" 的正确方式,重写 loadClass 是 "打破委派" 的方式,不要混淆。

总结:双亲委派的核心逻辑闭环

  1. 理论本质:向上委派父类加载,向下回退自身加载,保证类的唯一性和核心类安全;
  2. 源码实现ClassLoader.loadClass 方法硬编码了委派逻辑,findClass 负责实际查找,defineClass 负责字节码转 Class;
  3. 代码验证
    • 遵循委派:重写 findClass,实现自定义查找逻辑;
    • 打破委派:重写 loadClass,修改委派顺序;
  4. 实际价值:默认规则保护 JVM 核心,打破规则支持灵活场景(Tomcat 隔离、OSGi 热部署)。

理解双亲委派的关键,不是死记流程,而是能通过源码看懂 "为什么这么设计",通过自定义代码验证 "设计的作用",最终能解释 "实际开发中的相关问题"(比如类冲突、类转换异常)。

相关推荐
T***74251 小时前
【Spring Boot】 SpringBoot自动装配-Condition
java·spring boot·后端
FAREWELL000751 小时前
Lua学习记录(5) --- Lua中的协同程序 也称线程Coroutine的介绍
开发语言·学习·lua
Seven972 小时前
剑指offer-44、翻转单词序列
java
学不完了是吧2 小时前
“小白专属”python字符串处理文档
开发语言·python
醉风塘2 小时前
如何将class文件替换到Jar包中:完整指南
java·jar
Maya动画技术2 小时前
python的py转pyd方法(cython)
开发语言·python·spring
27669582922 小时前
雷池waf 逆向
java·开发语言·前端·python·wasm·waf·雷池waf
逸Y 仙X2 小时前
Java时间类型入门到实战
java·ide·spring·tomcat
Want5952 小时前
C/C++跳动的爱心③
java·c语言·c++
h***93662 小时前
记录 idea 启动 tomcat 控制台输出乱码问题解决
java·tomcat·intellij-idea