Java类加载机制——双亲委派与自定义类加载器

Java类加载机制

Java 的类加载机制是 JVM 将类的字节码文件(.class)加载到内存,并对其进行验证、准备、解析和初始化,最终形成可以被 JVM 直接使用的 Java 类型的过程。它是 Java 实现跨平台、动态扩展(如 SPI、热部署)的核心基础之一。

类加载的完整生命周期

一个 Java 类从被加载到 JVM 内存中,到最终被卸载,其生命周期包括加载、验证、准备、解析、初始化、使用、卸载 7 个阶段。其中加载、验证、准备、初始化、卸载 这 5 个阶段的顺序是确定的,而解析阶段则可能在初始化阶段之后(为了支持动态绑定,即晚期绑定)。
连接 验证 准备 解析 加载 初始化 使用 卸载

类加载ClassLoader

类加载ClassLoader介绍

类加载器是实现 "加载" 阶段的核心组件,负责获取类的二进制字节流。JVM 规范将类加载器分为启动类加载器(Bootstrap ClassLoader)扩展类加载器(Extension ClassLoader)应用程序类加载器(Application ClassLoader) ,以及用户自定义的自定义类加载器(Custom ClassLoader)。JVM会自顶向下尝试加载类
JVM 启动类加载器 BootstrapClassLoader 扩展类加载器 ExtensionClassLoader 应用程序类加载器 AppClassLoader 自定义类加载器 CustomClassLoader 自定义类加载器 CustomClassLoader 定义类加载器 CustomClassLoader

除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。

每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoadernull的话,那么该类加载器的父类加载器是 BootstrapClassLoader

java 复制代码
public abstract class ClassLoader {
    
    // 父加载器
    private final ClassLoader parent;

    @CallerSensitive
    public final ClassLoader getParent() {
        //...
    }
    ...
}

其中jdk8及之前版本存在ExtensionClassLoader,用于加载 $JAVA_HOME/jre/lib/ext 目录下的扩展 jar 包;Java 9 引入模块化系统(JPMS)后,JDK 架构发生根本性调整,导致 ExtensionClassLoader 被废弃。原 ExtClassLoader 的职责被 jdk.internal.loader.PlatformClassLoader 接管,但 PlatformClassLoader 不再基于 ext 目录,而是加载 Java SE 平台的 "系统模块"(如 java.desktopjava.sql 等非核心模块);

通过如下代码可以看出:

java 复制代码
public class App 
{
    public static void main( String[] args )
    {
        // 输出为null,默认为jdk内置BootstrapClassLoader
        System.out.println(String.class.getClassLoader());
        // 非核心库由PlatformClassLoader/ExtClassLoader加载
        System.out.println(Driver.class.getClassLoader());
        // 用户应用由AppClassLoader加载
        System.out.println(App.class.getClassLoader());
    }
}

双亲委派模型

双亲委派模型介绍

ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。JVM中被称为 "bootstrap class loader"的内置类加载器本身没有父类加载器,但是可以作为 ClassLoader 实例的父类加载器。

从上面的介绍可以看出:

  • ClassLoader 类使用委托模型来搜索类和资源。
  • 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
  • ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
双亲委派模型的作用

双亲委派模型是 Java 类加载机制的核心设计之一,其意义主要体现在安全性、唯一性、规范性等多个维度,是 JVM 实现类加载有序性和可靠性的关键保障,具体可分为以下几个核心方面:

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

    JVM 判定两个类是否为 "同一个类" 的标准是:类的全限定名相同 + 加载该类的类加载器相同。双亲委派模型通过 "先委派父类加载器加载" 的规则,确保同一个类只会被其最顶层的可用类加载器加载一次,而非被多个类加载器重复加载。

    例如,应用程序类加载器收到加载java.lang.String的请求时,会先委派给扩展类加载器,最终由启动类加载器加载该类。后续任何类加载器再请求加载java.lang.String时,都会因父类加载器已加载该类而直接复用,避免了内存中出现多个String类的实例,减少了内存开销,也保证了类的逻辑一致性。

  2. 保护核心类库的安全,防止 "类篡改" 攻击

  3. 双亲委派模型将 JVM 核心类库(如java.langjava.util等包下的类)的加载权完全交给启动类加载器(Bootstrap ClassLoader),这是对 Java 核心 API 的关键安全防护:

    • 若没有双亲委派模型,用户可自定义一个java.lang.String类,并通过应用程序类加载器加载,从而篡改核心类的逻辑(如修改Stringequals方法),引发严重的安全问题。
    • 而双亲委派模型下,用户自定义的java.lang.String类会因 "父类加载器(启动类加载器)已加载核心String类" 而无法被加载,从根本上杜绝了恶意代码替换核心类库的风险,保证了 Java 运行时环境的基础安全。
  4. 规范类加载的层级关系,实现类加载的有序性

    双亲委派模型为类加载器定义了清晰的层级结构(启动类加载器 → 扩展类加载器 → 应用程序类加载器 → 自定义类加载器),并规定了 "自上而下委派、自下而上加载" 的执行顺序,使得类加载过程具有明确的规范性和可预测性:

    • 不同类加载器的职责边界被明确划分:启动类加载器负责核心类库,扩展类加载器负责 JVM 扩展类,应用程序类加载器负责用户业务类,自定义类加载器负责特殊需求(如网络加载、加密类加载)。
    • 这种层级划分避免了类加载器之间的职责混乱,简化了类加载机制的设计与实现,也便于开发者理解和扩展自定义类加载器(只需遵循委派规则,即可与系统类加载器协同工作)。
  5. 为模块化类加载奠定基础

    双亲委派模型的层级委派思想,为 Java 后续的模块化发展(如 JPMS/Jigsaw、OSGI 的模块化类加载)提供了基础设计思路:

    • 尽管 OSGI 等模块化框架突破了双亲委派的严格顺序(采用 "平级委派 + 按需加载"),但依然借鉴了 "先委托高层 / 公共类加载器加载,再自行加载" 的核心思想,以实现模块间的类隔离与共享。
    • JPMS(Java 9 引入的模块系统)的类加载机制也兼容了双亲委派模型的核心逻辑,模块的加载优先级仍遵循 "系统模块优先于用户模块" 的原则,与双亲委派的安全设计一脉相承。
双亲委派模型的实现
java 复制代码
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 检查类是否已经被加载
        Class<?> c = findLoadedClass(name);
        // 如果没有被加载
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 如果有父类加载器,委派父类加载器加载类
                    c = parent.loadClass(name, false);
                } else {
                    // 如果没有父类加载器,使用JVM中BootstrapClassLoader加载类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
				
            }

            if (c == null) {
				// 如果仍未加载到类,使用findClass找到类
                long t1 = System.nanoTime();
                c = findClass(name);
                //...记录状态
            }
        }
        if (resolve) {
            // 对类进行连接操作
            resolveClass(c);
        }
        return c;
    }
}
打破双亲委派模型
为什么要打破双亲委托模型?

双亲委派模型决定了"自下而上加载,自上而下委派"的类加载顺序。
BootstrapClassLoader ExtClassLoader/PlatformClassLoader AppClassLoader java核心库 java扩展库 用户程序

对于Web 容器(Tomcat、Jetty)、模块化容器(OSGi)的核心诉求是类隔离 (不同应用 / 模块使用不同版本的类,互不干扰),而双亲委托的 "父优先" 会导致:父加载器(如AppClassLoader)加载的类会被所有子应用共享,无法隔离(比如两个 Webapp 依赖不同版本的 Spring,父加载器加载高版本后,低版本应用会冲突)。

如何打破双亲委派模型

自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

因为loadClass() 方法已经明确了加载类的流程,先委托给父类加载器加载,最终给BootstrapClassLoader去加载,如果都加载不到,才会调用findClass() 方法。只需要重写loadClass() 方法的加载流程即可

自定义类加载器作用

为什么要自定义类加载器

JVM 提供Thread.setContextClassLoader(),允许用户自定义当前线程的类加载器从而实现打破双亲委派机制,优先由用户设定的类加载器进行加载,当加载失败时才尝试使用父加载器加载,从而达到类隔离的场景。

如何自定义类加载器

URLClassLoader 是 Java 提供的标准自定义类加载器实现,核心作用是通过 URL 路径 (文件、JAR、网络地址等)加载类,突破默认 classpath 限制;默认遵循双亲委托模型,但可通过重写方法打破。基本上自定义的类加载器都继承自URLClassLoader

有如下诉求,App分别调用UserServiceAdminService,他们都依赖Calculator,但是版本不同。
App UserService Calculator:1.0 AdminService Calculator:1.0

Calculator 1.0提供了两个整数加法,Calculator 2.0提供了三个整数加法

java 复制代码
// 1.0服务两个整数相加
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}
// 2.0服务三个整数相加
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}
// admin服务调用Calculator 1.0服务
public class AdminService {
    private final Calculator calculator = new Calculator();

    public AdminService(Calculator calculator) {
    }

    public void doTask() {
        System.out.println("AdminService doTask" + this.calculator.add(1, 2));
    }
}
// user服务调用Calculator 2.0服务
public class UserService {
    private final Calculator calculator = new Calculator();

    public UserService(Calculator calculator) {
    }

    public void doTask() {
        System.out.println("UserService doTask" + this.calculator.add(1, 2, 3));
    }
}

可以使用IDEA将上述文件编译为class后,分别按package的目录层级打成4个jar包,放在src/main/resources目录,打包命令如下:

shell 复制代码
jar cfv xxx.jar org/

最后目录层级如下:

shell 复制代码
|---src
	|---main
		|---java
			|---xxx.xxx.xx
				|---App.java
    	|---resources
    		|---admin
    			|---1.0
    				|---admin.jar
            |---user
            	|---1.0
            		|---user.jar
            |---calculator
            	|---1.0
            		|---calculator.jar
               	|---2.0
            		|---calculator.jar

App.java中模拟扫描jar包并加载的过程,其中calculator1.0admin1.0使用classLoader1calculator2.0user1.0使用classLoader2,实现类加载隔离

shell 复制代码
|---BootstrapClassLoader
	|---PlatformClassLoader
		|---AppClassLoader
			|---App.class
    |---URLClassLoader1
    	|---calculator-1.0
    	|---admin-1.0
    |---URLClassLoader2
    	|---calculator-2.0
    	|---user-1.0

样例如下:

java 复制代码
public class App {
    public static void main(String[] args) throws Throwable {
        URL calculatorV1Url = App.class.getClassLoader().getResource("calculator/1.0/calculator.jar");
        URL adminServiceUrl = App.class.getClassLoader().getResource("admin/1.0/admin.jar");

        URL calculatorV2Url = App.class.getClassLoader().getResource("calculator/2.0/calculator.jar");
        URL userServiceUrl = App.class.getClassLoader().getResource("user/1.0/user.jar");
        try (URLClassLoader classLoader1 = new URLClassLoader(new URL[]{calculatorV1Url, adminServiceUrl})) {
            Object calculator1 = loadClass(classLoader1, "org.numb.java.base.classloader.Calculator");
            Object adminService = loadClass(classLoader1, "org.numb.java.base.classloader.AdminService", calculator1);
            adminService.getClass().getMethod("doTask").invoke(adminService);
        }
        try (URLClassLoader classLoader2 = new URLClassLoader(new URL[]{calculatorV2Url, userServiceUrl})) {
            Object calculator2 = loadClass(classLoader2, "org.numb.java.base.classloader.Calculator");
            Object userService = loadClass(classLoader2, "org.numb.java.base.classloader.UserService", calculator2);
            userService.getClass().getMethod("doTask").invoke(userService);
        }
    }

    private static Object loadClass(URLClassLoader urlClassLoader, String className, Object... args) throws Throwable {
        Class<?> aClass = urlClassLoader.loadClass(className);
        Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            if (args == null || args.length == 0) {
                if (declaredConstructor.getParameterCount() == 0) {
                    return declaredConstructor.newInstance();
                }
            }
            if (args != null && args.length == declaredConstructor.getParameterCount()) {
                return declaredConstructor.newInstance(args);
            }
        }
        return null;
    }


}

最后输出

shell 复制代码
AdminService doTask3
UserService doTask6
相关推荐
weibkreuz2 小时前
模块与组件、模块化与组件化的理解@3
开发语言·前端·javascript
SadSunset2 小时前
(29)阶段性个人总结
java
拾忆,想起2 小时前
单例模式深度解析:如何确保一个类只有一个实例
前端·javascript·python·微服务·单例模式·性能优化·dubbo
癫狂的兔子2 小时前
【Python】【NumPy】学习笔记
python·学习·numpy
Kurbaneli2 小时前
Python的起源与发展
python
540_5402 小时前
ADVANCE Day26
人工智能·python·机器学习
dazzle2 小时前
Python高级技巧:装饰器全面指南,从基础到高级应用
python
南_山无梅落2 小时前
11.Python 常用数据类型「增删改查」操作总结表格
python
乾元2 小时前
用 AI 做联动:当应用层出现问题,网络如何被“自动拉入决策回路”
运维·开发语言·网络·人工智能·ci/cd·自动化