Java 类加载器详解

什么是类加载器

类加载器(ClassLoader)是 Java 虚拟机(JVM)的重要组成部分,它负责将 Java 类的字节码加载到内存中,并转换为 JVM 可以识别的 Class 对象。

类加载器的作用

  1. 加载类:将类的字节码从磁盘、网络或其他来源加载到内存中
  2. 链接类:验证字节码的有效性,准备类的静态变量,解析符号引用
  3. 初始化类:执行静态初始化块,初始化静态变量
  4. 命名空间管理:每个类加载器都有自己的命名空间,相同名称的类在不同的类加载器中是不同的

类加载的过程

类加载的过程包括以下几个步骤:

  1. 加载(Loading):通过类的全限定名找到类的字节码,将其加载到内存中
  2. 链接(Linking)
    • 验证(Verification):验证字节码的有效性
    • 准备(Preparation):为静态变量分配内存并设置默认值
    • 解析(Resolution):将符号引用转换为直接引用
  3. 初始化(Initialization):执行静态初始化块,初始化静态变量

Java 内置的类加载器

Java 中有三种类加载器:

  1. 引导类加载器(Bootstrap ClassLoader)

    • 负责加载 Java 核心类库(如 rt.jar)
    • 由 C++ 实现,不是 Java 类
    • 没有父类加载器
  2. 扩展类加载器(Extension ClassLoader)

    • 负责加载 Java 扩展类库(如 lib/ext 目录下的 jar 包)
    • 父类加载器是引导类加载器
  3. 应用类加载器(Application ClassLoader)

    • 负责加载应用程序的类(如 classpath 下的类)
    • 父类加载器是扩展类加载器

类加载器的工作原理

类加载的伪代码

1
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
// 类加载的核心逻辑
Class<?> loadClass(String name) {
// 1. 检查是否已经加载过该类
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 调用父类加载器加载
if (parent != null) {
c = parent.loadClass(name);
} else {
// 3. 如果没有父类加载器,使用引导类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法加载
}

// 4. 如果父类加载器无法加载,自己加载
if (c == null) {
c = findClass(name);
}
}
return c;
}

// 查找并加载类的字节码
Class<?> findClass(String name) {
// 1. 找到类的字节码
byte[] b = loadClassData(name);
// 2. 将字节码转换为 Class 对象
return defineClass(name, b, 0, b.length);
}

类加载的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ClassLoaderDemo {
public static void main(String[] args) {
// 获取类加载器
ClassLoader classLoader = ClassLoaderDemo.class.getClassLoader();
System.out.println("应用类加载器:" + classLoader);

// 获取父类加载器
ClassLoader parentClassLoader = classLoader.getParent();
System.out.println("扩展类加载器:" + parentClassLoader);

// 获取父父类加载器(引导类加载器)
ClassLoader grandParentClassLoader = parentClassLoader.getParent();
System.out.println("引导类加载器:" + grandParentClassLoader); // 输出 null,因为引导类加载器不是 Java 类

// 获取核心类的类加载器
ClassLoader stringClassLoader = String.class.getClassLoader();
System.out.println("String 类的加载器:" + stringClassLoader); // 输出 null,因为由引导类加载器加载
}
}

双亲委派模型

什么是双亲委派模型

双亲委派模型是 Java 类加载器的一种工作机制,它要求每个类加载器在加载类之前,首先将加载请求委托给父类加载器,只有当父类加载器无法加载时,才由自己尝试加载。

双亲委派模型的工作流程

  1. 当一个类加载器收到加载类的请求时,它首先不会自己尝试加载,而是将请求委托给父类加载器
  2. 父类加载器也会同样将请求委托给它的父类加载器,直到到达顶层的引导类加载器
  3. 如果父类加载器能够加载该类,则返回加载的结果
  4. 如果父类加载器无法加载,则由当前类加载器尝试加载

双亲委派模型的代码实现

1
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
// 双亲委派模型的核心实现
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. 委托给父类加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 3. 如果没有父类,使用引导类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法加载
}

if (c == null) {
// 4. 父类加载器无法加载,自己加载
long t1 = System.nanoTime();
c = findClass(name);
// 记录统计信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

双亲委派模型的作用

  1. 避免类的重复加载:当父类加载器已经加载了该类时,子类加载器就不需要再加载一次
  2. 保护 Java 核心类库:防止核心类库被篡改,例如自定义一个 java.lang.String 类是无法被加载的
  3. 确保类的层次结构:保证了 Java 核心类的一致性,不同的类加载器加载的类之间的关系是确定的

为什么需要双亲委派模型

  1. 安全性:防止恶意代码替换 Java 核心类库的类
  2. 一致性:确保相同的类在不同的类加载器中是相同的
  3. 效率:避免类的重复加载,提高加载效率

自定义类加载器

如何自定义类加载器

自定义类加载器需要继承 ClassLoader 类,并重写 findClass 方法:

1
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
public class CustomClassLoader extends ClassLoader {
private String classPath;

public CustomClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 1. 读取类的字节码
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
// 2. 将字节码转换为 Class 对象
return defineClass(name, classData, 0, classData.length);
} catch (Exception e) {
throw new ClassNotFoundException(e.getMessage());
}
}

private byte[] loadClassData(String className) throws Exception {
// 将类名转换为路径
String path = classPath + File.separatorChar +
className.replace('.', File.separatorChar) + ".class";
// 读取字节码
try (InputStream is = new FileInputStream(path);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
}
}
}

自定义类加载器的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CustomClassLoaderTest {
public static void main(String[] args) throws Exception {
// 创建自定义类加载器
CustomClassLoader classLoader = new CustomClassLoader("d:/classes");
// 加载类
Class<?> clazz = classLoader.loadClass("com.example.TestClass");
// 创建实例
Object obj = clazz.newInstance();
// 调用方法
Method method = clazz.getMethod("sayHello");
method.invoke(obj);
}
}

自定义类加载器的作用

  1. 加载非标准位置的类:从网络、数据库、加密文件等非标准位置加载类
  2. 实现类的热部署:在不重启应用的情况下替换类的实现
  3. 实现类的加密和解密:对类的字节码进行加密,防止反编译
  4. 实现类的隔离:不同的类加载器加载的类是隔离的,可以在同一个 JVM 中运行不同版本的类

自定义类加载器的使用场景

  1. 应用服务器:如 Tomcat,需要隔离不同的 Web 应用
  2. OSGi 框架:需要实现模块的热部署和隔离
  3. 插件系统:需要动态加载插件
  4. 加密应用:需要对类进行加密保护
  5. 远程调用:需要从网络加载类

打破双亲委派模型

在某些情况下,需要打破双亲委派模型,例如:

  1. Tomcat:需要加载 Web 应用的类,而不是委托给父类加载器
  2. OSGi:需要实现模块的热部署和隔离

打破双亲委派模型的方法是重写 loadClass 方法,不按照双亲委派的顺序加载类。

打破双亲委派模型的示例

1
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
public class BreakClassLoader extends ClassLoader {
private String classPath;

public BreakClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 检查是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 直接自己加载,不委托给父类
c = findClass(name);
} catch (ClassNotFoundException e) {
// 3. 如果自己无法加载,再委托给父类
return super.loadClass(name, resolve);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 读取类的字节码并转换为 Class 对象
// 实现与自定义类加载器相同
// ...
}
}

类加载器的命名空间

每个类加载器都有自己的命名空间,命名空间由该类加载器及其所有父类加载器加载的类组成。

两个类相等的条件:

  1. 类的全限定名相同
  2. 由同一个类加载器加载

类加载器的引用

类加载器会引用它加载的类,类也会引用它的类加载器:

1
2
3
4
5
// 类引用类加载器
ClassLoader classLoader = TestClass.class.getClassLoader();

// 类加载器引用类(通过缓存)
// ClassLoader 内部有一个缓存,保存了已加载的类

类加载器的垃圾回收

当类加载器不再被引用时,它加载的类也可能被垃圾回收(如果这些类也不再被引用)。

总结

类加载器是 Java 虚拟机的重要组成部分,它负责将类的字节码加载到内存中并转换为 Class 对象。双亲委派模型是类加载器的核心机制,它保证了 Java 核心类的安全性和一致性。自定义类加载器可以实现一些特殊的功能,如加载非标准位置的类、实现类的热部署、加密类等。

通过理解类加载器的工作原理,我们可以更好地掌握 Java 的运行机制,解决一些与类加载相关的问题,如类冲突、类加载失败等。同时,自定义类加载器也为我们提供了一种灵活的扩展机制,可以根据需要实现一些特殊的功能。