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

简介: Java类加载机制是JVM加载.class文件并完成验证、准备、解析、初始化的过程,支持跨平台与动态扩展。通过类加载器实现双亲委派模型,确保类的唯一性与安全性,同时可通过自定义ClassLoader打破委派链,实现类隔离,广泛应用于Web容器与模块化系统。

Java类加载机制

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

类加载的完整生命周期

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

graph LR
A[加载]
subgraph 连接
B[验证]
C[准备]
D[解析]
end
E[初始化]
F[使用]
G[卸载]
A-->B-->C-.->D-.->E-->F-->G

类加载ClassLoader

类加载ClassLoader介绍

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

graph BT
subgraph JVM
A[启动类加载器 BootstrapClassLoader]
end
B[扩展类加载器 ExtensionClassLoader]
C[应用程序类加载器 AppClassLoader]
D[自定义类加载器 CustomClassLoader]
E[自定义类加载器 CustomClassLoader]
F[定义类加载器 CustomClassLoader]
B-->A
C-->B
D-->C
E-->C
F-->D

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

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

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 等非核心模块);

通过如下代码可以看出:

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 引入的模块系统)的类加载机制也兼容了双亲委派模型的核心逻辑,模块的加载优先级仍遵循 “系统模块优先于用户模块” 的原则,与双亲委派的安全设计一脉相承。

双亲委派模型的实现

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;
    }
}

打破双亲委派模型

为什么要打破双亲委托模型?

双亲委派模型决定了"自下而上加载,自上而下委派"的类加载顺序。

graph BT
A[BootstrapClassLoader]
B[ExtClassLoader/PlatformClassLoader]
C[AppClassLoader]

X[java核心库]
Y[java扩展库]
Z[用户程序]
C-->B-->A
Z-->Y-->X

对于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,但是版本不同。

graph BT
A[App]
B[UserService]
C[Calculator:1.0]
D[AdminService]
E[Calculator:1.0]
E-->D-->A
C-->B-->A

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

// 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目录,打包命令如下:

jar cfv xxx.jar org/

最后目录层级如下:

|---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,实现类加载隔离

|---BootstrapClassLoader
    |---PlatformClassLoader
        |---AppClassLoader
            |---App.class
    |---URLClassLoader1
        |---calculator-1.0
        |---admin-1.0
    |---URLClassLoader2
        |---calculator-2.0
        |---user-1.0

样例如下:

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;
    }


}

最后输出

AdminService doTask3
UserService doTask6
目录
相关文章
|
3天前
|
数据采集 人工智能 安全
|
12天前
|
云安全 监控 安全
|
4天前
|
自然语言处理 API
万相 Wan2.6 全新升级发布!人人都能当导演的时代来了
通义万相2.6全新升级,支持文生图、图生视频、文生视频,打造电影级创作体验。智能分镜、角色扮演、音画同步,让创意一键成片,大众也能轻松制作高质量短视频。
1063 151
|
4天前
|
编解码 人工智能 机器人
通义万相2.6,模型使用指南
智能分镜 | 多镜头叙事 | 支持15秒视频生成 | 高品质声音生成 | 多人稳定对话
|
17天前
|
机器学习/深度学习 人工智能 自然语言处理
Z-Image:冲击体验上限的下一代图像生成模型
通义实验室推出全新文生图模型Z-Image,以6B参数实现“快、稳、轻、准”突破。Turbo版本仅需8步亚秒级生成,支持16GB显存设备,中英双语理解与文字渲染尤为出色,真实感和美学表现媲美国际顶尖模型,被誉为“最值得关注的开源生图模型之一”。
1739 9
|
9天前
|
人工智能 自然语言处理 API
一句话生成拓扑图!AI+Draw.io 封神开源组合,工具让你的效率爆炸
一句话生成拓扑图!next-ai-draw-io 结合 AI 与 Draw.io,通过自然语言秒出架构图,支持私有部署、免费大模型接口,彻底解放生产力,绘图效率直接爆炸。
685 152
|
11天前
|
人工智能 安全 前端开发
AgentScope Java v1.0 发布,让 Java 开发者轻松构建企业级 Agentic 应用
AgentScope 重磅发布 Java 版本,拥抱企业开发主流技术栈。
653 12
|
6天前
|
SQL 自然语言处理 调度
Agent Skills 的一次工程实践
**本文采用 Agent Skills 实现整体智能体**,开发框架采用 AgentScope,模型使用 **qwen3-max**。Agent Skills 是 Anthropic 新推出的一种有别于mcp server的一种开发方式,用于为 AI **引入可共享的专业技能**。经验封装到**可发现、可复用的能力单元**中,每个技能以文件夹形式存在,包含特定任务的指导性说明(SKILL.md 文件)、脚本代码和资源等 。大模型可以根据需要动态加载这些技能,从而扩展自身的功能。目前不少国内外的一些框架也开始支持此种的开发方式,详细介绍如下。
413 4