类加载器

  |  

摘要: 本文主要是关于类加载器的原理和应用。

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


类加载器

类加载器根据类的二进制名(binary name)读取 Java 编译器编译好的字节码文件(.class 文件),生成一个 Class 类的实例。之后,JVM 用 Class 实例来生成类的对象。

下面是 Java 语言规范定义的二进制名的一些例子:

1
2
3
4
java.lang.String
javax.swing.JSpinner$DefaultEditor
java.security.KeyStore$Builder$FileBuilder$1
java.net.URLClassLoader$3$1

类加载器的分类

java.lang.ClassLoader 是一个抽象的类,几乎所有的类加载器都是该类的实例。JVM 在执行 Java 代码时,至少会用到三种类加载器:

(1) 引导类加载器

引导类加载器是在 JVM 运行时内嵌在 JVM 中的一段用来加载 Java 核心类库的特殊 C++ 代码。Java.lang.String 类就是由引导类加载器加载的。

引导类加载器不是用 Java 代码编写的,所以它并不是 ClassLoader 类的实例,且没有父级。在 HotSpot 虚拟机中用 null 表示引导类加载器。

(2) 平台类加载器

平台类加载器用于加载平台类,可以用作 ClassLoader 实例的父级。平台类包括 Java SE 平台 API、它们的实现类,以及由平台类加载器或其祖先定义的特定于 JDK 的运行时类。

(3) 系统类加载器

系统类加载器又被称为应用程序类加载器。其通常用于在应用程序类路径、模块路径,以及特定于 JDK 的工具上定义类,我们自己编写的 Java 类通常都是由此类加载器完成加载的。平台类加载器是系统类加载器的父级或祖先。


java.lang.ClassLoader 相关的 API

ClassLoader 中有两个静态方法,用于得到平台类加载器和系统类加载器的实例。

1
2
public static ClassLoader getPlatformClassLoader()
public static ClassLoader getSystemClassLoader()

如果要得到某个加载器的父级类加载器,则可以调用 getParent 方法:

1
2
// 如果类加载器的父级是引导类加载器,那么 getParent 方法将返回 null。
public final ClassLoader getParent()

每个 Class 对象都包含对定义它的类加载器的引用。Class 类的 getClassLoader 方法可以得到定义该类的类加载器:

1
public ClassLoader getClassLoader()

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PrintClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
// 获取平台类加载器
ClassLoader platformCl = ClassLoader.getPlatformClassLoader();
System.out.println("平台类加载器: " + platformCl);
System.out.println("平台类加载器的父级: " + platformCl.getParent());

// 获取系统类加载器
ClassLoader systemCl = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器: " + systemCl);
System.out.println("系统类加载器的父级: " + systemCl.getParent());

// 获取 String 类的类加载器
ClassLoader strCl = String.class.getClassLoader();
System.out.println("String 类的类加载器: " + strCl);

// 获取当前类的类加载器
Class Cl = Class.forName("PrintClassLoader");
ClassLoader currentCl = Cl.getClassLoader();
System.out.println("当前类的类加载器: " + currentCl);
}
}

结果:

1
2
3
4
5
6
平台类加载器: jdk.internal.loader.ClassLoaders$PlatformClassLoader@4517d9a3
平台类加载器的父级: null
系统类加载器: jdk.internal.loader.ClassLoaders$AppClassLoader@55054057
系统类加载器的父级: jdk.internal.loader.ClassLoaders$PlatformClassLoader@4517d9a3
String 类的类加载器: null
当前类的类加载器: jdk.internal.loader.ClassLoaders$AppClassLoader@55054057

类加载器的加载机制

当 JVM 要加载某个类时,JVM 会先指定一个类加载器,负责加载此类。

而被指定的类加载器在尝试去根据某个类的二进制名查找其对应的字节码文件并定义之前,会首先委托给其父级加载器(getParent 方法返回的类加载器)尝试加载,如果加载失败,就会由自己来尝试加载此类。

在一般情况下,这个由 JVM 指定的类加载器就是系统类加载器,JVM 会自动调用其 loadClass 方法来开启类的加载过程,如下图:

这个加载机制就称为类加载的双亲委托模型,即由不同的类加载器负责加载特定的类。

类加载采用委托模型,可以保证 Java 核心类库的安全,即保证由引导类加载器加载的类不能被用户随便替换,用户不能自己随便定义一个名为 java.lang.String 的类来替换 Java 核心类库的 java.lang.String 类,否则会抛出 ClassCastException。


自定义类加载器

可以继承 ClassLoader 类,来扩展 Java 虚拟机动态加载类的方式。有几个应用场景:

  • 你的字节码的来源不是文件
  • 为了防止别人反编译你编写的类,对字节码文件做了混淆或加密
  • 需要自定义的类加载器来加载类

findClass 方法

自定义类加载器只需要重写 findClass 方法即可:

1
protected Class<?> findClass(String name) throws ClassNotFoundException

loadClass 方法

ClassLoader 类中有一个 loadClass 方法,使用指定的二进制名加载类,返回一个 Class 对象,与 Class.forName 方法的作用类似。

在 ClassLoader 实例上调用 loadClass 方法,会自动调用 findClass 方法。

defineClass 方法

在重写的 findClass 方法中,我们可以采用任何方式来得到一个类的字节码数据,然后调用 ClassLoader 类中的一个 final 方法 defineClass 将存放字节码数据的字节数组转换为 Class 类的实例。:

1
protected final Class<?> defineClass(String name, byte[] b,int off, int len) throws ClassFormatError

例子: 从文件中加载类的类加载器

在 findClass 中,我们先调用了辅助方法 loadData,然后调用 defineClass 方法将保存字节码数据的字节数组转换为 Class 类的实例。

在 loadData 辅助方法中,我们通过读取字节码文件的方式加载类。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.io.File;
import java.io.FileInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {
// 文件路径
private String filePath;

public MyClassLoader(String filePath) {
this.filePath = filePath;
}

/**
* 重写 findClass 方法
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clz = null;
byte[] data = loadData();
if(data != null) {
// 调用父类的 defineClass 方法,得到 Class 实例
clz = defineClass(name, data, 0, data.length);
}
return clz;
}

/**
* 辅助方法,读取字节码文件,将内容转换为字节数组
* 这里可以根据需求以任何方式得到类的字节码数据,例如有解密操作等等。
*/
private byte[] loadData() {
File file = new File(filePath);
if(file == null) {
return null;
}
byte[] data = null;
try (
// 构造文件输入流对象
FileInputStream in = new FileInputStream(file);
// 构造字节数组输出流对象,该对象将内容写到内部的一个缓冲区
ByteArrayOutputStream out = new ByteArrayOutputStream();
) {
byte[] buf = new byte[1024];
int size = 0;
while((size = in.read(buf)) != -1) {
out.write(buf, 0, size);
}
// 将字节数组输出流对象内部的缓冲区内容复制到一个新的字节数组
data = out.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return data;
}
}

我们以文章 使用 Class 和反射创建类的对象 中的 Person 类为例。假设这个类已经经过编译生成了 Person.class。我们通过自定义的类加载器来加载这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.lang.reflect.Constructor;

public class MyClassLoaderTest {
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("Person.class");
Class<?> clz = classLoader.loadClass("Person");

Constructor noArgCons = clz.getConstructor();
Object obj = noArgCons.newInstance();
System.out.println(clz);
System.out.println(obj);
}
}

结果:

1
2
class Person
姓名: 匿名

Share