Java类加载

一个 Java 应用在主动使用某个类时,如果该类还未加载到内存中,那么会由类加载器读取指定的 class 文件加载该类、链接、然后由 JVM 对该类进行初始化。

类的生命周期

1
加载 -> 链接(验证、准备、解析) -> 初始化 -> 使用 -> 卸载

加载:加载阶段的目的是将类的 .class 文件加载到内存中。在这个阶段,类加载器会根据类的全限定名来获取定义该类的二进制字节流,并将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。加载过程会创建一个 java.lang.Class 类的实例来表示这个类。这个Class对象作为程序中每个类的数据访问入口。

链接:在链接阶段,Java 类加载器对类进行验证、准备(默认初始值、static变量等等)和解析操作。将类与类的关系(符号引用转为直接引用)确定好,校验字节码

初始化:初始化是类加载的最后一步,也是真正执行类中定义的 Java 代码(字节码),初始化阶段是执行类构造器 <init> 方法的过程。类的初始化使用了懒加载机制,所有实现必须在每个类或接口被首次主动使用才初始化。

使用:类在加载完毕后,会有代码段来引用该类,如初始化该类的对象,或者通过反射获取该类的元数据。

卸载:该类所有的实例都已被回收、该类的类加载器已经被回收后,那么这个类会在 FullGC 后被卸载。

类加载器

Java 的类加载器中采用双亲委派机制(Parent-Delegate Model),该机制的核心思想是:如果一个类加载器收到了类加载请求,默认先将该请求委托给其上级类加载器处理,只有当上级级加载器无法加载该类时,才会尝试自行加载。

类加载器的层级

类加载器分为以下几个层级:

  • 启动类加载器(Bootstrap ClassLoader): 负责加载 %JAVA_HOME%/jre/lib 目录下的核心Java类库如 rt.jar、charsets.jar 等。

  • 扩展类加载器(Extension ClassLoader): 负责加载 %JAVA_HOME%/jre/lib/ext 目录下的扩展类库。在 JDK9 中被重命名为 Platform ClassLoader,主要加载 JDK 平台的非核心扩展类库(除java.*之外的)。

  • 应用类加载器(Application ClassLoader): 负责加载用户类路径(ClassPath)下的应用程序类。

这三种类加载器之间存在父子层级关系。启动类加载器是最高级别的加载器,没有父加载器;扩展类加载器的父加载器是启动类加载器;应用类加载器的父加载器是扩展类加载器。除此之外还有自定义类加载器,用于根据实际需求处理类加载请求。

双亲委派的特点

  1. 通过双亲委派机制,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
  2. 双亲委派可以保证 Java 核心类的安全性。BootstrapClassLoader 在加载类的时候,只会加载 JAVA_HOME 中的 jar 包里面的类,如 java.lang.String,这个类不会也不能被随意替换,可以避免有人自定义一个有破坏功能的,有效防止核心 Java API 被篡改。
  3. 双亲委派可以保持类加载的一致性,确保了同一个类的加载由同一个类加载器完成,从而在运行时保证了类型的唯一性和相同性。这也有助于减轻类加载器在处理相互关联的类时的复杂性。
  4. 在使用双亲委派机制进行类加载的过程中,需要不断地查询并委托父类加载器,这意味着类加载所需要的时间可能会增加。在类数量庞大或类加载器层次比较深的情况下,这种时间延迟可能会变得更加明显。

虽然可以通过破坏双亲委派屏蔽 Bootstrap ClassLoader,但无法重写 java.* 包下的类。要破坏双亲委派模型需要继承 ClassLoader 并重写其中的 loadClass()findClass() 方法,最后调用 ClassLoader 中的 defineClass() 方法将字节流转换为 JVM 可识别的 class,该方法被 final 关键字修饰,不允许被重写。在该方法中限制了类名不能以 java.* 开头,因此保护了核心 Java API 不被篡改。

java.lang.ClassLoader#preDefineClass 中截取的 891~899 行的相关代码如下:

1
2
3
4
5
6
7
8
9
// Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")
&& this != getBuiltinPlatformClassLoader()) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}

如何破坏双亲委派

ClassLoader 中有两个重要的方法,loadClass()findClass() 方法。

破坏双亲委派机制只需要自定义一个类加载器,继承 ClassLoader 类重写其中的 loadClass() 方法,使其不进行双亲委派即可。

findClass() 方法用于定义类加载逻辑,如果不想破坏双亲委派只需重写该方法,如果上级的类加载器加载失败,会调用自己的 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
/**
* 重写方法破坏双亲委派
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null) {
// 如果已加载类中还没有该类, 尝试用自定义的方法加载
try {
loadedClass = findClassInPath(name);
} catch (ClassNotFoundException e) {
// 如果自定义加载方法找不到类,则委托给父类加载器
loadedClass = super.loadClass(name, resolve);
}
}
if (resolve) {
resolveClass(loadedClass);
}
return loadedClass;
}

/**
* 自定义加载方法
*/
private Class<?> findClassInPath(String className) throws ClassNotFoundException {
try {
// 使用类名 className 在 classesPath 指定的目录下查找对应的 .class 文件
String filePath = className.replace('.', '/') + ".class";
// 将文件内容读取为字节数组
byte[] classBytes = Files.readAllBytes(Paths.get(classesPath, filePath));
// 调用 defineClass 方法,将其转换为 Java 类的 Class 对象
return defineClass(className, classBytes, 0, classBytes.length);
} catch (Exception e) {
throw new ClassNotFoundException("Class not found in classes path: " + className, e);
}
}