深入ClassLoader:从双亲委派到SPI

读懂了class文件之后, 今天来聊一聊加载class的那些事儿(基于Java SE 8)

一、类加载器

在jvm中主要有三大内置类加载器

加载器类型 实现类 加载路径 源码位置
Bootstrap C/C++实现 sun.boot.class.path hotspot/src/share/vm/...
Extension ExtClassLoader java.ext.dirs sun.misc.Launcher$ExtClassLoader
Application AppClassLoader java.class.path sun.misc.Launcher$AppClassLoader

Bootstrap类加载器

作为jvm最顶层类加载器,jvm实现的一部分,由C/C++实现(HotspotVM是C++)也是唯一一个非ClassLoader子类的加载器,默认情况下负责加载%JAVA_HOME%/jre/lib目录,也可以通过-Xbootclasspath指定加载路径(但这一步有较高的风险性,如果指定加载了与核心jar包同名同路径的class,会造成逻辑破坏)

可以通过getClassLoader()查看类被哪个加载器加载

这段示例代码中输出了两个信息,一是First类本身的类加载器位于rt.jar中的sum.misc.Launcher$AppClassLoader,二则是负责加载核心jar包的BootstrapClassLoader, 但因为Bootstrap是由本地语言实现,所以在java程序中通过getClassLoader()获取其结果显示为null

Extension类加载器

该类加载器是由java代码实现,位于rt.jar中sun.misc.Launcher$ExtClassloader从这里可以发现,他与AppClassLoader同为Launcher的内部类,且都继承自java.net.URLClassLoader这里暂不展开,ExtClassLoader主要负责加载%JAVA_HOME%/jre/lib/ext/目录

比如随便找一个ext下的jaccess.jar中的ButtonTranslator类:

Application类加载器

该类加载器也是java代码实现,主要负责加载classpath目录,也就是我们开发的应用代码的类加载器,比如第一个demo中的First类,其加载器就是AppClassLoader

类加载器层级(实现层面)

这是从实现的角度上Ext/App加载器的关系,从抽象的角度,ClassLoader定义了加载器的规范,SecureClassLoader扩展了安全性,URLClassLoader扩展了通过url加载的能力,而Ext和App则各自分工,那么从逻辑角度,Ext和App包括Bootstrap是如何做到各自分工的呢?

二、双亲委派模型 Parent Delegation Model

基础概念

双亲委派模型是JVM定义的一套默认的类加载机制,当一个类被类加载器加载时,他首先不会自身尝试加载该类,而是把加载请求委托给父加载器去完成,层层往上,只有当所有父加载器都无法完成加载请求时,子加载器才会尝试去加载,其核心概念总结为:自底向上委托,自顶向下加载

源码分析

一、核心初始化流程

1.1 Launcher构建加载器层次

java

ini 复制代码
public Launcher() {
    // 1. 创建ExtClassLoader(父加载器)
    ClassLoader extcl = ExtClassLoader.getExtClassLoader();
    
    // 2. 创建AppClassLoader,传入extcl作为parent
    loader = AppClassLoader.getAppClassLoader(extcl);
}

关键:必须先创建父加载器,再创建子加载器,构建完整的委托链。


二、ExtClassLoader创建过程

2.1 单例模式获取实例

scss 复制代码
public static ExtClassLoader getExtClassLoader() throws IOException {
    if (instance == null) {
        synchronized(ExtClassLoader.class) {
            if (instance == null) {
                instance = createExtClassLoader();  // 双重检查锁
            }
        }
    }
    return instance;
}

2.2 确定扩展加载路径

java 复制代码
private static ExtClassLoader createExtClassLoader() throws IOException {
    return AccessController.doPrivileged(
        new PrivilegedExceptionAction<ExtClassLoader>() {
            public ExtClassLoader run() throws IOException {
                // 获取扩展目录:java.ext.dirs系统属性
                final File[] dirs = getExtDirs();
                return new ExtClassLoader(dirs);  // 创建实例
            }
        });
}

2.3 ExtClassLoader构造函数

java 复制代码
public ExtClassLoader(File[] dirs) throws IOException {
    // 关键:parent=null,表示父加载器是BootstrapClassLoader
    super(getExtURLs(dirs), null, factory);
}

设计要点parent=null表示委托链顶端,由BootstrapClassLoader加载。


三、AppClassLoader创建过程

3.1 获取类路径并创建实例

java 复制代码
public static ClassLoader getAppClassLoader(final ClassLoader extcl) 
        throws IOException {
    
    // 获取应用类路径:java.class.path系统属性
    final String s = System.getProperty("java.class.path");
    final File[] path = getClassPath(s);
    
    return AccessController.doPrivileged(
        new PrivilegedAction<AppClassLoader>() {
            public AppClassLoader run() {
                URL[] urls = pathToURLs(path);
                // 关键:传入extcl作为parent参数
                return new AppClassLoader(urls, extcl);
            }
        });
}

3.2 AppClassLoader构造函数

scss 复制代码
AppClassLoader(URL[] urls, ClassLoader parent) {
    // 关键:parent参数是ExtClassLoader实例
    super(urls, parent, factory);
}

四、核心设计要点

4.1 委托链完整构建

复制代码
启动顺序:ExtClassLoader → AppClassLoader
委托链:AppClassLoader → ExtClassLoader → BootstrapClassLoader

4.2 加载范围配置

加载器 系统属性 职责范围
ExtClassLoader java.ext.dirs 扩展JAR包
AppClassLoader java.class.path 应用类路径

4.3 关键设计决策

  1. 单例模式:ExtClassLoader全局唯一
  2. parent=null:ExtClassLoader父加载器为Bootstrap
  3. 层次传递:AppClassLoader以ExtClassLoader为parent
  4. 安全上下文 :通过AccessController.doPrivileged执行

五、验证委托链

scss 复制代码
// 运行时验证委托链
ClassLoader appLoader = ClassLoader.getSystemClassLoader();
System.out.println(appLoader);  // AppClassLoader
System.out.println(appLoader.getParent());  // ExtClassLoader  
System.out.println(appLoader.getParent().getParent());  // null (Bootstrap)

总结:Launcher通过精确的初始化顺序和参数传递,构建了不可变的三层类加载器委托链,这是双亲委派机制能够稳定运行的基石。

六、核心加载过程

双亲委派机制的执行分为两个明确阶段:

1. 委派阶段(递归向上查询)

请求从子加载器发起,通过parent.loadClass()递归调用,沿加载器层次结构向上传递,直至parentnull的BootstrapClassLoader。这一阶段仅进行类查询,不执行实际加载。

2. 加载阶段(逐级向下尝试)

从BootstrapClassLoader开始,每层加载器依次尝试:

  • findBootstrapClassOrNull() / findClass():在自身责任范围内查找类资源
  • 若找到资源,则通过defineClass()defineClass0()本地方法,将字节码转换为Class对象
  • 若未找到,则返回null或抛出异常,控制权传递到下一级加载器

关键洞察

  • 递归发生在委派阶段,是方法调用层面的递归
  • 实际加载发生在返回阶段,是责任链模式的逐级尝试
  • 每一层加载器都是先依赖父加载器,失败后才自主加载,确保核心类优先由高层加载器加载

这种设计既保证了Java核心库的安全性(防止用户替换核心类),又通过逐级回退机制实现了类加载的灵活扩展。

三、SPI Service Provider Interface

一、SPI核心概念

1.1 什么是SPI?

SPI是Java提供的一种服务发现机制 ,通过解耦接口与实现,实现可插拔的组件扩展

1.2 核心思想

markdown 复制代码
接口定义(标准) ← 配置文件 → 具体实现(扩展)
     ↑                          ↑
    JDK                       开发者

二、SPI打破双亲委派的原理

2.1 经典问题:JDBC驱动加载

ini 复制代码
// 传统方式需要显式加载驱动,硬编码且不够灵活
Connection conn = DriverManager.getConnection(new com.mysql.Driver());

// SPI方式:自动发现并加载驱动
Connection conn = DriverManager.getConnection(url);
//DriverManager通过SPI自动发现

2.2 关键类:ServiceLoader

java 复制代码
// SPI的核心加载器
public final class ServiceLoader<S> implements Iterable<S> {
    // 配置文件路径
    private static final String PREFIX = "META-INF/services/";
    
    // 加载服务实现
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 使用线程上下文类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
}

2.3 打破双亲委派的关键

scss 复制代码
// DriverManager的静态初始化块
static {
    loadInitialDrivers();  // 驱动加载
    println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
    // 通过ServiceLoader加载驱动,load中传递当前线程上下文的ClassLoader进行处理
    ServiceLoader<Driver> loadedDrivers = 
        ServiceLoader.load(Driver.class);
    
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    while (driversIterator.hasNext()) {
        driversIterator.next();  // 触发驱动加载和注册
    }
}

"SPI机制的核心类ServiceLoader在设计时就固定使用线程上下文类加载器来加载服务实现。当由BootstrapClassLoader加载的DriverManager调用ServiceLoader.load(Driver.class)时,ServiceLoader获取当前线程的上下文类加载器(通常是AppClassLoader,或其他自定义加载器),然后在后续的Class.forName(className, false, loader)中明确指定使用这个加载器来加载驱动类。这样就绕过了双亲委派模型中'父加载器加载的代码不能直接访问子加载器中的类'的限制。"

总结

Java类加载体系是一个精心设计的层次化安全模型,双亲委派是其基石,保障了Java平台的核心稳定性。SPI机制则是这一模型的智慧扩展,通过线程上下文类加载器这一"桥梁",在保持安全的前提下实现了父加载器对子加载器服务的发现。这种"规范中的灵活"设计哲学,使得Java既能坚守类型安全的核心原则,又能支撑起庞大的生态系统,成为企业级应用的首选平台。

理解这一机制不仅有助于解决类加载相关问题,更是深入理解Java平台设计思想、编写高质量可扩展代码的关键。从Class.forName的硬编码到SPI的自动发现,从严格委派到有控制打破,Java类加载机制的演进正是软件工程"关注点分离"、"约定优于配置"等原则的完美体现。

相关推荐
MicoZone2 小时前
jvm(更新中)
jvm
东华万里4 小时前
Release 版本禁用 assert:NDEBUG 的底层逻辑与效率优化
java·jvm·算法
听风吟丶7 小时前
Java NIO 深度解析:从核心组件到高并发实战
java·开发语言·jvm
a努力。7 小时前
小红书Java面试被问:ThreadLocal 内存泄漏问题及解决方案
java·jvm·后端·算法·面试·架构
4***14907 小时前
高并发时代的“确定性”挑战——为何稳定性正在成为 JVM 的下一场核心竞争?
java·开发语言·jvm
代码代码快快显灵8 小时前
Android跨应用数据共享:ContentProvider详解
jvm·数据库·oracle
大大大大物~8 小时前
JVM 之 垃圾回收算法及其内部实现原理【垃圾回收的核心问题有哪些?分别怎么解决的?可达性分析解决了什么问题?回收算法有哪些?内部怎么实现的?】
jvm·算法
冰冰菜的扣jio8 小时前
JVM中的垃圾回收详解
java·jvm
未若君雅裁8 小时前
JVM核心原理总结
java·jvm