1. 引言
类加载器(Class Loader)是负责加载类的核心组件。它能在运行时动态地将 Java 类加载到 JVM(Java 虚拟机)中,并作为 JRE(Java 运行时环境)的一部分存在。得益于类加载器,JVM 无需了解底层文件或文件系统即可运行 Java 程序。
JVM 不会一次性加载所有 Java 类,而是在应用程序需要时才加载。这时类加载器就派上用场了——它们负责将类加载到内存中。
本文将深入探讨不同类型的内置类加载器及其工作原理,最后介绍如何实现自定义类加载器。
2. 类加载器的核心功能
类加载器主要有两大功能:
- 加载类:通过内置或自定义类加载器加载类。我们可以继承
java.lang.ClassLoader
抽象类来实现自定义加载器 - 定位资源:资源指
.class
文件、配置信息或图片等数据。通常将资源与应用程序或库打包,方便定位
⚠️ 需要注意:类加载器不会为数组类创建对象。Java 运行时会按需自动创建数组类。因此,当使用 Class#getClassLoader()
获取数组类的加载器时,返回的是其元素类型的加载器。如果元素类型是基本数据类型,则数组类没有类加载器。
3. 内置类加载器类型
Java 运行时支持三种内置类加载器:
- 启动类加载器(Bootstrap Class Loader):JVM 内置加载器,在代码中表示为
null
- 平台类加载器(Platform Class Loader):加载平台类(包括 Java SE 平台 API、实现类及 JDK 特定运行时类),是系统类加载器的父加载器
- 系统类加载器(System Class Loader):也称应用类加载器(Application Class Loader),负责加载应用程序类路径、模块路径和 JDK 特定工具中的类
3.1. 演示示例
通过以下代码演示不同类加载器的加载行为:
public void printClassLoaders() throws ClassNotFoundException {
System.out.println("Platform Classloader:"
+ ClassLoader.getPlatformClassLoader());
System.out.println("System Classloader:"
+ ClassLoader.getSystemClassLoader());
System.out.println("Classloader of this class:"
+ PrintClassLoader.class.getClassLoader());
System.out.println("Classloader of DriverManager:"
+ DriverManager.class.getClassLoader());
System.out.println("Classloader of ArrayList:"
+ ArrayList.class.getClassLoader());
}
执行结果:
Platform Classloader:jdk.internal.loader.ClassLoaders$PlatformClassLoader@5674cd4d
System Classloader:jdk.internal.loader.ClassLoaders$AppClassLoader@33909752
Classloader of this class:jdk.internal.loader.ClassLoaders$AppClassLoader@33909752
Classloader of DriverManager:jdk.internal.loader.ClassLoaders$PlatformClassLoader@5674cd4d
Classloader of ArrayList:null
输出显示三种加载器:启动类加载器(显示为 null
)、平台类加载器和系统类加载器。
- 系统类加载器加载了包含示例方法的类(记住:它负责加载类路径中的文件)
- 平台类加载器加载了
DriverManager
类 - 启动类加载器加载了
ArrayList
类(它是所有加载器的父级,但没有父加载器)
❌ ArrayList
的加载器显示为 null
,因为启动类加载器由本地代码(非 Java)实现,不会显示为 Java 类。其行为在不同 JVM 中可能存在差异。
3.2. 启动类加载器
java.lang.ClassLoader
的实例负责加载 Java 类,但类加载器本身也是类。那么谁加载 java.lang.ClassLoader
呢?这就是启动类加载器(Bootstrap Class Loader)的职责。它主要加载 JDK 内部类(如 rt.jar
)和 $JAVA_HOME/jre/lib
目录下的核心库。启动类加载器是所有其他 ClassLoader
实例的父级。
启动类加载器是核心 JVM 的一部分,由本地代码实现,不同平台可能有不同实现。
3.3. 平台类加载器
平台类加载器是启动类加载器的子加载器,负责加载标准核心 Java 类,使这些类对平台上运行的所有应用程序可用。
3.4. 系统类加载器
系统类加载器(或应用类加载器)负责将所有应用程序级别的类加载到 JVM 中。它加载类路径环境变量、-classpath
或 -cp
命令行选项指定的文件,同时也是平台类加载器的子加载器。
4. 类加载器的工作原理
类加载器是 Java 运行时环境的一部分。当 JVM 请求类时,类加载器会尝试定位类,并通过完全限定名将类定义加载到运行时。java.lang.ClassLoader.loadClass(String name, boolean resolve)
方法负责使用类的二进制名称加载类定义。该方法执行有序搜索:
- 调用
findLoadedClass(String name)
检查类是否已加载。若已加载则返回该类,否则返回null
- 调用父类加载器的
loadClass(String)
方法。若父加载器为null
,则使用 JVM 内置加载器 - 调用
findClass(String)
方法查找类
若未找到类且 resolve
标志为 true
,则对生成的二进制 Class
对象调用 resolveClass(Class)
方法。若类未找到,抛出 java.lang.ClassNotFoundException
。
接下来分析类加载器的三个关键特性。
4.1. 委托模型
委托模型指 ClassLoader
在尝试查找类或资源前,会先委托给父类加载器。默认情况下,委托模型是分层的。ClassLoader
类支持并发加载类,因此具备并行能力。并行加载器可在初始化时注册自身。
类加载器遵循委托模型:当请求查找类或资源时,ClassLoader
实例会先委托给父类加载器。
例如,当请求加载应用类时,系统类加载器首先委托给平台类加载器,平台类加载器再委托给启动类加载器。只有当启动类加载器和平台类加载器都加载失败时,系统类加载器才会尝试自行加载。
4.2. 类的唯一性
委托模型确保了类的唯一性,因为总是优先向上委托。只有当父类加载器无法找到类时,当前实例才会尝试加载。
4.3. 可见性
子类加载器对父类加载器加载的类可见,反之则不成立。
例如:
- 系统类加载器加载的类能看见平台类加载器和启动类加载器加载的类
- 但平台类加载器加载的类无法看见系统类加载器加载的类
若类 A 由应用类加载器加载,类 B 由平台类加载器加载:
- 对其他应用类加载器加载的类,A 和 B 均可见
- 但对其他平台类加载器加载的类,只有 B 可见
5. 自定义类加载器
内置类加载器在文件位于文件系统时已足够。但当需要从本地硬盘或网络加载类时,就需要自定义类加载器。
5.1. 自定义类加载器的使用场景
自定义类加载器不仅用于运行时加载类,典型场景包括:
- 修改现有字节码(如织入代理)
- 动态创建类(如 JDBC 中通过动态类加载切换不同驱动实现)
- 实现类版本控制(为同名同包的类加载不同字节码),可通过 URL 类加载器(通过 URL 加载 JAR)或自定义加载器实现
具体应用案例:
- 浏览器使用自定义类加载器从网站加载可执行内容。不同网页的 Applet 通过独立类加载器加载。即使这些 Applet 同名,若由不同加载器加载,也会被视为不同组件
5.2. 创建自定义类加载器
假设需要从文件加载类,需继承 ClassLoader
并重写 findClass()
方法:
public class CustomClassLoader extends ClassLoader {
@Override
public Class findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassFromFile(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassFromFile(String fileName) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
fileName.replace('.', File.separatorChar) + ".class");
byte[] buffer;
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = 0;
try {
while ( (nextValue = inputStream.read()) != -1 ) {
byteStream.write(nextValue);
}
} catch (IOException e) {
e.printStackTrace();
}
buffer = byteStream.toByteArray();
return buffer;
}
}
此示例中,自定义类加载器扩展了默认加载器,从指定文件加载字节数组。
6. 深入理解 java.lang.ClassLoader
通过分析 java.lang.ClassLoader
的关键方法,更清晰地理解其工作原理。
6.1. loadClass() 方法
根据名称参数加载类,名称参数是完全限定类名:
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
JVM 调用此方法解析类引用时,resolve
设为 true
。但并非总是需要解析类。**若只需判断类是否存在,可将 resolve
设为 false
**。
该方法是类加载器的入口点,默认实现按前述有序搜索类。
6.2. defineClass() 方法
负责将字节数组转换为类实例。使用类前需先解析:
protected final Class<?> defineClass(
String name, byte[] b, int off, int len) throws ClassFormatError
若数据不包含有效类,抛出 ClassFormatError
。此方法标记为 final
,不可重写。
6.3. findClass() 方法
根据完全限定名查找类。遵循委托模型的自定义类加载器需重写此方法:
protected Class<?> findClass(
String name) throws ClassNotFoundException
当父类加载器无法找到请求的类时,loadClass()
会调用此方法。默认实现:若所有父加载器都找不到类,则抛出 ClassNotFoundException
。
6.4. getParent() 方法
返回用于委托的父类加载器:
public final ClassLoader getParent()
某些实现(如 4.4 节所述)用 null
表示启动类加载器。
6.5. getResource() 方法
尝试查找指定名称的资源:
public URL getResource(String name)
首先委托给父类加载器查找资源。若父加载器为 null
,则搜索 JVM 内置类加载器的路径。失败后调用 findResource(String)
查找资源。资源名可相对或绝对于类路径。
返回用于读取资源的 URL 对象;若资源未找到或调用者无足够权限,则返回 null
。
✅ 关键点:Java 从类路径加载资源,且资源加载与位置无关——只要环境配置正确,代码运行位置不影响资源查找。
7. 上下文类加载器
上下文类加载器提供了 J2SE 引入的类加载委托方案的替代方案。
如前所述,JVM 中的类加载器遵循分层模型,除启动类加载器外,每个加载器都有唯一父加载器。但当 JVM 核心类需要动态加载应用开发者提供的类或资源时,可能遇到问题。
例如 JNDI:核心功能由 rt.jar
中的启动类实现,但这些 JNDI 类可能需要加载独立厂商实现的 JNDI 提供者(部署在应用类路径中)。这要求启动类加载器(父加载器)加载应用加载器(子加载器)可见的类。
J2SE 委托模型在此失效,需通过线程上下文加载器解决。
java.lang.Thread
类提供 getContextClassLoader()
方法,返回特定线程的上下文类加载器。上下文类加载器由线程创建者在加载资源和类时设置。自 Java SE 9 起,fork/join 公共池中的线程始终返回系统类加载器作为其上下文类加载器。
8. 总结
类加载器是执行 Java 程序的关键组件。本文深入介绍了其核心概念:
- 讨论了启动类加载器、平台类加载器和系统类加载器三种类型。启动类加载器是所有加载器的父级,负责加载 JDK 内部类;平台类加载器和系统类加载器分别加载 Java 平台类和类路径中的类
- 分析了类加载器的工作原理及委托模型、可见性和唯一性等特性
- 演示了如何创建自定义类加载器
- 介绍了上下文类加载器的概念
掌握类加载器机制对理解 Java 运行时行为至关重要,尤其在处理动态类加载、模块化应用和复杂依赖管理时。