JVM类加载机制与双亲委派使用及说明

 更新时间:2026年02月06日 15:27:40   作者:是码龙不是码农  
文章介绍了JVM的四大类加载器(启动类加载器、扩展类加载器、应用程序类加载器和自定义类加载器),双亲委派机制及其核心优势,以及如何打破双亲委派机制的场景和实现方式

一、JVM 四大类加载器

JVM 的类加载器(ClassLoader)负责将.class字节码文件加载到内存中,并生成对应的Class对象。

Java 默认提供了四层层级结构的类加载器,从上到下依次为:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器。

1. 层级结构与核心特性

类加载器名称英文 / 简称加载范围实现方式父加载器
启动类加载器Bootstrap ClassLoaderJRE 核心类库:rt.jar、resources.jar、sun.boot.class.path路径下的类(如java.lang.*、java.util.*)C/C++ 实现,JVM 内置,无对应 Java 对象,无法在代码中直接获取无(顶层加载器)

扩展类加载器

(jdk9+后改名为平台类加载器)

Extension ClassLoaderJRE 扩展目录jre/lib/ext、java.ext.dirs指定路径下的扩展类Java 实现,继承自URLClassLoader启动类加载器
应用程序类加载器Application ClassLoader(系统类加载器)项目classpath下的自定义类、第三方依赖包Java 实现,继承自URLClassLoader,是ClassLoader.getSystemClassLoader()的返回值扩展类加载器
自定义类加载器Custom ClassLoader自定义路径的类(网络、加密字节码、自定义目录等)继承ClassLoader重写核心方法实现默认为应用程序类加载器

补充关键说明

  • 层级关系≠继承关系:类加载器的父子关系是组合引用(通过parent字段关联),不是 Java 类的继承;
  • 获取系统类加载器:代码中调用ClassLoader.getSystemClassLoader(),默认获取的就是应用程序类加载器;
  • Bootstrap 特殊点:它是 JVM 本地代码实现的,在 Java 中获取它会返回null

2. 代码验证类加载器

通过简单代码查看不同类的加载器,直观理解层级关系:

public class ClassLoaderDemo {
    public static void main(String[] args) {
        // 1. 核心类:由Bootstrap加载,输出null
        ClassLoader bootstrapLoader = String.class.getClassLoader();
        System.out.println("String类加载器: " + bootstrapLoader);

        // 2. 系统类加载器(应用类加载器)
        ClassLoader appLoader = ClassLoaderDemo.class.getClassLoader();
        System.out.println("自定义类加载器: " + appLoader);

        // 3. 应用类加载器的父加载器:扩展类加载器
        ClassLoader extLoader = appLoader.getParent();
        System.out.println("应用类加载器的父加载器: " + extLoader);

        // 4. 扩展类加载器的父加载器:Bootstrap,输出null
        System.out.println("扩展类加载器的父加载器: " + extLoader.getParent());
    }
}

二、双亲委派机制

1. 定义

双亲委派机制是 JVM 类加载的默认规则:当一个类加载器收到类加载请求时,不会自己先尝试加载,而是将请求向上委托给父加载器,递归向上直到顶层启动类加载器;只有当父加载器无法加载该类时,子加载器才会尝试自己加载。

2. 执行流程(递归逻辑)

自定义类加载器收到加载请求,先检查是否已加载该类,已加载则直接返回Class对象;

未加载则委托给父加载器(应用类加载器)

应用类加载器重复检查逻辑,委托给扩展类加载器

扩展类加载器重复检查逻辑,委托给启动类加载器

启动类加载器尝试在自身加载路径查找类:

  • 找到:直接加载并返回Class对象;
  • 未找到:向下回溯,让子加载器尝试加载;

若所有层级加载器都无法加载,抛出ClassNotFoundException

3. 核心优势

避免类重复加载:保证同一个类在 JVM 中只被加载一次,生成唯一的Class对象;

保障 Java 核心 API 安全(沙箱安全):防止恶意代码自定义核心类(如java.lang.String)替换 JDK 原生类,避免核心 API 被篡改;

层级管理类资源:规范不同范围类的加载边界,提升类加载的稳定性。

4. 源码层面解析

双亲委派的核心逻辑封装在ClassLoaderloadClass(String name, boolean resolve)方法中(JDK8 源码核心逻辑):

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. 无父加载器,直接使用启动类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器无法加载,捕获异常
            }
            // 4. 父加载器加载失败,当前加载器自行加载
            if (c == null) {
                c = findClass(name);
            }
        }
        // 解析类
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

关键方法:findClass()是子类的扩展点,默认抛出异常,自定义加载器需要重写它。

三、打破双亲委派机制

1. 为什么要打破?

默认的双亲委派无法满足所有场景,常见需求:

  • 加载自定义路径 / 特殊来源的类(网络字节码、加密字节码、模块化 jar);
  • 实现类隔离(如 Web 容器 Tomcat、SPI 机制、热部署);
  • 同一个应用中需要加载同一个类的不同版本

2. 打破的核心原理

双亲委派的逻辑写在loadClass()方法中,重写loadClass()方法,跳过向上委托的逻辑,就能直接打破该机制。

3. 标准实现方式:自定义类加载器

步骤

  • 继承java.lang.ClassLoader抽象类;
  • 重写loadClass()方法,破坏默认的双亲委派递归逻辑;
  • 重写findClass()方法,实现自定义的类字节码读取逻辑。

完整示例代码

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * 打破双亲委派的自定义类加载器
 */
public class CustomClassLoader extends ClassLoader {
    // 自定义类加载路径
    private final String classPath;

    public CustomClassLoader(String classPath) {
        // 关键:不指定父加载器(默认父加载器为系统类加载器,也可手动设置)
        this.classPath = classPath;
    }

    /**
     * 重写loadClass,核心:跳过双亲委派的向上委托逻辑
     */
    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 1. 检查是否已加载
            Class<?> clazz = findLoadedClass(name);
            if (clazz != null) {
                return clazz;
            }

            // 2. 打破规则:核心类仍交给Bootstrap加载,避免JDK核心类加载异常
            if (name.startsWith("java.")) {
                return getSystemClassLoader().loadClass(name);
            }

            // 3. 不委托父加载器,直接自己加载
            try {
                clazz = findClass(name);
            } catch (Exception e) {
                // 加载失败,降级为系统类加载器加载
                return super.loadClass(name, resolve);
            }

            if (resolve) {
                resolveClass(clazz);
            }
            return clazz;
        }
    }

    /**
     * 重写findClass:从自定义路径读取字节码
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassBytes(name);
        if (classData == null) {
            throw new ClassNotFoundException("无法找到类:" + name);
        }
        // 将字节码转换为Class对象
        return defineClass(name, classData, 0, classData.length);
    }

    /**
     * 从文件系统读取.class字节码
     */
    private byte[] getClassBytes(String className) {
        String path = classPath + "/" + className.replace(".", "/") + ".class";
        try (FileInputStream fis = new FileInputStream(path);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {

            byte[] buffer = new byte[1024];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            return bos.toByteArray();
        } catch (IOException e) {
            return null;
        }
    }
}

测试代码

public class TestCustomLoader {
    public static void main(String[] args) throws Exception {
        // 自定义加载路径
        CustomClassLoader loader = new CustomClassLoader("D:/test/classes");
        // 加载自定义类
        Class<?> clazz = loader.loadClass("com.test.DemoClass");
        // 打印加载器,验证为自定义加载器
        System.out.println("类加载器: " + clazz.getClassLoader());
    }
}

4. 行业中打破双亲委派的经典场景

场景 1:Tomcat 等 Web 容器

  • 需求:一个 Web 服务器部署多个应用,不同应用可能依赖同一个类的不同版本,需要类隔离;
  • 实现:每个 Web 应用对应一个独立的WebAppClassLoader优先加载应用自身的类,跳过双亲委派向上委托逻辑,避免类冲突。

场景 2:JDBC SPI 服务发现

  • JDK 的rt.jar中定义了java.sql.Driver接口(由 Bootstrap 加载),但具体驱动实现类(MySQL/PostgreSQL 驱动)在classpath下;
  • Bootstrap 加载器无法加载应用层的驱动类,因此通过Thread.getContextClassLoader()(线程上下文类加载器)打破委派,让启动类加载器反向使用应用类加载器加载实现类。

场景 3:热部署 / 热加载

  • 如 JRebel、Spring Boot DevTools,通过自定义类加载器重新加载修改后的类,绕过默认的缓存机制,实现代码热更新。

总结

核心知识点回顾:

  1. 四大类加载器:Bootstrap(顶层、C 实现)→ Extension → Application → 自定义加载器,层级为组合关系而非继承;
  2. 双亲委派机制:向上委托、向下加载,核心价值是防重复加载、保障核心 API 安全,逻辑在ClassLoader.loadClass()中;
  3. 打破方式重写loadClass()方法跳过向上委托,配合自定义findClass()实现个性化加载;
  4. 工业级场景:Tomcat 类隔离、JDBC SPI、热部署框架是打破双亲委派的典型应用。

面试答题话术参考:

  • JVM 有四层类加载器,从顶层到底层分别是启动类、扩展类、应用类和自定义类加载器。
  • 双亲委派是类加载的默认规则,请求会递归向上委托给父加载器,父加载器无法加载时子加载器才会处理,主要作用是避免类重复和保证核心类安全。
  • 打破该机制的核心方法是自定义 ClassLoader 并重写 loadClass 方法,跳过委托逻辑,像 Tomcat 的类隔离、JDBC 的 SPI 机制都是实际业务中打破该机制的经典场景。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 基于Java 生产者消费者模式(详细分析)

    基于Java 生产者消费者模式(详细分析)

    下面小编就为大家分享一篇基于Java 生产者消费者模式(详细分析),具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-01-01
  • 如何在Spring Boot启动时运行定制的代码

    如何在Spring Boot启动时运行定制的代码

    在本文中您将学习如何挂钩应用程序引导程序生命周期并在Spring Boot启动时执行代码。文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-12-12
  • Java实现的计算稀疏矩阵余弦相似度示例

    Java实现的计算稀疏矩阵余弦相似度示例

    这篇文章主要介绍了Java实现的计算稀疏矩阵余弦相似度功能,涉及java基于HashMap的数值计算相关操作技巧,需要的朋友可以参考下
    2018-07-07
  • 一篇文章带你深入了解Java类加载

    一篇文章带你深入了解Java类加载

    这篇文章主要介绍了Java中类加载过程全面解析,具有一定参考价值,需要的朋友可以了解下,希望能给你带来帮助
    2021-08-08
  • Mybatis基于注解与XML开发使用流程

    Mybatis基于注解与XML开发使用流程

    MyBatis是Java的持久化框架,目的是为了使操作数据库更加方便、灵活、高效,可以通过Java注解和XML文件来映射Java对象和SQL语句,提供了非常灵活的SQL编写方式和动态SQL语句的创建方式,这篇文章主要介绍了Mybatis基于注解与XML开发,需要的朋友可以参考下
    2023-07-07
  • springboot实现单、多文件上传过程(前后端都有)

    springboot实现单、多文件上传过程(前后端都有)

    Spring Boot中实现文件上传,前端使用`file`类型的`input`表单,后端通过`MultipartFile`类接收文件,上传接口接收`MultipartFile`参数,调用其方法处理文件,单文件上传直接处理,多文件上传需遍历`MultipartFile`数组
    2025-12-12
  • Java内部类和匿名内部类的用法说明

    Java内部类和匿名内部类的用法说明

    这篇文章主要介绍了Java内部类和匿名内部类的用法说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08
  • 利用POI生成EXCEL文件的方法实例

    利用POI生成EXCEL文件的方法实例

    Apache POI 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java程式对Microsoft Office格式档案读和写的功能,下面这篇文章主要给大家介绍了关于利用POI生成EXCEL文件的相关资料,需要的朋友可以参考下
    2018-07-07
  • Java静态和非静态成员变量初始化过程解析

    Java静态和非静态成员变量初始化过程解析

    这篇文章主要介绍了Java静态和非静态成员变量初始化过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • SpringBoot利用jasypt实现配置文件加密的完整指南

    SpringBoot利用jasypt实现配置文件加密的完整指南

    在实际开发中,若出于安全考虑不想暴露一些敏感的配置,如数据库密码等,就需要对配置文件进行加密,下面我们来看看如何使用jasypt给配置文件加密吧
    2025-03-03

最新评论