概述
不同 JVM 实现细节各异,下文只讨论 HotSpot 在 JDK 9 及以后(含当前 JDK 26)的行为。JDK 8 及更早的 ExtClassLoader、jre/lib/ext、sun.misc.Launcher 等表述见文末「JDK 8 及更早」。
本文从四方面展开:内置 ClassLoader 层次、双亲委托、自定义 ClassLoader、打破双亲委托的常见场景。类加载器「去哪里找类」由 classpath / module path 决定,概念见 Java Classpath 与 Module Path。
内置 ClassLoader 层次
JDK 运行时提供三个内置加载器(ClassLoader 官方文档):
| 加载器 | 获取方式 | 职责 |
|---|---|---|
| Bootstrap | Java 代码中为 null |
加载 java.base 等核心模块;C++ 实现,不是 ClassLoader 子类 |
| Platform(原 Extension) | ClassLoader.getPlatformClassLoader() |
加载 Java SE 平台 API 及其模块(非应用 classpath) |
| System / Application | ClassLoader.getSystemClassLoader() |
加载应用 classpath、module path 上的类 |
父子关系:
Bootstrap (null)
└── PlatformClassLoader
└── AppClassLoader (System ClassLoader)
实现类位于 jdk.internal.loader.ClassLoaders(PlatformClassLoader、AppClassLoader),均为内部 BuiltinClassLoader 体系。不要依赖具体类名写业务逻辑。
Bootstrap ClassLoader
- 虚拟机内置,没有 Java 父加载器;在
getParent()链上用null表示。 - 由 C++ 实现,不继承
java.lang.ClassLoader,无法在 Java 里直接拿到实例。 - JDK 9 起通过模块系统加载核心库(如
java.base中的java.lang.*),不再从rt.jar或%JAVA_HOME%/jre/lib整包加载。
PlatformClassLoader
- JDK 9 起取代 ExtClassLoader(扩展类加载器)。
- 负责 Java SE 平台类:平台 API、其实现类,以及 JCP 标准化但不在 SE 核心里的模块。
- 平台类由哪个加载器定义(Bootstrap 或 Platform)因模块而异,应用代码不应假设「某个平台类一定由 Platform 加载」。
- 为支持模块升级/覆盖,Platform 有时需要委托给 System 等其它加载器,可见性比 JDK 8 的纯树形委托更灵活。
AppClassLoader(System ClassLoader)
ClassLoader.getSystemClassLoader()的返回值,也是默认的 context class loader 来源之一。- 加载应用 classpath、module path,以及部分 JDK 工具模块(如
jdk.compiler)。 - 线程上下文类加载器(
Thread.setContextClassLoader)常用于 SPI 等场景(见下文)。
与 URLClassLoader 的关系
JDK 8 中 AppClassLoader、ExtClassLoader 继承 java.net.URLClassLoader。JDK 9 起内置加载器不再继承 URLClassLoader;自定义加载器仍可用 URLClassLoader 或继承 ClassLoader 自行实现 findClass。
查看当前层次
public class ClassLoaderProbe {
public static void main(String[] args) {
ClassLoader ctx = Thread.currentThread().getContextClassLoader();
ClassLoader sys = ClassLoader.getSystemClassLoader();
ClassLoader platform = ClassLoader.getPlatformClassLoader();
System.out.println("context: " + ctx);
System.out.println("system: " + sys);
System.out.println("platform: " + platform);
System.out.println("system.parent: " + sys.getParent());
System.out.println("platform.parent: " + platform.getParent()); // null => Bootstrap
}
}
在 JDK 26 上典型输出类似:
system: jdk.internal.loader.ClassLoaders$AppClassLoader@...
platform: jdk.internal.loader.ClassLoaders$PlatformClassLoader@...
system.parent: jdk.internal.loader.ClassLoaders$PlatformClassLoader@...
platform.parent: null
双亲委托模型
加载类时,ClassLoader 默认采用 parent-first(双亲委托):
- 查当前加载器是否已加载(
findLoadedClass)。 - 未命中则委托 父加载器 加载(父加载器递归同样流程,直到 Bootstrap)。
- 父链都未加载到时,由 当前加载器 执行
findClass(或内置加载器的等价逻辑)并缓存。
为什么要委托
JVM 里类的身份是 全限定名 + 定义该类的 ClassLoader(命名空间)。同名类被不同加载器加载,在 JVM 中是两个不同的 Class。
委托模型保证 java.util.HashMap 等核心类由 Bootstrap(或平台模块路径)统一加载,应用里的多个自定义加载器共享同一份 JDK 类型,避免「同名不同类」的混乱。
loadClass 如何实现委托
loadClass(String, boolean) 未被 final 修饰,子类可以 override,从而打破双亲委托。默认实现逻辑(简化,与 JDK 26 源码一致):
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
if (c == null) {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
自定义加载器若遵循双亲委托,通常只 override findClass;若 override loadClass,则完全自行控制委托顺序。
findClass 与 defineClass
findClass:默认抛ClassNotFoundException,留给子类实现(从磁盘、网络、内存等读取字节码)。defineClass:final,是 JVM 校验并注册Class对象的入口;字节码须符合 class 文件格式。
能否用自定义加载器替换 java.lang.String?不能。即使打破双亲委托,ClassLoader 对 java.* 等包有访问检查,且 Bootstrap 已加载的核心类不会被应用加载器重新定义。
打破双亲委托的场景
SPI 与线程上下文类加载器
JDBC、JNDI 等 SPI 的接口在 JDK(Bootstrap / Platform)里,实现 JAR 在应用 classpath 上。接口侧无法直接 Class.forName 到厂商实现,因此使用 Thread.currentThread().getContextClassLoader()(默认多为 AppClassLoader)由「子侧」加载 SPI 实现。这是典型的 父加载器需要子加载器已加载的类 的反常情况。
模块化与 OSGi、应用服务器
OSGi、部分 Java EE / 应用服务器会为每个模块/部署单元使用独立 ClassLoader,并采用 child-first 或扁平委托,不再严格 parent-first。
自定义 ClassLoader 简要流程
- 定位字节码(classpath、URL、数据库等)。
defineClass将字节注册为Class。- 用
Class.getDeclaredConstructor().newInstance()等创建实例(勿用已废弃的Class.newInstance())。
URL url = new URL("file:/path/to/classes/");
URLClassLoader loader = new URLClassLoader(new URL[] { url });
Class<?> clazz = loader.loadClass("com.example.Student");
Object stu = clazz.getDeclaredConstructor().newInstance();
不同 ClassLoader:谁能看到谁
JVM 里类的身份是 全限定名 + 定义该类的 ClassLoader(上文「命名空间」)。因此:
| 关系 | 可见性 |
|---|---|
| 子加载器 → 父加载器已加载的类 | 可见(双亲委托:先让父加载) |
| 父加载器 → 子加载器加载的类 | 默认不可见 |
| 两个无父子关系的加载器(兄弟) | 互不可见(各自命名空间),除非用反射等方式桥接 |
「互相看不到」指的是:不能用 Foo.class 直接当同一个类型用(instanceof、赋值会失败),不是进程里完全没有这份字节码。
自定义 URLClassLoader 加载的 com.app.Plugin 与 AppClassLoader 加载的 com.app.Plugin,在 JVM 里是 两个不同的 Class。
与 Java Agent 的关系
Attach loadAgent 后,agent JAR 里的类 通常不走业务 AppClassLoader 去 loadClass,而是由 JVM 的 agent 机制装入(与业务 classpath 分离,减少版本冲突)。
但这 不意味着 agent 与业务「完全隔离、改不了业务」:
Instrumentation+ClassFileTransformer改的是 业务类对应的字节码(定义类仍由业务 ClassLoader 加载,只是retransform换了方法体里的指令)。- 织入的探针里可以
INVOKESTATIC到 agent 包里的类,或读写业务对象——运行时都在 同一进程。
JDK 8 及更早(历史参考)
| 现代名称 | JDK 8 及更早 |
|---|---|
| PlatformClassLoader | ExtClassLoader(sun.misc.Launcher$ExtClassLoader) |
| AppClassLoader | sun.misc.Launcher$AppClassLoader |
| 平台模块 | %JAVA_HOME%/jre/lib/ext、java.ext.dirs |
| 核心库 | rt.jar、sun.boot.class.path |
| 内置加载器基类 | 二者均继承 URLClassLoader |
JDK 9 引入模块系统后,上述路径与类名在 HotSpot 中已废弃或仅作兼容;学习新项目应以本文「内置 ClassLoader 层次」一节为准。
维护记录
| 时间 | 修改内容 | 原因 |
|---|---|---|
| 2026-06-21 | 按 JDK 9+ / Java 26 重写内置加载器、双亲委托与示例;删除 JDK 8 / 外文转载冗余段落;合并 PlatformClassLoader 说明 | 原文 ExtClassLoader、URLClassLoader、jre/lib/ext 等与现行 HotSpot 不符 |
| 2026-06-28 | 概述补充 classpath / module path 概念链到新建 classpath.md | 拆分 classpath 概念专文,java-classloader-2.md 退役合并至此 |