ClassLoader体系概述

3 阅读7分钟

ClassLoader体系概述

最近项目中需要运行时加载自定义 Jar 包,涉及到 Java 的 ClassLoader 体系。为了彻底搞清楚其中的原理(特别是 JDK 9 模块化后的变化),以及 Spring Boot 胖 Jar 中的自定义类加载机制,我整理了这份笔记,也方便以后查阅。

1.本文结构

  1. ClassLoader 的类体系
    展示 ClassLoader 的继承关系图,并解释为何存在两条继承线(JDK 内部实现线 vs 公开 API 线),以及各自职责。

  2. 两条继承线的分工 详细阐述 JDK 内部实现线(模块化感知、双亲委派)和公开 API 线(可扩展、URLClassLoader 体系)的分工与总结。

  3. 模块化下的双亲委派体系
    通过堆栈分析说明 JDK 9+ 模块化系统如何改变线性委派模型;给出 AppClassLoader 加载类的详细流程图,并以 java.sql.ConnectionStringcom.example.MyService 为例演示模块化路由过程。

  4. 自定义 ClassLoader(以 LaunchedClassLoader 为例) 结合 Spring Boot 源码,展示自定义类加载器的创建与委派链,重点分析其 loadClass 方法的特殊处理逻辑(包前缀绕过双亲委派),并解释为何需要这样设计。

2.ClassLoader的类体系

graph TD
    A[java.lang.ClassLoader] --> B[jdk.internal.loader.BuiltinClassLoader]
    B --> C[PlatformClassLoader]
    B --> D[AppClassLoader]
    A --> E[java.security.SecureClassLoader]
    E --> F[java.net.URLClassLoader]
    A --> G[... 其他自定义类加载器]
    F --> H[Spring Boot LaunchedClassLoader]

可以看出,上图的继承体系分为两条线。
左边的PlatformClassLoader和AppClassLoader都继承了jdk.internal.loader.BuiltinClassLoader,jdk.internal.loader.BuiltinClassLoader继承了java.lang.ClassLoader。
右边的Spring Boot LaunchedClassLoader继承了java.net.URLClassLoader,java.net.URLClassLoader继承了java.security.SecureClassLoader,java.security.SecureClassLoader继承了java.lang.ClassLoader。


3. 两条继承线的分工

分支一:JDK 内部实现线
职责:

  • 双亲委派:继承自 ClassLoader的模板方法loadClass,并且BuiltinClassLoader在委派前增加了模块化路由(findLoadedModule精准直达模块所属加载器)。
  • 模块化感知:维护packageToModule映射,确保模块边界和包归属不被破坏。
  • 类路径来源:PlatformClassLoader和AppClassLoader分别负责不同的类来源(模块路径 vs classpath),完成 JVM 自身的类加载体系。

分支二:公开 API 线(提供可扩展的类加载器)
职责:

  • 提供开发者接口:任何想要自定义类路径的人,只需要继承URLClassLoader并传入一组URL,无需关心底层字节码读取细节。
  • 不关心模块:URLClassLoader完全不涉及 JPMS 的模块系统,它只知道从给定的 URL列表里找类和资源(适用于传统的 classpath 或 Spring Boot 的 jar:nested场景)。
  • 安全能力:通过父类SecureClassLoader绑定了CodeSource和ProtectionDomain,所以即使是自定义加载器,加载的类也带安全出身。

总结:一条是 JVM 的“内政官”,负责系统稳定和模块秩序;另一条是“开放口岸”,允许世界各地的代码以统一方式进入 Java 运行时。因此,当我们真正需要实现一个可加载外部 Jar 的自定义类加载器时,几乎无一例外地会继承 URLClassLoader(或它的子类,如 Spring Boot 的 LaunchedClassLoader)。


4.ClassLoader在模块化下的双亲委派体系

先看看SpringBoot胖jar加载org.springframework.boot.loader.launch.JarLauncher类的堆栈。 加载JarLauncher的是AppClassLoader。

动画1.gif 如上面动图所展示的堆栈,在LauncherHelper的loadMainClass中,调用Class.forName加载Launcher类。之后进入native方法forName0,JVM再调用loadClass方法。那么JVM具体调用哪个ClassLoader的loadClass方法呢?这就看native方法中传入的loader具体是哪个对象,JVM会根据传入的对象,调用该对象的loadClass方法。

private static native Class<?> forName0(String name, boolean initialize,  
ClassLoader loader,  
Class<?> caller)  
   throws ClassNotFoundException;

通过堆栈看到,首先进入的是ClassLoader的loadClass,那是不是说明forName0传入的对象是ClassLoader对象呢?不是的,我们通过前面的堆栈可以看到传入的ClassLoader其实是AppClassLoader的对象。AppClassLoader是ClassLoader的子类,它并未覆写loadClass函数,所以调用loadClass函数时,实际上是调用它的父类ClassLoader的loadClass函数。后面看到堆栈两次进入BuiltinClassLoader的loadClassOrNull函数,也是这个道理。

在没有模块化的 JDK 8 及之前,委托链是线性的:

AppClassLoader
  └─ parent → PlatformClassLoader
                └─ parent → BootstrapClassLoader

JDK 9 引入模块系统后,BuiltinClassLoader 作为 PlatformClassLoader和AppClassLoader的共同父类,多了一个关键数据结构:

// 包名 → 该包所属的已解析模块
ConcurrentHashMap<String, LoadedModule> packageToModule;

这个映射表记录了哪个包归属于哪个模块。而模块系统要求:如果一个包已经被某个模块声明,那么该包下的所有类,必须由定义该模块的类加载器来加载。 这就打破了一味向上委托的线性模型,变成了先查模块映射,再按需委托的图状模型。
梳理一下AppClassLoader加载类的流程图,如下。

flowchart TD
    Start["AppClassLoader.loadClass(cn)"] --> Builtin[调用 BuiltinClassLoader.loadClass]
    Builtin --> LONull["调用 loadClassOrNull(cn, resolve)"]
    
    LONull --> Sync["synchronized (getClassLoadingLock(cn))"]
    
    Sync --> CheckLoaded{"1. findLoadedClass(cn)\n是否已加载?"}
    CheckLoaded -- "是" --> ReturnC["返回已加载的 Class"]
    
    CheckLoaded -- "否" --> ModuleLookup{"2. findLoadedModule(cn)\n查找包所属模块"}
    
    %% 有模块的分支
    ModuleLookup -- "找到模块 M" --> CheckLoader{"M.loader() == this ?\n模块是否属于当前加载器?"}
    CheckLoader -- "是 (属于自己)" --> WaitModule{"VM.isModuleSystemInited() ?"}
    WaitModule -- "是" --> LoadFromModule["findClassInModuleOrNull(loadedModule, cn)"]
    LoadFromModule --> ReturnC
    
    WaitModule -- "否 (系统未就绪)" --> ReturnC
    
    CheckLoader -- "否 (属于其他加载器)" --> Delegate["c = M.loader().loadClassOrNull(cn)\n直接跳转到模块所属加载器"]
    Delegate --> ReturnC
    
    %% 无模块的分支
    ModuleLookup -- "未找到模块\n(类在 classpath 上)" --> CheckParent{"3. parent != null ?"}
    
    CheckParent -- "是" --> ParentLoad["c = parent.loadClassOrNull(cn)\n委派 PlatformClassLoader"]
    
    %% PlatformClassLoader 内部
    ParentLoad --> PLAT["PlatformClassLoader\n走同样的 loadClassOrNull 流程"]
    PLAT --> PLAT_Module{"findLoadedModule(cn) ?"}
    PLAT_Module -- "找到平台模块" --> PLAT_Own{"属于自己 ?"}
    PLAT_Own -- "是" --> PLAT_Load["从平台模块加载"]
    PLAT_Load --> ParentResult["返回 c 或 null"]
    PLAT_Own -- "否(Bootstrap)" --> PLAT_Delegate["委托 Bootstrap"]
    PLAT_Delegate --> ParentResult
    
    PLAT_Module -- "未找到" --> PLAT_Parent{"parent != null ?"}
    PLAT_Parent -- "否 (null = Bootstrap)" --> Bootstrap["findBootstrapClassOrNull(cn)"]
    Bootstrap --> PLAT_ClassPath{"Bootstrap 找到 ?"}
    PLAT_ClassPath -- "是" --> ParentResult
    PLAT_ClassPath -- "否" --> PLAT_OwnCP["PlatformCL 查自己的 classpath"]
    PLAT_OwnCP --> ParentResult
    
    ParentResult --> CheckParentResult{"c != null ?"}
    CheckParentResult -- "是" --> ReturnC
    CheckParentResult -- "否" --> OwnClassPath
    
    CheckParent -- "否 (parent 为 null)" --> OwnClassPath
    
    %% AppCL 自己的 classpath
    OwnClassPath["4. 有 classpath 且 VM 已启动 ?"] -->|"是"| FindOnCP["findClassOnClassPathOrNull(cn)\n扫描 classpath 上所有 JAR 和目录"]
    FindOnCP --> ReturnC
    
    OwnClassPath -->|"否"| ReturnNull["返回 null"]
    
    %% 最终结果
    ReturnC --> FinalCheck{"c == null ?"}
    FinalCheck -- "是" --> ThrowCNF["throw ClassNotFoundException"]
    FinalCheck -- "否" --> Resolve{"resolve == true ?"}
    Resolve -- "是" --> DoResolve["resolveClass(c)"]
    DoResolve --> Done["返回 Class"]
    Resolve -- "否" --> Done

    %% 样式定义
    style ModuleLookup fill:#e1f5ff,stroke:#01579b,stroke-width:2px
    style CheckLoader fill:#e1f5ff,stroke:#01579b,stroke-width:2px
    style CheckParent fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style FindOnCP fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
    style ReturnC fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    style ThrowCNF fill:#ffebee,stroke:#b71c1c,stroke-width:2px

与传统的双亲委派相比,模块化的双亲委派,在委派parent类加载器加载之前,会先做模块化判断。
模块化路由举例
a. 加载 java.sql.Connection

假设应用代码触发 AppClassLoader.loadClass("java.sql.Connection")

步骤 1:模块化判断

findLoadedModule("java.sql.Connection")

提取包名 java.sql

  • 查到它属于 java.sql 模块
  • 该模块被 JDK 内部定义,由 PlatformClassLoader 负责加载
  • 发现 PlatformClassLoader ≠ 当前对象(AppClassLoader)
  • 直接委托给 PlatformClassLoader.loadClass("java.sql.Connection")

步骤 2:PlatformClassLoader 处理

PlatformClassLoader 自己的 loadClass

  • 检查缓存 → 未找到
  • 进入标准双亲委派:super.loadClass → ClassLoader.loadClass
  • 委托 parent(Bootstrap) → Bootstrap 找不到
  • 自己 findClass("java.sql.Connection") → 找到,返回

这样,即使 AppClassLoader 和 PlatformClassLoader 之间还存在传统的父子链,模块化判断绕过了逐步向上委派的步骤,直接把请求路由到了正确的模块加载器。

b. 加载 String

触发 AppClassLoader.loadClass("java.lang.String")

  • findLoadedModule("java.lang.String") → 包 java.lang → 属于 java.base 模块
  • java.base 由 Bootstrap 定义
  • 委托给 Bootstrap → 直接返回已加载的 String.class

这里模块化判断甚至直接跳过了 PlatformClassLoader。

c. 加载自己应用里的 com.example.MyService

触发 AppClassLoader.loadClass("com.example.MyService")

  • findLoadedModule("com.example.MyService") → 包 com.example
  • packageToModule 中没有该包的条目(你的应用代码不在模块路径上,而是在 classpath 上)
  • 返回 null → 走标准双亲委派
  • ClassLoader.loadClass → 委派 Platform → 委派 Bootstrap → 都找不到
  • AppClassLoader.findClass("com.example.MyService") → 从 classpath 找到

5. 自定义ClassLoader,以LaunchedClassLoader举例

在springboot的Launcher中有这样一段代码

private ClassLoader createClassLoader(URL[] urls) {  
    // 获取当前的类加载器,当前类是Launcher,类加载器是AppClassLoader
    ClassLoader parent = getClass().getClassLoader(); 
    // 创建一个LauncherClassLoader对象,将parent(即AppClassLoader)作为父加载器。
    return new LaunchedClassLoader(isExploded(), getArchive(), urls, parent);  
}

故LauncherClassLoader的实例委托链为:

BootstrapClassLoader (C++, null)
         ↑ parent
PlatformClassLoader
         ↑ parent
AppClassLoader
         ↑ parent
LaunchedClassLoader   ← 自定义加载器

LauncherClassLoader的loadClass比较简单,代码如下

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {  
    if (name.startsWith(JAR_MODE_PACKAGE_PREFIX) || name.equals(JAR_MODE_RUNNER_CLASS_NAME)) {  
        try {  
            Class<?> result = loadClassInLaunchedClassLoader(name);  
            if (resolve) {  
                resolveClass(result);  
            }  
            return result;  
        }  
        catch (ClassNotFoundException ex) {  
        // Ignore  
        }  
    }  
    return super.loadClass(name, resolve);  
}

逐层逻辑

  1. 判断类名是否需要特殊处理 如果全限定类名以 JAR_MODE_PACKAGE_PREFIX(例如 org.springframework.boot.loader.)开头, 或者类名严格等于 JAR_MODE_RUNNER_CLASS_NAME
    则进入自定义加载流程。
    否则,直接走标准双亲委派 super.loadClass(name, resolve)
  2. 自定义加载流程
    调用 loadClassInLaunchedClassLoader(name),该方法内部会使用 LaunchedClassLoader 的 URL 类路径(包含 BOOT-INF/classes/ 和 BOOT-INF/lib/*.jar 的 jar:nested URL)去查找并定义类。 如果找到并成功加载,得到一个 Class<?> 对象 result。 如果 resolve 参数为 true,则调用 resolveClass(result) 完成该类的链接(解析符号引用)。 返回已加载的 Class 对象。
  3. 标准双亲委派
    对于不匹配包前缀的类,或者自定义加载器找不到的类,调用 super.loadClass(name, resolve)。 这会将请求交给父类 ClassLoader 的模板方法,执行“先委托父加载器,找不到再自己 findClass”的经典流程。 这里的父加载器通常是 AppClassLoader,因此会向上委托到 PlatformClassLoader 和 BootstrapClassLoader 去查找核心类库和 classpath 上的类。

为什么要有这段逻辑?

回顾 Spring Boot 的胖 JAR 结构:

app.jar
├── org/springframework/boot/loader/...   ← 启动器自己的类
├── BOOT-INF/classes/                     ← 你的应用类
└── BOOT-INF/lib/                         ← 你的第三方依赖

启动器在运行时,创建了一个自定义的ClassLoader(例如LaunchedClassLoader),它知道如何从BOOT-INF/下面加载类。但是JVM 启动时,首先加载的是启动器自身的类(org.springframework.boot.loader.*),这些类是由 AppClassLoader从胖 JAR 的根目录加载的。

现在问题来了:自定义的ClassLoader继承自URLClassLoader,按照双亲委派模型,所有加载请求会先向上委派给AppClassLoader。如果应用代码中引用了某个同时存在于启动器包路径和BOOT-INF/classes/中的类,那么AppClassLoader会优先从根目录找到它,而不是从BOOT-INF/classes/加载。这会导致类版本混乱,尤其是当 Spring Boot 的启动器依赖和用户应用依赖存在重叠时。

此外,某些特定的“桥接”类(如JarModeRunner)必须由自定义加载器加载,以确保它们能访问到嵌套 JAR 内部的类。