JVM - 再谈类的加载器
# 1. 类加载器概述
类加载器 (ClassLoader) 是 Java 虚拟机 (JVM) 执行类加载机制的基础和核心组件。
# 1.1 ClassLoader 的核心作用
ClassLoader 的主要职责是负责定位并加载类(或接口)的二进制字节流 (.class
文件数据),将其读入 JVM 内存中,并最终转换成一个与该类对应的 java.lang.Class
对象实例。这个 Class
对象随后会被 JVM 用于后续的链接(验证、准备、解析)和初始化阶段。
关键点: ClassLoader 主要影响类加载过程中的加载阶段。它不直接控制链接和初始化阶段的行为,类的可执行性最终由执行引擎 (Execution Engine) 决定。
历史与发展: 类加载器最初是为了 Java Applet 技术而设计的,但其灵活的设计(未绑定在 JVM 内部)使其在现代 Java 生态中扮演着至关重要的角色,如 OSGi 模块化框架、字节码加密/解密、热部署等领域都离不开类加载器。
# 1.2 类加载方式:显式 vs. 隐式
JVM 将类文件加载到内存的方式可以分为两种:
- 显式加载 (Explicit Loading): 在程序代码中直接调用 ClassLoader 的 API 来加载类。
Class.forName("com.example.MyClass")
:不仅加载类,还会执行类的初始化。myClassLoader.loadClass("com.example.MyClass")
:只加载类(执行加载、链接阶段),不保证立即初始化,初始化通常在该类首次主动使用时触发。
- 隐式加载 (Implicit Loading): 程序代码中没有直接调用 ClassLoader API,由 JVM 在运行时自动加载。
- 例如,当 JVM 加载类 A 时,如果类 A 引用了类 B(如创建类 B 的实例、调用类 B 的静态方法等),JVM 会自动通过相应的类加载器去加载类 B。
- 最常见的加载方式,如
new MyClass()
。
实际开发中,这两种方式通常混合使用。
// 文件名: LoadingTypes.java
class User { // 假设 User 类已存在
static { System.out.println("User 类初始化"); }
}
public class LoadingTypes {
public static void main(String[] args) {
System.out.println("--- 开始 ---");
// 1. 隐式加载 User 类 (new 关键字触发)
// 这里会加载 User.class 并进行初始化
System.out.println("准备隐式加载 User...");
User user = new User();
System.out.println("User 实例已创建");
System.out.println("--- 分隔符 ---");
try {
// 2. 显式加载 User 类 (Class.forName)
// 这里会加载 User.class 并进行初始化 (如果之前未初始化)
System.out.println("准备显式加载 User (forName)...");
Class<?> clazz1 = Class.forName("User");
System.out.println("User 类已加载 (forName)");
System.out.println("--- 分割线 ---");
// 3. 显式加载 User 类 (ClassLoader.loadClass)
// 这里只加载 User.class (执行加载、链接),通常不立即初始化
System.out.println("准备显式加载 User (loadClass)...");
ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz2 = sysClassLoader.loadClass("User");
System.out.println("User 类已加载 (loadClass)");
// 如果此时访问 User 的静态成员或创建实例,才会触发初始化
// 例如: User.someStaticMethod(); 或 new User();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("--- 结束 ---");
}
}
// 可能的输出 (假设 User 类首次被加载):
// --- 开始 ---
// 准备隐式加载 User...
// User 类初始化
// User 实例已创建
// --- 分隔符 ---
// 准备显式加载 User (forName)...
// User 类已加载 (forName) <-- 注意这里没有再次打印初始化信息,因为已被加载并初始化
// --- 分割线 ---
// 准备显式加载 User (loadClass)...
// User 类已加载 (loadClass) <-- 同样没有打印初始化信息
// --- 结束 ---
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 1.3 为何要理解类加载器?
尽管日常开发很少直接编写类加载器,但理解其机制非常重要:
- 问题排查: 快速定位和解决
ClassNotFoundException
或NoClassDefFoundError
等常见类加载相关异常。 - 高级特性: 实现类的动态加载(如插件化)、字节码加密解密等高级功能。
- 框架理解: 深入理解 Tomcat、Spring、OSGi 等框架如何利用自定义类加载器实现应用隔离、热部署等。
- 自定义扩展: 编写自定义类加载器以满足特定需求,如从非标准来源加载类。
# 1.4 类加载器的命名空间 (Namespace)
命名空间是理解类加载器隔离机制的关键概念。
- 类的唯一性: 在 JVM 中,一个类的唯一性是由加载它的类加载器和这个类本身的全限定名共同确定的。
- 命名空间定义: 每个类加载器实例都有一个独立的类命名空间。这个空间由该加载器以及它所有父加载器加载的类共同构成。
- 隔离性:
- 同一命名空间: 不允许出现全限定名相同的两个类。
- 不同命名空间: 可以出现全限定名相同的两个类。这意味着,即使两个类来源于同一个
.class
文件,如果由不同的类加载器实例加载,它们在 JVM 中也被视为两个完全不同的类。instanceof
检查会返回false
,类型转换会抛出ClassCastException
。
应用: 利用命名空间的隔离性,可以在大型应用或容器中加载同一个类的不同版本,实现模块间的依赖隔离。
代码示例:不同加载器加载同一类
// 文件名: MyClassLoader.java (一个简单的自定义类加载器)
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String rootDir; // 类文件根目录
public MyClassLoader(String rootDir) {
this.rootDir = rootDir;
}
// 重写 findClass 方法,定义加载逻辑
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 将类名转换为文件路径 (例如 com.example.MyClass -> rootDir/com/example/MyClass.class)
String filePath = rootDir + File.separator + name.replace('.', File.separatorChar) + ".class";
File classFile = new File(filePath);
if (!classFile.exists()) {
throw new ClassNotFoundException("类文件未找到: " + filePath);
}
ByteArrayOutputStream baos = null;
FileInputStream fis = null;
try {
fis = new FileInputStream(classFile);
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
// 读取字节码数据
while ((len = fis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
byte[] classData = baos.toByteArray();
// 调用 defineClass 将字节数组转换为 Class 对象
// 第一个参数 name 为 null 表示让 JVM 自动推断类名 (不推荐,最好明确指定)
// return defineClass(null, classData, 0, classData.length);
// 推荐明确指定类名
return defineClass(name, classData, 0, classData.length);
} catch (IOException e) {
throw new ClassNotFoundException("加载类出错: " + name, e);
} finally {
// 关闭流省略...
try { if (fis != null) fis.close(); } catch (IOException e) {}
try { if (baos != null) baos.close(); } catch (IOException e) {}
}
}
}
// 文件名: NamespaceTest.java
public class NamespaceTest {
public static void main(String[] args) throws Exception {
// 假设 Desktop/classes 目录下有 User.class 文件
String rootDir = System.getProperty("user.home") + File.separator + "Desktop" + File.separator + "classes";
String className = "com.example.chapter04.User"; // 假设 User 类在此包下
// 创建自定义类加载器 1
MyClassLoader loader1 = new MyClassLoader(rootDir);
Class<?> clazz1 = loader1.loadClass(className); // 使用 loadClass (内部会调用 findClass)
// 创建自定义类加载器 2
MyClassLoader loader2 = new MyClassLoader(rootDir);
Class<?> clazz2 = loader2.loadClass(className);
// 比较两个 Class 对象
System.out.println("clazz1 == clazz2: " + (clazz1 == clazz2)); // 输出: false (因为加载器不同)
// 打印各自的类加载器
System.out.println("clazz1 ClassLoader: " + clazz1.getClassLoader());
System.out.println("clazz2 ClassLoader: " + clazz2.getClassLoader());
// 使用系统类加载器加载同一个类
Class<?> clazz3 = ClassLoader.getSystemClassLoader().loadClass(className);
System.out.println("clazz3 ClassLoader: " + clazz3.getClassLoader());
// 比较 clazz1 和 clazz3
System.out.println("clazz1 == clazz3: " + (clazz1 == clazz3)); // 输出: false
// 打印自定义加载器的父加载器 (默认是系统/应用加载器)
System.out.println("loader1 Parent: " + loader1.getParent());
}
}
// 假设 User.java:
// package com.example.chapter04;
// public class User {}
// 需要先编译 User.java 得到 User.class, 并放到 Desktop/classes/com/example/chapter04/ 目录下
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
输出示例:
clazz1 == clazz2: false
clazz1 ClassLoader: MyClassLoader@1b6d3586
clazz2 ClassLoader: MyClassLoader@74a14482
clazz3 ClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
clazz1 == clazz3: false
loader1 Parent: sun.misc.Launcher$AppClassLoader@18b4aac2
2
3
4
5
6
(可见,即使是同一个 .class
文件,由不同类加载器实例加载后,得到的 Class
对象是不同的)
# 1.5 类加载机制基本特征总结
- 双亲委派模型 (Parent-Delegation Model): 类加载请求优先委托给父加载器处理,父加载器无法处理时才由子加载器自己加载。这是 Java 推荐的模型,但并非强制。
- 可见性 (Visibility): 子加载器可以访问(看到)父加载器加载的类,但父加载器不能访问子加载器加载的类。
- 单一性 (Uniqueness): 由于可见性,父加载器加载过的类,子加载器不会重复加载,保证了核心类库的唯一性。但处于“邻居”关系(非父子)的加载器之间可以加载同名类。
# 2. 类加载器分类
JVM 主要包含两类加载器:
- 引导类加载器 (Bootstrap ClassLoader): JVM 自身的一部分,通常由 C++ 实现。
- 自定义类加载器 (User-Defined ClassLoader): 所有继承自
java.lang.ClassLoader
的加载器,都属于自定义加载器。
在 Java 程序中最常见的是以下三层类加载器结构:
- 顶层: 引导类加载器 (Bootstrap ClassLoader)
- 中间层: 扩展类加载器 (Extension ClassLoader) (JDK 9 后更名为 平台类加载器 Platform ClassLoader)
- 底层: 应用程序类加载器 (Application ClassLoader / System ClassLoader)
- 更底层: 用户自定义类加载器 (User-Defined ClassLoader) (可选)
重要概念:
上图中的层级关系通常表示父子委派关系,而非继承关系。下层加载器持有上层加载器的引用(
parent
字段)。// 伪代码示意包含关系 class ClassLoader { ClassLoader parent; // 持有父加载器的引用 // ... } // AppClassLoader 实例会持有一个 ExtClassLoader 实例的引用作为 parent // ExtClassLoader 实例会持有一个代表 Bootstrap ClassLoader (通常为 null) 的引用作为 parent
1
2
3
4
5
6
7
# 2.1 引导类加载器 (Bootstrap ClassLoader)
- 实现: 通常由 C/C++ 实现,嵌入在 JVM 内部。
- 职责: 加载 Java 的核心类库,即
JAVA_HOME/jre/lib/rt.jar
、resources.jar
或sun.boot.class.path
指定路径下的内容。这些是 JVM 自身运行必需的类,如java.lang.Object
,java.lang.String
等。 - 父加载器: 没有父加载器。它是类加载器层级结构的顶端。
- 获取方式: 在 Java 代码中尝试获取它的引用通常返回
null
(因为它不是 Java 类)。 - 安全限制: 出于安全考虑,通常只加载包名为
java.*
,javax.*
,sun.*
等开头的类。 - 职责传递: 负责加载扩展类加载器和应用程序类加载器,并将它们设为自己的子加载器(逻辑上的)。
查看 Bootstrap 加载路径:
// 文件名: BootstrapPath.java
import java.net.URL;
public class BootstrapPath {
public static void main(String[] args) {
System.out.println("********** 启动类加载器 **************");
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
// 验证核心类库加载器 (如 String)
try {
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println("String 的加载器: " + classLoader); // 输出 null
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(输出会列出 rt.jar
等核心库路径,并显示 String
的加载器为 null
)
# 2.2 扩展类加载器 (Extension ClassLoader / Platform ClassLoader)
- 实现: 由 Java 语言编写,具体实现类是
sun.misc.Launcher$ExtClassLoader
(JDK 8) 或jdk.internal.loader.ClassLoaders$PlatformClassLoader
(JDK 9+)。 - 继承关系: 间接继承自
java.lang.ClassLoader
。 - 父加载器: 引导类加载器。
- 职责: 负责加载 扩展目录 下的类库。
- JDK 8:
JAVA_HOME/jre/lib/ext
目录,或由系统属性java.ext.dirs
指定的路径。 - JDK 9+: 模块化系统取代了
ext
目录机制。平台类加载器负责加载一些平台相关的模块。
- JDK 8:
- 开发者使用: 开发者可以将自己开发的、需要通用的 JAR 包放入扩展目录,由扩展类加载器加载。
查看扩展加载路径 (JDK 8):
// 文件名: ExtPath.java
public class ExtPath {
public static void main(String[] args) {
System.out.println("*********** 扩展类加载器 *************");
String extDirs = System.getProperty("java.ext.dirs");
if (extDirs != null) {
for (String path : extDirs.split(java.io.File.pathSeparator)) {
System.out.println(path);
}
} else {
System.out.println("java.ext.dirs is null (可能在 JDK 9+ 环境)");
}
// 验证扩展库加载器 (示例, 需确认该类在 ext 目录下)
try {
// 例如加载 JCE (Java Cryptography Extension) 中的类
ClassLoader classLoader1 = Class.forName("javax.crypto.Cipher").getClassLoader();
// 在某些 JDK 版本或配置下,JCE 可能由 Bootstrap 加载器加载,这里需具体情况分析
// 更可靠的是找一个确定在 ext 目录下的库,如 sunec.jar 中的类
// ClassLoader classLoader1 = Class.forName("sun.security.ec.CurveDB").getClassLoader();
System.out.println("示例扩展类的加载器: " + classLoader1); // 应为 ExtClassLoader
} catch (ClassNotFoundException e) {
System.out.println("示例类未找到或由其他加载器加载");
// e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(JDK 9+ 中 java.ext.dirs
通常为 null)
# 2.3 应用程序类加载器 (Application ClassLoader / System ClassLoader)
- 实现: 由 Java 语言编写,具体实现类是
sun.misc.Launcher$AppClassLoader
(JDK 8) 或jdk.internal.loader.ClassLoaders$AppClassLoader
(JDK 9+)。 - 继承关系: 间接继承自
java.lang.ClassLoader
。 - 父加载器: 扩展类加载器 (或平台类加载器)。
- 职责: 负责加载应用程序 classpath(环境变量
CLASSPATH
或-cp
/-classpath
命令行参数,以及java.class.path
系统属性)下的类库。 - 默认加载器: 是程序中默认的类加载器。我们自己编写的 Java 类,在没有指定自定义加载器的情况下,通常都是由它加载的。
- 获取方式: 可以通过
ClassLoader.getSystemClassLoader()
方法获取。 - 自定义加载器的父: 通常作为用户自定义类加载器的默认父加载器。
# 2.4 用户自定义类加载器 (User-Defined ClassLoader)
- 实现: 开发者通过继承
java.lang.ClassLoader
类(或其子类如URLClassLoader
)并重写特定方法(通常是findClass
)来实现。 - 目的:
- 实现类的隔离加载 (如 Tomcat 为每个 Web 应用创建加载器)。
- 从非标准来源加载类 (如网络、数据库、加密文件)。
- 在加载时动态修改字节码。
- 实现热部署或热替换。
- 父加载器: 默认是应用程序类加载器,但可以在构造时指定其他加载器作为父加载器。
# 3. 测试不同的类加载器
可以通过 Class
对象的 getClassLoader()
方法获取加载该类的类加载器。
注意事项:
getClassLoader()
返回null
表示该类是由引导类加载器加载的。- 数组类的加载器与其元素类型的加载器相同。如果元素是基本类型,则数组类的加载器为
null
。
// 文件名: ClassLoaderHierarchyTest.java
public class ClassLoaderHierarchyTest {
public static void main(String[] args) {
try {
// 1. 获取当前类的加载器 (AppClassLoader)
ClassLoader appClassLoader = ClassLoaderHierarchyTest.class.getClassLoader();
System.out.println("当前类加载器: " + appClassLoader);
// 2. 获取 AppClassLoader 的父加载器 (ExtClassLoader/PlatformClassLoader)
ClassLoader extPlatformClassLoader = appClassLoader.getParent();
System.out.println("父加载器 (Ext/Platform): " + extPlatformClassLoader);
// 3. 获取 Ext/Platform ClassLoader 的父加载器 (Bootstrap, 返回 null)
ClassLoader bootstrapClassLoader = extPlatformClassLoader.getParent();
System.out.println("祖父加载器 (Bootstrap): " + bootstrapClassLoader); // 输出 null
System.out.println("--- 分割线 ---");
// 4. 获取核心类库 String 的加载器 (Bootstrap, 返回 null)
ClassLoader stringClassLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println("String 的加载器: " + stringClassLoader); // 输出 null
// 5. 获取系统类加载器 (AppClassLoader)
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器: " + systemClassLoader);
System.out.println("--- 数组加载器 ---");
// 6. 引用类型数组的加载器 (同元素类型加载器, String 是 Bootstrap -> null)
String[] strArray = new String[10];
System.out.println("String[] 加载器: " + strArray.getClass().getClassLoader()); // 输出 null
// 7. 自定义类型数组的加载器 (同元素类型加载器, ClassLoaderHierarchyTest 是 App -> AppCL)
ClassLoaderHierarchyTest[] testArray = new ClassLoaderHierarchyTest[10];
System.out.println("ClassLoaderHierarchyTest[] 加载器: " + testArray.getClass().getClassLoader());
// 8. 基本类型数组的加载器 (无加载器 -> null)
int[] intArray = new int[10];
System.out.println("int[] 加载器: " + intArray.getClass().getClassLoader()); // 输出 null
System.out.println("--- 线程上下文加载器 ---");
// 9. 获取当前线程的上下文类加载器 (通常是 AppClassLoader)
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println("线程上下文加载器: " + contextClassLoader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
运行结果 (JDK 8 示例):
当前类加载器: sun.misc.Launcher$AppClassLoader@18b4aac2
父加载器 (Ext/Platform): sun.misc.Launcher$ExtClassLoader@1b6d3586
祖父加载器 (Bootstrap): null
--- 分割线 ---
String 的加载器: null
系统类加载器: sun.misc.Launcher$AppClassLoader@18b4aac2
--- 数组加载器 ---
String[] 加载器: null
ClassLoaderHierarchyTest[] 加载器: sun.misc.Launcher$AppClassLoader@18b4aac2
int[] 加载器: null
--- 线程上下文加载器 ---
线程上下文加载器: sun.misc.Launcher$AppClassLoader@18b4aac2
2
3
4
5
6
7
8
9
10
11
12
# 4. ClassLoader 源码解析
理解 java.lang.ClassLoader
类的源码有助于深入掌握类加载机制,特别是双亲委派模型。
# 4.1 ClassLoader
的主要方法
java.lang.ClassLoader
是一个抽象类,定义了类加载器的基本框架。
public final ClassLoader getParent()
: 返回当前类加载器的父加载器。如果父加载器是引导类加载器,则返回null
。public Class<?> loadClass(String name) throws ClassNotFoundException
: 加载类的入口方法。它实现了双亲委派模型的核心逻辑。通常不建议子类重写此方法。默认实现会先检查缓存,然后委派给父加载器,最后调用findClass()
。protected Class<?> findClass(String name) throws ClassNotFoundException
: 根据类的二进制名称查找类。这是推荐子类重写的方法,用于实现自定义的类加载逻辑(如从特定路径或网络加载字节码)。ClassLoader
的默认实现是直接抛出ClassNotFoundException
。protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
: 将一个字节数组 (b
) 转换为java.lang.Class
类的实例。这是将原始字节码数据转化为 JVM 内部表示的关键方法。通常在findClass()
方法中获取到字节码后调用此方法。此方法是final
的,子类不能重写,由 JVM 底层实现。protected final void resolveClass(Class<?> c)
: 链接指定的 Java 类。调用此方法会触发类的链接阶段(验证、准备、解析)。loadClass(String name, boolean resolve)
方法的第二个参数resolve
为true
时会调用此方法。protected final Class<?> findLoadedClass(String name)
: 检查当前类加载器是否已经加载过指定名称的类(检查缓存)。loadClass
方法首先调用此方法。此方法是final
的。
# 4.2 loadClass(String name, boolean resolve)
源码分析 (双亲委派实现)
这是理解双亲委派模型的关键。其简化逻辑如下 (基于 JDK 8 源码注释):
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 同步锁,确保线程安全
synchronized (getClassLoadingLock(name)) {
// 1. 检查当前加载器的缓存,看是否已经加载过此类?
Class<?> c = findLoadedClass(name);
if (c == null) { // 如果缓存中没有
long t0 = System.nanoTime(); // 记录时间 (性能统计)
try {
// 2. 获取父加载器
ClassLoader parent = this.getParent();
if (parent != null) {
// 3. 如果父加载器存在,则委派给父加载器加载
// 递归调用父加载器的 loadClass
// 注意这里的 resolve 参数传 false,表示父加载器加载时通常不立即解析
c = parent.loadClass(name, false);
} else {
// 4. 如果父加载器不存在 (即当前加载器的父是 Bootstrap ClassLoader)
// 则委派给 Bootstrap ClassLoader 加载
c = findBootstrapClassOrNull(name); // 这是一个 native 或内部方法
}
} catch (ClassNotFoundException e) {
// 如果父加载器或 Bootstrap 加载器抛出 CNFE,说明它们无法加载
// 忽略这个异常,继续由子加载器自己尝试加载
}
if (c == null) { // 如果父加载器和 Bootstrap 都没能加载成功
// 5. 调用当前加载器自己的 findClass 方法来查找和加载类
// 这一步是留给子类去实现自定义加载逻辑的地方
long t1 = System.nanoTime(); // 记录时间
c = findClass(name); // 调用子类重写的 findClass
// 记录性能统计数据...
}
}
// 6. 如果调用者要求解析 (resolve 为 true)
if (resolve) {
// 执行链接阶段的解析操作
resolveClass(c);
}
// 7. 返回加载的 Class 对象
return c;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
总结 loadClass
的双亲委派流程:
- 查缓存: 检查自己是否已加载。
- 委派父: 如果有父加载器,调用
parent.loadClass()
。 - 委派启动: 如果父加载器是
null
,调用启动类加载器加载。 - 自己找: 如果以上都失败,调用自己的
findClass()
方法。
# 4.3 SecureClassLoader
与 URLClassLoader
SecureClassLoader
: 继承自ClassLoader
,增加了与代码源 (CodeSource
) 和权限 (PermissionCollection
) 相关的功能,用于增强安全性。一般不直接使用。URLClassLoader
: 继承自SecureClassLoader
。它实现了findClass
和findResource
等方法,可以从本地文件系统或远程 URL 指定的JAR
文件和目录中加载类和资源。- 方便自定义: 如果自定义类加载器只需要从特定目录或 JAR 文件加载,可以直接继承
URLClassLoader
,只需在构造函数中传入相应的 URL 即可,无需重写findClass
方法。系统类加载器和扩展类加载器(JDK 8)都是URLClassLoader
的实例(或子类实例)。
# 4.4 ExtClassLoader
与 AppClassLoader
的实现
- 它们都是
sun.misc.Launcher
的内部类。 - JDK 8 中,它们都继承自
URLClassLoader
。 - 它们自身没有重写
loadClass
方法(或重写了但内部仍调用super.loadClass
),因此严格遵守双亲委派模型。 - 它们主要是在构造时,通过
URLClassLoader
的能力,设置了各自负责加载的路径(ext
目录或classpath
)。
# 4.5 Class.forName()
与 ClassLoader.loadClass()
的区别
特性 | Class.forName(className) | ClassLoader.loadClass(className) |
---|---|---|
调用方式 | 静态方法 (Class. ) | 实例方法 (classLoader. ) |
类加载器 | 使用调用者的类加载器及上下文加载器 | 使用指定的 ClassLoader 实例进行加载 |
初始化 | 会执行类的初始化阶段 (<clinit> ) | 不保证立即执行初始化,仅加载和链接 |
主要用途 | 获取 Class 对象并确保其已初始化(如 JDBC 驱动加载) | 仅加载类定义,延迟初始化,或使用特定加载器加载 |
# 5. 双亲委派模型 (Parent-Delegation Model)
双亲委派模型是 Java 设计者推荐的类加载器工作机制,自 JDK 1.2 起引入,用于组织和协调不同类加载器的工作。
# 5.1 定义与本质
- 定义: 当一个类加载器收到加载类的请求时,它首先不会自己尝试加载,而是将请求向上委托给它的父加载器去完成。每一层的加载器都如此,因此所有加载请求最终都会传送到顶层的启动类加载器。只有当父加载器反馈自己无法完成加载请求(在它的搜索范围内找不到所需的类)时,子加载器才会自己尝试去加载。
- 本质: 规定了类加载的优先级和顺序:启动类加载器 -> 扩展类加载器 -> 应用程序类加载器 -> 自定义类加载器。只有上层加载器找不到时,下层加载器才有机会加载。
# 5.2 优势
- 避免类的重复加载: 当父加载器已经加载了某个类时,子加载器就不会再次加载。这保证了在 JVM 中,同一个全限定名的类只存在一个
Class
对象,确保了类的全局唯一性。 - 保护程序安全,防止核心 API 被篡改: Java 核心类库(如
java.lang.Object
,java.lang.String
)总是由启动类加载器加载。双亲委派模型确保了无论哪个加载器收到加载核心库类的请求,最终都会委托给启动类加载器。这可以防止用户编写与核心库同名的恶意类来覆盖或替换系统类,保障了 JVM 的安全运行。例如,用户无法自己写一个java.lang.String
类并让系统使用它。
# 5.3 实现
双亲委派模型的核心逻辑实现在 java.lang.ClassLoader
的 loadClass(String name, boolean resolve)
方法中,具体分析见 4.2 节。
# 5.4 弊端
双亲委派模型的主要弊端在于其单向的委派结构:子加载器可以访问父加载器加载的类,但父加载器无法访问子加载器加载的类。
这在某些场景下会产生问题:基础类(通常由上层加载器加载)需要调用用户实现的代码(通常由下层加载器加载)。
- 例子: SPI (Service Provider Interface) 机制。
- Java 核心库(如 JNDI, JDBC, JCE 等,由启动类加载器加载)定义了服务接口 (SPI)。
- 具体的服务实现由第三方厂商提供,放在应用程序的 classpath 下(由应用程序类加载器加载)。
- 核心库中的工厂类(如
DriverManager.getConnection()
)需要找到并加载 classpath 下的服务实现类来创建实例。 - 按照双亲委派,启动类加载器无法看到应用程序类加载器加载的实现类,导致工厂方法无法工作。
# 5.5 如何解决弊端:线程上下文类加载器
为了解决上述弊端,Java 引入了线程上下文类加载器 (Thread Context ClassLoader, TCCL)。
- 概念: 每个线程都有一个关联的上下文类加载器。可以通过
Thread.currentThread().getContextClassLoader()
获取,通过Thread.currentThread().setContextClassLoader()
设置。 - 默认值: 如果没有手动设置,线程会从其父线程继承上下文类加载器。应用程序主线程的默认上下文类加载器通常是应用程序类加载器。
- 工作方式:
- 基础类(如 JDBC API)在需要加载 SPI 实现时,不再依赖启动类加载器。
- 它获取当前线程的上下文类加载器 (通常是 AppClassLoader)。
- 使用这个上下文类加载器去加载并实例化 SPI 的实现类。
- 效果: 这相当于一种“逆向”的类加载请求,允许了父加载器(逻辑上的,因为代码在父加载器加载的类中执行)请求子加载器(线程上下文加载器)来加载类,从而打破了双亲委派模型的严格限制。
- 应用: JNDI, JDBC, JCE, JAXB, JBI 以及
java.util.ServiceLoader
等都使用了线程上下文类加载器来加载服务提供者。
# 5.6 破坏双亲委派模型的场景总结
双亲委派模型是推荐而非强制,历史上出现过几次“被破坏”的情况:
- JDK 1.2 前的兼容性: 为了兼容旧版本用户自定义
ClassLoader
重写loadClass()
的代码,JDK 1.2 引入findClass()
并建议用户重写后者。这使得旧代码依然可以不遵循双亲委派。 - 模型自身缺陷 (SPI): 如上所述,基础类需要调用用户代码,通过线程上下文类加载器绕开了双亲委派。
- 程序动态性需求 (OSGi, 热部署): 为了实现模块化、热部署、热替换等功能,需要更灵活的类加载机制。
- OSGi (Open Services Gateway initiative): 采用复杂的网状类加载器结构。每个模块 (Bundle) 有自己的加载器。加载类时,除了向上委派,还会在模块依赖、导入/导出包等之间进行查找,打破了严格的层级结构。
- 热部署/热替换: 基本思路是每次重新部署时,废弃旧的类加载器实例,创建一个新的类加载器实例来加载新版本的类文件。由于不同加载器实例加载的同名类被视为不同类型,从而实现了类的替换。
注意: "破坏"不一定是贬义词,它通常是为了解决特定问题或实现更高级功能而进行的必要调整和创新。
# 5.7 热替换的简单模拟
利用类加载器的命名空间隔离特性可以模拟热替换:
- 编写一个自定义类加载器,用于加载需要热替换的类 (
Demo1.class
)。 - 在一个循环中:
- 创建新的自定义类加载器实例。
- 使用新加载器加载
Demo1
类,获取Class
对象。 - 通过反射创建
Demo1
的实例并调用其方法。 Thread.sleep()
一段时间。
- 当需要更新时,替换
Demo1.class
文件。 - 下一次循环时,新的类加载器会加载新版本的
Demo1.class
文件,从而执行更新后的代码。旧的类加载器及其加载的旧版本类会在没有引用后被 GC 回收(理论上)。
示例代码 (LoopRun 和 Demo1):
// 文件名: Demo1.java (放在 D:/hotspot/ 目录下)
// package some.package; // 假设没有包或根据实际情况调整
public class Demo1 {
public void hot() {
// 初始版本
// System.out.println("Old Demo1 - Version 1");
// 修改后重新编译的版本
System.out.println("New Demo1 - Version 2 HOT!");
}
}
// --- 需要先编译 Demo1.java ---
// javac -d D:/hotspot Demo1.java
// 文件名: LoopRun.java
import java.lang.reflect.Method;
public class LoopRun {
public static void main(String args[]) {
// 无限循环,模拟持续运行的服务
while (true) {
try {
// 1. 每次循环都创建 *新* 的自定义类加载器实例
// 加载路径指向 Demo1.class 所在的目录
MyClassLoader loader = new MyClassLoader("D:/hotspot"); // 使用前面定义的 MyClassLoader
// 2. 使用 *新* 加载器加载指定类
// 注意类名需要包含包名 (如果 Demo1 有包)
// Class<?> clazz = loader.loadClass("some.package.Demo1");
Class<?> clazz = loader.loadClass("Demo1"); // 假设无包
// 3. 通过反射创建类的实例
Object demo = clazz.newInstance();
// 4. 获取并调用 hot 方法
Method m = clazz.getMethod("hot");
m.invoke(demo);
// 5. 等待一段时间
Thread.sleep(5000); // 每 5 秒重新加载一次
} catch (Exception e) {
System.out.println("加载或执行出错: " + e);
try {
Thread.sleep(5000);
} catch (InterruptedException ex) { }
}
}
}
}
// MyClassLoader 需要和 LoopRun 在同一个项目中,或者能被 LoopRun 的加载器找到
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
(运行 LoopRun
,然后修改 Demo1.java
中的打印内容,重新编译 Demo1.java
(确保 .class
文件被更新到 D:/hotspot/
),观察 LoopRun
输出的变化。)
# 6. 沙箱安全机制 (Sandbox)
Java 安全模型的核心是沙箱机制,旨在限制不可信代码的执行权限,保护本地系统资源。
- 沙箱: 一个受限制的程序运行环境。
- 机制: 将 Java 代码(尤其是来自网络的不可信代码)限制在 JVM 特定的运行范围内,严格控制其对本地系统资源(CPU、内存、文件系统、网络等)的访问。
- 目的: 提供代码隔离,防止恶意代码破坏本地系统。
# 沙箱机制的演进
- JDK 1.0: 简单的沙箱模型。本地代码完全信任,可访问所有资源。远程代码(如 Applet)完全不信任,运行在严格受限的沙箱内,几乎无法访问本地资源。
- JDK 1.1: 引入安全策略 (Security Policy) 和代码签名 (Code Signing)。允许用户对代码(根据来源或签名)授予特定的本地资源访问权限。Applet 可以请求权限。
- JDK 1.2: 引入保护域 (Protection Domain) 的概念。代码根据其来源 (
CodeSource
- URL 和证书) 和用户配置的安全策略文件 (java.policy
) 被加载到不同的保护域中。每个保护域关联一组权限 (PermissionCollection
)。代码只能执行其所在保护域拥有的权限允许的操作。类加载器在加载类时确定其所属的保护域。 - JDK 1.6 及之后: 进一步完善,引入栈检查机制 (Stack Inspection)。当代码尝试访问受限资源时(如
checkPermission
调用),AccessController
会检查当前调用栈上所有方法的保护域是否都拥有所需的权限。只有当调用链上的所有代码都具备该权限时,访问才被允许。这提供了更细粒度的权限控制。
双亲委派模型也是沙箱安全的重要一环,它防止用户代码替换核心类库,保证了基础 API 的安全性和一致性。
# 7. 自定义类加载器
# 7.1 为何需要自定义?
- 隔离加载类: 实现模块化,防止不同模块间的类库版本冲突(如 Tomcat, OSGi)。
- 修改类加载方式: 不遵循标准双亲委派,实现按需加载、延迟加载等。
- 扩展加载源: 从网络、数据库、加密文件等非标准位置加载类。
- 防止源码泄露: 加载前对加密的字节码进行解密。
# 7.2 实现方式
- 继承: 创建一个新类,继承自
java.lang.ClassLoader
(或更具体的URLClassLoader
)。 - 重写
findClass(String name)
(推荐):- 在此方法中实现查找和获取类的字节码数据的逻辑(例如,从文件、网络读取,解密等)。
- 获取到字节码数据(
byte[] classData
)后,调用defineClass(name, classData, 0, classData.length)
将字节数组转换为Class
对象并返回。 - 保留
loadClass
的默认实现可以继续利用双亲委派模型。
- 重写
loadClass(String name)
(不推荐):- 如果需要完全改变类加载逻辑(不使用双亲委派),可以重写此方法。
- 需要自己处理缓存检查、父类委派(如果需要)以及调用
findClass
或defineClass
的逻辑。 - 容易破坏双亲委派模型,引入复杂性。
父加载器: 自定义类加载器的父加载器默认是系统类加载器 (AppClassLoader)。可以通过调用 super(parentClassLoader)
构造方法来指定不同的父加载器。
# 7.3 类型转换问题
使用自定义类加载器时需要注意:如果同一个类被不同的类加载器实例加载,即使它们来自同一个 .class
文件,JVM 也会视它们为不同的类型。尝试在它们之间进行类型转换会抛出 ClassCastException
。
# 7.4 代码示例 (自定义加载器实现)
// 文件名: MyFileSystemClassLoader.java
import java.io.*;
public class MyFileSystemClassLoader extends ClassLoader {
private String rootDir; // 类文件根目录
public MyFileSystemClassLoader(String rootDir) {
// 默认父加载器是 AppClassLoader
this.rootDir = rootDir;
}
public MyFileSystemClassLoader(ClassLoader parent, String rootDir) {
// 可以指定父加载器
super(parent);
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// System.out.println("MyFileSystemClassLoader: finding class " + name); // 调试信息
byte[] classData = getClassData(name); // 获取字节码数据
if (classData == null) {
throw new ClassNotFoundException("未能在指定目录下找到类: " + name);
} else {
// 调用 defineClass 将字节码转换为 Class 对象
return defineClass(name, classData, 0, classData.length);
}
}
// 从文件系统读取字节码数据
private byte[] getClassData(String className) {
String path = rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try (InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = ins.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
// 文件未找到, findClass 应该抛出 ClassNotFoundException
// System.err.println("类文件未找到: " + path);
return null; // 或者直接在这里抛出 CNFE
} catch (IOException e) {
System.err.println("读取类文件时出错: " + path);
e.printStackTrace();
return null;
}
}
}
// 文件名: MyClassLoaderTest.java
public class MyClassLoaderTest {
public static void main(String[] args) {
// 指定 Demo1.class 所在的目录,例如 D:/myclasses
// 需要先手动编译一个 Demo1.java 并将 Demo1.class 放到 D:/myclasses/ 目录下
String loadPath = "D:/myclasses";
String classNameToLoad = "Demo1"; // 假设 Demo1 没有包名
MyFileSystemClassLoader loader = new MyFileSystemClassLoader(loadPath);
try {
// 使用自定义加载器的 loadClass 方法加载类
// loadClass 会先尝试委派给父加载器 (AppClassLoader)
// 如果 AppClassLoader 在 classpath 找不到 Demo1, 则会调用 loader 的 findClass 方法
System.out.println("尝试加载: " + classNameToLoad);
Class<?> clazz = loader.loadClass(classNameToLoad);
System.out.println("类加载成功!");
// 打印加载器信息
System.out.println("加载 " + clazz.getName() + " 的类加载器为: " + clazz.getClassLoader().getClass().getName());
System.out.println("其父加载器为: " + clazz.getClassLoader().getParent().getClass().getName());
// 可以尝试创建实例或调用方法 (如果 Demo1 有的话)
// Object instance = clazz.newInstance();
} catch (ClassNotFoundException e) {
System.err.println("类未找到: " + classNameToLoad);
e.printStackTrace();
} catch (Exception e) { // 捕获其他可能的反射异常
e.printStackTrace();
}
}
}
// 示例 Demo1.java (放在 D:/myclasses/ 编译后)
// public class Demo1 {
// static { System.out.println("Demo1 类初始化"); }
// public Demo1() { System.out.println("Demo1 实例创建"); }
// }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
运行结果示例 (假设 AppClassLoader 找不到 Demo1):
尝试加载: Demo1
类加载成功!
加载 Demo1 的类加载器为: MyFileSystemClassLoader
其父加载器为: sun.misc.Launcher$AppClassLoader
2
3
4
# 8. Java 9 类加载器新特性
JDK 9 引入了 模块化系统 (JPMS),对类加载器架构和行为产生了一些重要影响,但核心的双亲委派模型得以保留。
- 扩展机制移除:
lib/ext
目录和java.ext.dirs
属性被废弃。模块化系统提供了更规范的扩展方式。 - 扩展类加载器更名: Extension ClassLoader 被重命名为 平台类加载器 (Platform Class Loader)。可以通过
ClassLoader.getPlatformClassLoader()
获取。 - 类加载器继承关系改变:
- Platform ClassLoader 和 AppClassLoader 不再继承自
java.net.URLClassLoader
。 - 它们现在都继承自一个新的内部类
jdk.internal.loader.BuiltinClassLoader
。 - 如果代码直接依赖于
URLClassLoader
的特定方法或继承关系,可能在 JDK 9+ 中失效。
- Platform ClassLoader 和 AppClassLoader 不再继承自
- 类加载器命名: 类加载器实例可以有名称(在构造时指定),通过
getName()
获取。Platform ClassLoader 名称为 "platform",AppClassLoader 名称为 "app"。这有助于调试。 - 启动类加载器实现: 仍然是 JVM 内部实现(C++ 和 Java 结合),但获取方式 (
getParent()
返回null
) 保持不变以兼容旧代码。 - 委派关系微调:
- Platform 和 App ClassLoader 在向上委派前,会先检查请求加载的类是否属于某个系统模块。
- 如果类属于某个系统模块,请求会优先委派给负责该模块的加载器(可能是 Platform 或 App 自身,或更上层的加载器)。这确保了模块内类的加载一致性。
- 如果类不属于系统模块,则继续沿用传统的双亲委派向上查找。
代码示例 (JDK 9+):
// 文件名: Java9ClassLoaderTest.java
public class Java9ClassLoaderTest {
public static void main(String[] args) {
// AppClassLoader
ClassLoader appClassLoader = Java9ClassLoaderTest.class.getClassLoader();
System.out.println("App ClassLoader: " + appClassLoader);
System.out.println("App ClassLoader Name: " + appClassLoader.getName()); // 输出 app
// PlatformClassLoader
ClassLoader platformClassLoader = appClassLoader.getParent();
System.out.println("Platform ClassLoader: " + platformClassLoader);
System.out.println("Platform ClassLoader Name: " + platformClassLoader.getName()); // 输出 platform
// Bootstrap ClassLoader (null)
ClassLoader bootstrapClassLoader = platformClassLoader.getParent();
System.out.println("Bootstrap ClassLoader: " + bootstrapClassLoader); // 输出 null
// 通过 API 获取
System.out.println("System ClassLoader: " + ClassLoader.getSystemClassLoader());
System.out.println("Platform ClassLoader (API): " + ClassLoader.getPlatformClassLoader());
// 查看核心类加载器
System.out.println("String ClassLoader: " + String.class.getClassLoader()); // 输出 null (Bootstrap)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
输出示例 (JDK 9+):
App ClassLoader: jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
App ClassLoader Name: app
Platform ClassLoader: jdk.internal.loader.ClassLoaders$PlatformClassLoader@43a25848
Platform ClassLoader Name: platform
Bootstrap ClassLoader: null
System ClassLoader: jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
Platform ClassLoader (API): jdk.internal.loader.ClassLoaders$PlatformClassLoader@43a25848
String ClassLoader: null
2
3
4
5
6
7
8
JDK 9+ 双亲委派示意图:
(注意模块检查优先于向上委派)
各加载器负责的模块示例 (非详尽列表):
- 启动类加载器:
java.base
,java.logging
,jdk.internal.vm.ci
等核心基础模块。 - 平台类加载器:
java.sql
,java.xml
,jdk.security.auth
等标准库但非最核心的模块。 - 应用程序类加载器:
jdk.compiler
,jdk.jlink
等开发工具相关模块,以及 classpath 下的应用程序模块(未命名模块)。