github star 较多的Java双亲委派机制【类加载的核心内容加星】

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

双亲委派机制是 JVM 类加载器的核心加载规则,其核心思想是:类加载器在加载类时,先将加载请求委派给父类加载器,只有当父类加载器无法加载该类时,子类加载器才会尝试自己加载

Taimili 艾米莉 ( 一款专业的 GitHub star 管理和github 加星涨星工具taimili.com )

艾米莉 是一款优雅便捷的 GitHub star 管理和github 加星涨星工具,基于 PHP & javascript 构建, 能对github 得 star fork follow watch 管理和提升,最适合github 的深度用户

一、前置基础:类加载器的层次结构

要理解双亲委派,首先要明确 JVM 的类加载器体系(逻辑上的父子关系,非继承):

类加载器类型 实现语言 加载路径 父加载器(逻辑) 核心说明
启动类加载器(Bootstrap) C++ JRE/lib/rt.jar(核心类库,如 java.lang.*) 最顶层,不属于 Java 类体系
扩展类加载器(Extension) Java JRE/lib/ext/*.jar 或 java.ext.dirs 启动类加载器 sun.misc.Launcher$ExtClassLoader
应用程序类加载器(Application) Java ClassPath(项目代码、第三方 jar) 扩展类加载器 sun.misc.Launcher$AppClassLoader(默认系统类加载器)
自定义类加载器 Java 自定义路径 应用程序类加载器 继承 ClassLoader 重写 findClass

注意:"双亲" 并非指 "父类 + 母类",而是 "父加载器" 的形象化说法;启动类加载器是 C++ 实现,因此在 Java 代码中获取其 getParent 会返回 null(逻辑父为无)。

欢迎访问我的个人github 项目个人主页 github.com/AndyBulushe... , 里面AI 工具全免费。程序员的绝佳好帮手

二、双亲委派的核心加载流程

当任意类加载器收到类加载请求(如加载com.example.Demo),执行以下步骤:

  1. 委派请求:当前类加载器不直接加载,而是将请求向上委派给父类加载器;
  2. 递归委派:父类加载器重复第一步,直到请求到达启动类加载器;
  3. 父类尝试加载:启动类加载器检查自己的加载路径,若能找到并加载该类,则返回 Class 对象;若不能,向下传递 "加载失败";
  4. 子类兜底加载:从扩展类加载器开始,依次检查自身路径,若能加载则返回,否则继续向下;
  5. 最终兜底 :若所有父加载器都无法加载,最终由最初的子类加载器尝试加载,若仍失败则抛出ClassNotFoundException

流程示意图

plaintext

markdown 复制代码
自定义类加载器 → 应用程序类加载器 → 扩展类加载器 → 启动类加载器
                    ↑(委派)          ↑(委派)        ↑(委派)
                    ↓(加载失败)      ↓(加载失败)    ↓(加载失败)
自定义类加载器 ← 应用程序类加载器 ← 扩展类加载器 ← 启动类加载器

三、双亲委派的核心实现(源码层面)

双亲委派的逻辑核心在java.lang.ClassLoaderloadClass方法中(JDK8 为例):

java

运行

scss 复制代码
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 {
                // 2. 有父加载器则委派给父加载器加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 无父加载器(如应用类加载器的父是扩展类,最终到启动类),调用启动类加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器加载失败(抛出异常),继续向下
            }

            // 4. 父加载器未加载到,自己尝试加载
            if (c == null) {
                long t1 = System.nanoTime();
                // findClass是自定义加载器需要重写的方法(默认抛出异常)
                c = findClass(name);
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        // 5. 解析类(可选)
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

关键逻辑:

  • 先检查缓存(避免重复加载);
  • 优先委派父加载器;
  • 父加载失败后,调用findClass(子类重写此方法实现自定义加载逻辑)。

四、双亲委派机制的核心优势

1. 避免类重复加载

同一个类(全限定名)不会被不同类加载器加载多次,保证 JVM 中类的唯一性(类的唯一性由 "类加载器 + 全限定名" 共同决定)。

2. 保护核心类库安全(沙箱机制)

防止核心 API 被篡改,例如:若自定义java.lang.String类,由于双亲委派,启动类加载器会优先加载 JDK 自带的String,自定义的String永远不会被加载,避免恶意代码替换核心类。

3. 保证类加载的优先级

核心类(如java.lang.Object)由最顶层的启动类加载器加载,保证基础类的优先加载和全局可用。

五、打破双亲委派的场景(核心例外)

双亲委派并非绝对规则,某些场景下需要主动打破,核心场景如下:

1. SPI 机制(Service Provider Interface)

典型例子:JDBC、JNDI。

  • 问题:JDBC 的DriverManager由启动类加载器加载(位于 rt.jar),但 JDBC 驱动(如 MySQL 驱动)是第三方类,位于 ClassPath,启动类加载器无法加载;
  • 解决方案:使用线程上下文类加载器(Thread Context ClassLoader) ,允许启动类加载器委托应用程序类加载器加载第三方类(反向委派,打破双亲委派)。

2. Tomcat 等 Web 容器的类加载

Tomcat 为了隔离不同 Web 应用(避免类冲突),自定义了类加载器(WebAppClassLoader),核心规则:

  • 先加载 Web 应用自身的类(/WEB-INF/classes、/WEB-INF/lib);
  • 加载失败后,再委派给父类加载器;
  • 打破了 "先委派父加载器" 的规则,保证每个 Web 应用的类独立。

3. OSGi / 模块化框架

OSGi 框架支持模块化热部署,需要灵活的类加载策略,允许子类加载器优先加载,甚至同一类被不同加载器加载(实现模块化隔离)。

4. 自定义类加载器的特殊需求

例如:热部署(重新加载类)、加密类加载(需自定义加载逻辑),需重写loadClass方法,跳过双亲委派。

六、实战验证:双亲委派的效果

示例 1:验证核心类无法被自定义加载

尝试自定义java.lang.String类:

java

运行

kotlin 复制代码
package java.lang;

public class String {
    public String toString() {
        return "自定义String";
    }
}

运行结果:抛出java.lang.SecurityException: Prohibited package name: java.lang,因为 JVM 对核心包名(java.lang)做了保护,且双亲委派优先加载 JDK 自带的 String。

示例 2:自定义类加载器(默认遵循双亲委派)

java

运行

scala 复制代码
// 自定义类加载器
class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 模拟加载自定义路径的类(简化版)
        byte[] classBytes = loadClassBytes(name);
        return defineClass(name, classBytes, 0, classBytes.length);
    }

    private byte[] loadClassBytes(String name) {
        // 实际场景:从文件/网络读取类字节码,此处省略实现
        return new byte[0];
    }
}

// 测试
public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        MyClassLoader classLoader = new MyClassLoader();
        // 加载com.example.Demo,会先委派给应用程序类加载器
        Class<?> clazz = classLoader.loadClass("com.example.Demo");
        System.out.println(clazz.getClassLoader()); // 输出应用程序类加载器(父加载器加载成功)
    }
}

七、总结

  • 双亲委派是类加载的默认规则,核心是 "父优先";
  • 本质是为了类的唯一性和核心 API 的安全;
  • 打破双亲委派的核心原因是 "按需加载"(如 SPI、容器隔离);
  • 自定义类加载器时,若需打破双亲委派,需重写loadClass方法(而非仅重写findClass)。
相关推荐
编程火箭车36 分钟前
【Java SE 基础学习打卡】19 运算符(中)
java·java入门·运算符·编程基础·赋值运算符·复合赋值·自增自减
是一个Bug36 分钟前
Spring事件监听器源码深度解析
java·数据库·spring
蜂蜜黄油呀土豆41 分钟前
ThreadLocal 深度解析:它解决了什么、原理是什么、如何正确使用(含代码与实战建议)
java·并发编程·内存泄漏·threadlocal
毕设源码-郭学长1 小时前
【开题答辩全过程】以 高校教室管理系统为例,包含答辩的问题和答案
java·spring boot
罗不丢1 小时前
UTC,Date,LocalDate转换问题解决方法
java
Klong.k1 小时前
谈谈session、application存储对象
java·tomcat
Moe4881 小时前
Spring Boot启动魔法:SpringApplication.run()源码全流程拆解
java·后端·面试
阿杰AJie1 小时前
Java 常见场景中需要使用 try 的示例集
java·后端
0***v7771 小时前
JavaWeb项目打包、部署至Tomcat并启动的全程指南(图文详解)
java·tomcat