Skip to content

引导 / 扩展 / 系统类加载器使用

类加载器的三层架构

Java 的类加载器不是扁平的,而是一个分层的树状结构

┌─────────────────────────────────────────────┐
│         引导类加载器(Bootstrap)           │
│     加载 JAVA_HOME/jre/lib/rt.jar 等        │
│     核心 Java 类库(java.lang.String 等)    │
└──────────────────┬────────────────────────┘
                   │  parent = null(顶层)
┌──────────────────▼────────────────────────┐
│        扩展类加载器(Extension)            │
│   加载 JAVA_HOME/jre/lib/ext/*.jar         │
│   或者 -Djava.ext.dirs 指定的目录           │
└──────────────────┬────────────────────────┘
                   │  parent = Bootstrap
┌──────────────────▼────────────────────────┐
│        应用类加载器(Application / System) │
│      加载 classpath、-cp、-jar 指定的类    │
└──────────────────┬────────────────────────┘
                   │  parent = Extension
┌──────────────────▼────────────────────────┐
│         自定义类加载器(User Define)       │
│     继承 ClassLoader,重写 findClass()     │
└─────────────────────────────────────────────┘

引导类加载器(Bootstrap ClassLoader)

这是 JVM 最早初始化的类加载器。它不是用 Java 代码写的(null),而是用 C++ 编写的 native 代码,负责加载 Java 的核心类库。

java
public class BootstrapLoader {
    public static void main(String[] args) {
        // 获取引导类加载器
        ClassLoader bootstrap = String.class.getClassLoader();
        System.out.println(bootstrap);  // null

        // 确认:核心类库都是 Bootstrap 加载的
        ClassLoader object = Object.class.getClassLoader();
        System.out.println(object);  // null
    }
}

它加载什么

JAVA_HOME/jre/lib/ 目录下的核心 JAR:
rt.jar          -- Java 运行时类(java.lang.*, java.util.* 等)
charsets.jar    -- 字符集相关
resources.jar   -- 资源文件
...

注意:不同 JDK 版本目录结构可能略有差异,但核心思想不变。

为什么是 null

String.class.getClassLoader() 返回 null,不代表 String 没有类加载器,而是表示它的类加载器是引导类加载器。JVM 规范规定,如果某个类加载器的父加载器是引导类加载器(即没有父加载器),getClassLoader() 返回 null

扩展类加载器(Extension ClassLoader)

Extension ClassLoader 是标准扩展机制的实现。它的父加载器是 Bootstrap。

java
public class ExtensionLoader {
    public static void main(String[] args) {
        // 获取扩展类加载器
        ClassLoader ext = sun.misc.Launcher.getExtClassLoader();
        System.out.println(ext);
        // sun.misc.ExtClassLoader@...
    }
}

加载位置

扩展类加载器会从两个位置加载类:

  1. JDK 的扩展目录JAVA_HOME/jre/lib/ext/*.jar
  2. 系统属性指定的目录java.ext.dirs 参数指定的路径
bash
# 查看扩展类加载器的加载路径
java -Djava.ext.dirs=/my/ext/dir MyApp

# 默认的扩展目录
# $JAVA_HOME/jre/lib/ext

一个典型的使用场景:你想给所有 Java 应用程序提供一些共享的类库,可以把 JAR 放到扩展目录中,而无需在每个应用的 classpath 中指定。

应用类加载器(Application ClassLoader)

Application ClassLoader(也叫 System ClassLoader)负责加载应用程序 classpath 下的类。它是大多数 Java 应用直接打交道的类加载器。

java
public class ApplicationLoader {
    public static void main(String[] args) {
        // 获取应用类加载器
        ClassLoader app = ApplicationLoader.class.getClassLoader();
        System.out.println(app);
        // sun.misc.Launcher$AppClassLoader@...

        // 获取应用类加载器的父加载器:扩展类加载器
        ClassLoader parent = app.getParent();
        System.out.println(parent);
        // sun.misc.ExtClassLoader@...

        // 父加载器的父加载器:引导类加载器
        System.out.println(parent.getParent());  // null
    }
}

classpath 的来源

Application ClassLoader 加载的 classpath 来源:

  • java -cp 参数指定的目录或 JAR
  • java -jar 指定的 JAR 中的 META-INF/MANIFEST.MFClass-Path 指定的依赖
  • 环境变量 CLASSPATH(JDK 6+ 不推荐设置)

双亲委派的工作过程

三层加载器之间有一个重要的协作机制:双亲委派(Parent Delegation Model)

委派的含义

当一个类加载器收到加载请求时,它不会自己去尝试加载,而是先把请求委派给父加载器处理。所有加载请求最终都会传递到最顶层的 Bootstrap ClassLoader。只有当父加载器无法完成这个请求时,子加载器才会尝试自己加载。

java
// ClassLoader.loadClass() 的核心逻辑(简化)
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 第一步:先问问父加载器能不能加载
    Class<?> c = findLoadedClass(name);  // 先检查是否已加载
    if (c == null) {
        try {
            if (parent != null) {
                // 委派给父加载器
                c = parent.loadClass(name, false);
            } else {
                // 没有父加载器,尝试 Bootstrap
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // 父加载器找不到,子加载器自己来
        }

        // 如果父加载器也没找到,子加载器自己加载
        if (c == null) {
            c = findClass(name);
        }
    }
    return c;
}

为什么要双亲委派

核心目的是安全性:确保核心类库不会被恶意替换。

场景:用户试图用自己写的 java.lang.String 类

Application ClassLoader 收到加载请求

委派给 Extension ClassLoader

委派给 Bootstrap ClassLoader

Bootstrap 加载的是 JAVA_HOME/jre/lib/rt.jar 中的 String

用户写的 java.lang.String 永远不会被加载

防止了恶意代码伪装成核心类库的安全攻击

如果没有双亲委派,用户可以写一个 java.lang.String,在类中植入恶意代码,然后通过 java -cp . 运行。由于 java.lang.String 是所有 Java 程序都会用到的类,这就构成了一个严重的安全漏洞。

类加载器的层级关系图

加载请求


Application ClassLoader
    │ "父加载器,我能加载吗?"

Extension ClassLoader
    │ "父加载器,我能加载吗?"

Bootstrap ClassLoader(顶层)
    │ "这是我的职责范围,我来加载"

返回 Class 对象,逐层向下传递

查看当前 JVM 的类加载器

java
public class ClassLoaderViewer {
    public static void main(String[] args) {
        // Bootstrap
        System.out.println("Bootstrap: " + String.class.getClassLoader());

        // Extension
        System.out.println("Extension: " + sun.misc.Launcher.getExtClassLoader());

        // Application
        System.out.println("Application: " + ClassLoaderViewer.class.getClassLoader());

        // 当前线程的上下文类加载器
        System.out.println("Context: " + Thread.currentThread().getContextClassLoader());
    }
}

类加载器的显示使用

有时候需要在代码中显式使用特定的类加载器来加载类:

java
public class ExplicitLoading {
    public static void main(String[] args) throws Exception {
        // 方式一:直接使用当前类的类加载器
        ClassLoader loader = ExplicitLoading.class.getClassLoader();

        // 方式二:使用线程上下文类加载器
        ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();

        // 方式三:显式指定类加载器
        Class<?> clazz = Class.forName("com.example.MyClass", true, loader);

        // SPI 场景:ServiceLoader 使用线程上下文类加载器
        // ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
    }
}

本节小结

三层类加载器各司其职:

类加载器加载范围父加载器代码表示
Bootstrap核心类库 rt.jar无(nullnull
Extensionjre/lib/ext 下的 JARBootstrapExtClassLoader
Applicationclasspath 下的类ExtensionAppClassLoader

双亲委派机制保证了核心类库的加载安全,也为类加载器的协作提供了清晰的层次。

接下来,我们来看 自定义类加载器(实现与场景),理解如何打破这层默认结构。

基于 VitePress 构建