引导 / 扩展 / 系统类加载器使用
类加载器的三层架构
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 的核心类库。
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。
public class ExtensionLoader {
public static void main(String[] args) {
// 获取扩展类加载器
ClassLoader ext = sun.misc.Launcher.getExtClassLoader();
System.out.println(ext);
// sun.misc.ExtClassLoader@...
}
}加载位置
扩展类加载器会从两个位置加载类:
- JDK 的扩展目录:
JAVA_HOME/jre/lib/ext/*.jar - 系统属性指定的目录:
java.ext.dirs参数指定的路径
# 查看扩展类加载器的加载路径
java -Djava.ext.dirs=/my/ext/dir MyApp
# 默认的扩展目录
# $JAVA_HOME/jre/lib/ext一个典型的使用场景:你想给所有 Java 应用程序提供一些共享的类库,可以把 JAR 放到扩展目录中,而无需在每个应用的 classpath 中指定。
应用类加载器(Application ClassLoader)
Application ClassLoader(也叫 System ClassLoader)负责加载应用程序 classpath 下的类。它是大多数 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参数指定的目录或 JARjava -jar指定的 JAR 中的META-INF/MANIFEST.MF中Class-Path指定的依赖- 环境变量
CLASSPATH(JDK 6+ 不推荐设置)
双亲委派的工作过程
三层加载器之间有一个重要的协作机制:双亲委派(Parent Delegation Model)。
委派的含义
当一个类加载器收到加载请求时,它不会自己去尝试加载,而是先把请求委派给父加载器处理。所有加载请求最终都会传递到最顶层的 Bootstrap ClassLoader。只有当父加载器无法完成这个请求时,子加载器才会尝试自己加载。
// 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 的类加载器
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());
}
}类加载器的显示使用
有时候需要在代码中显式使用特定的类加载器来加载类:
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 | 无(null) | null |
| Extension | jre/lib/ext 下的 JAR | Bootstrap | ExtClassLoader |
| Application | classpath 下的类 | Extension | AppClassLoader |
双亲委派机制保证了核心类库的加载安全,也为类加载器的协作提供了清晰的层次。
接下来,我们来看 自定义类加载器(实现与场景),理解如何打破这层默认结构。
