Appearance
JVM 类加载系统
本文介绍JVM中的类加载系统,类加载系统负责将字节码文件(*.class)加载进内存。本文代码示例使用JDK 1.8。
1. 类的加载过程
类的加载过程分为三大阶段:加载阶段、链接阶段和初始化阶段。当完成加载过程后,我们就可以在程序中正常使用类了。
在Java中数据类型分为基本数据类型和引用数据类型,基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。
1.1 加载阶段-loading
1.1.1 加载阶段介绍
加载,简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。
所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVM从字节码文件中解析出常量池、类字段、类方法等信息,并将这些信息存放在类模板对象中,这样JVM在运行期便能通过类模板对象获取Java类中的任意信息。反射的机制便是基于这一基础,如果JVM没有将Java类的声明信息存储起来,程序中也无法使用反射。
在加载类时,Java虚拟机完成了以下三件事:
通过类的全名,获取类的二进制数据流;
对于类的二进制数据流,可以有多种途径产生或获得(只要获取的字节码符合JVM规范即可):
- 可以从文件系统读取一个class后缀的文件(最常见);
- 读取jar、zip等归档数据包中的类文件;
- 读取存放在数据库中的类的二进制数据;
- 使用类似于HTTP之类的协议通过网络加载;
- 在运行时动态生成类的二进制数据;
解析类的二进制数据流为方法区中的数据结构(类模板对象);
在堆中创建
java.lang.Class对象,引用方法区中的类模板对象,作为方法区中这个类的各个数据的访问入口;

1.1.2 数组的加载
由于数组类型没有对应的 .class 文件,其加载过程与普通类有所不同,可以概括为以下步骤:
确定数组元素的加载:
如果数组的元素是基本数据类型(例如
int、boolean、float等),JVM 会直接创建对应的数组类型,不需要加载元素类型。如果数组的元素是引用类型(例如
String、自定义类等),JVM 会检查该类型是否已经被加载。如果尚未加载,则会触发该类型的加载。注意,不会触发初始化阶段:
javapublic class ArrayLoadDemo { public static void main(String[] args) { Order[] orders = new Order[10]; } } class Order{ static { System.out.println("Order static block"); } } // 结果不会输出 Order static block
创建数组类型对象: JVM 会在内部生成一个代表该数组类型的
java.lang.Class对象。这个Class对象包含了数组的元数据信息,例如元素类型、维度等。确定数组类型的类加载器: 数组类型的类加载器由以下规则确定:
如果数组的元素类型是基本数据类型,则数组类型的类加载器与引导类加载器 (Bootstrap ClassLoader) 相同。
如果数组的元素类型是引用类型,则数组类型的类加载器与该组件类型的类加载器相同。
javapublic class ArrayLoadDemo { public static void main(String[] args) { Order[] orders = new Order[10]; ClassLoader orderArrClassLoader = orders.getClass().getClassLoader(); ClassLoader orderClassLoader = Order.class.getClassLoader(); System.out.println(orderArrClassLoader); // jdk.internal.loader.ClassLoaders$AppClassLoader@76ed5528 System.out.println(orderClassLoader == orderArrClassLoader); // true int[] intArr = new int[10]; System.out.println(intArr.getClass().getClassLoader()); // null } } class Order{ static { System.out.println("Order static block"); } }null 则说明是引导类加载器,下文会详细说明。
1.2 链接阶段-linking
链接阶段又分为三个步骤:验证、准备与解析。
1.2.1 验证-verification
验证的目的是保证加载的字节码是合法合理、符合规范的。
验证分为以下内容:
- 格式检查:包括魔数检查、版本检查、长度检查等;
- 语义检查:包括某类是否继承了
final修饰的父类、某类是否有父类、抽象方法是否有实现等; - 字节码验证:包括跳转至零是否指向了正确位置、操作数类型是否合理等;
- 符号引用验证:验证解析后的引用是否存在;
格式检查包括但不限于以下内容:
魔数 (Magic Number) 验证: 检查字节流的开头四个字节是否为
0xCAFEBABE。这是 Class 文件的重要标志。主、次版本号验证: 检查 Class 文件的版本号是否在当前 JVM 能够接受的范围内。高版本的 Class 文件可能包含当前 JVM 不支持的特性。
常量池项的类型和格式验证: 检查常量池中的每个常量项的类型标记是否合法,以及其数据格式是否符合规范。例如,检查字符串常量的长度是否超出限制,UTF-8 编码是否有效等。
文件中各个部分的长度和偏移量验证: 检查 Class 文件中各个部分的长度是否符合规范,例如字段表、方法表、属性表等的长度是否有效。
是否包含不支持的属性: 检查 Class 文件中是否包含当前 JVM 版本不支持的属性。
注意:格式验证会和加载阶段一起执行,格式验证通过后,类加载器才会成功将类的二进制数据信息加载到方法区中。
语义检查包括但不限于以下内容:
类和接口的继承规则验证:
检查是否有父类 (除了
java.lang.Object)。检查父类是否为
final类(final类不能被继承)。检查类实现的接口是否合法(例如,接口不能继承自类)。
检查是否出现了循环继承。
字段和方法的类型验证: 检查字段和方法的描述符是否引用了不存在的类或接口,或者引用了不合法的类型。
字段和方法的修饰符验证: 检查字段和方法的修饰符 (例如
public、private、static、final等) 的组合是否合法。例如,final和abstract不能同时修饰一个方法。方法签名验证: 检查子类重写父类方法时,方法签名 (方法名、参数类型和顺序、返回值类型) 是否符合规则。
常量池中的符号引用验证: 虽然符号引用验证主要在解析阶段进行,但在语义检查时也会对常量池中与类、字段、方法相关的符号引用进行一些初步的检查,例如确保指向的类名、字段名、方法名等符合基本格式。
字节码验证包括但不限于以下内容:
操作数栈和局部变量表的类型匹配验证: 确保在任何时刻,操作数栈的数据类型与指令代码序列都能配合工作。例如,不会出现将一个
int类型的值压入栈后,却按照long类型来使用的情况。指令跳转的合法性验证: 确保跳转指令不会跳转到方法体以外的字节码指令上。
方法体中的类型转换验证: 确保方法体中的类型转换总是有效的。
异常处理的合法性验证: 检查异常处理表中的范围是否有效,以及异常处理器类型是否正确。
方法体中的常量池访问验证: 确保对常量池的访问是合法的,例如不会访问不存在的常量项,或者访问类型不匹配的常量项。
多态调用的类型验证: 确保在进行多态方法调用时,实际执行的方法是符合预期的。
数据流分析: 确保变量在赋值前已经被初始化,方法调用参数类型和数量是正确的。
控制流分析: 确保方法中的所有执行路径最终都能正常结束,不会出现无限循环等情况。
符号引用验证包括但不限于以下内容:
符号引用指向的类是否存在并且可访问: 验证符号引用中通过字符串描述的全限定名是否能找到对应的类,并且当前类是否有权限访问该类。
符号引用指向的字段是否存在并且可访问: 在指定的类中是否存在符合字段描述符以及简单名称所描述的字段,并且当前类是否有权限访问该字段。
符号引用指向的方法是否存在并且可访问: 在指定的类中是否存在符合方法描述符以及简单名称所描述的方法,并且当前类是否有权限访问该方法。
符号引用指向的接口方法是否存在并且可被实现: 对于接口方法,验证引用的方法是否存在于接口中,并且当前类是否实现了该接口或者父类是否实现了该接口。
注意:符号引用验证发生在链接阶段的解析(Resolution)步骤中。当 JVM 需要将常量池中的符号引用转化为直接引用时,会进行符号引用验证,以确保解析后的符号引用可以被正确地访问到。
1.2.2 准备-preparation
准备环节是为类的静态变量分配内存,并将其初始化为默认值。Java虚拟机为各类型的变量设置的默认初始值如下表:
| 类型 | 默认初始值 |
|---|---|
| byte | (byte)0 |
| short | (short)0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0 |
| char | \u0000 |
| boolean | false |
| reference | null |
注意:Java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是0,所以boolean的默认值就是false。
- 准备环节不包括基本数据类型用
static final修饰的情况,因为final修饰的字段在编译的时候就确定了具体值,在准备阶段会显式赋值; - 在准备环节不会为实例变量分配初始值,实例变量是会随着对象一起分配到Java堆中,而类变量是分配在方法区中的;
- 在准备环节不会像初始化阶段会有代码执行;
在IDEA中安装插件:jclasslib Bytecode Viewer
查看下面的例子:

在准备阶段,常量(final修饰的字段)就会显式赋值了,并不会赋初始值。
1.2.3 解析-resolution
解析环节,简而言之就是将类、接口、字段、方法的符号引用转换为直接引用。
在编译阶段,一个 Java 类并不知道它所引用的其他类、方法或字段的实际内存地址。因此,它使用符号引用来表示这些被引用的元素。符号引用是一组符号化的描述符,例如:
- 类和接口的全限定名 (Fully Qualified Name)
- 字段的名称和描述符 (Name and Descriptor)
- 方法的名称和描述符 (Name and Descriptor)
在解析阶段,JVM 需要根据这些符号引用在运行时常量池中找到对应的实体,并将其替换为可以直接指向这些实体的内存地址、偏移量或者句柄等直接引用。
解析阶段主要做了以下事情:
- 查找符号引用指向的实体: JVM 根据常量池中的符号引用,在方法区中查找对应的类、接口、字段或方法。这个查找过程可能涉及到类加载器的工作,如果被引用的类尚未加载,则会触发该类的加载过程(但不一定完成初始化)。
- 验证访问权限: 在查找成功后,JVM 会进行符号引用验证(Symbolic References Verification),检查当前类是否具有对被引用实体进行访问的权限。例如,检查是否尝试访问私有(private)成员等。如果权限不足,会抛出
IllegalAccessError异常。 - 替换为直接引用: 如果查找成功且权限验证通过,JVM 会将常量池中对应的符号引用替换为直接引用。直接引用可以是以下几种形式:
- 直接指向目标的指针 (Pointer): 指向方法区中类、接口、字段或方法的内存地址。
- 相对偏移量 (Offset): 指向方法区中字段或方法相对于其所属类型的偏移量。
- 间接定位到目标的句柄 (Handle): 一种更抽象的引用形式,由 JVM 实现决定。
解析的对象主要包括常量池中以下类型的符号引用:
- 类和接口的符号引用 (
CONSTANT_Class_info):解析为指向方法区中对应的类或接口的直接引用。 - 字段的符号引用 (
CONSTANT_Fieldref_info):解析为指向方法区中对应的字段的直接引用。 - 方法的符号引用 (
CONSTANT_Methodref_info):解析为指向方法区中对应的类方法的直接引用。 - 接口方法的符号引用 (
CONSTANT_InterfaceMethodref_info):解析为指向方法区中对应的接口方法的直接引用。 - 方法句柄和方法类型 (
CONSTANT_MethodHandle_info和CONSTANT_MethodType_info):虽然它们的解析过程更复杂,但也属于解析阶段的范畴。
**解析的发生时机:**解析阶段可以在类加载过程的任何时刻发生,具体取决于 JVM 的实现策略。通常有两种策略:
- 即时解析(Eager Resolution): 在链接阶段一开始就对所有符号引用进行解析。
- 延迟解析(Lazy Resolution): 只有当符号引用真正被使用时(例如,当需要执行对应的指令时)才进行解析。这种策略可以节省一些不必要的解析开销,尤其是在类中存在大量未被使用到的符号引用的情况下。HotSpot 虚拟机通常采用延迟解析的策略。
1.3 初始化阶段-initialization
1.3.1 概述
初始化阶段的主要任务是为类的静态变量赋予正确的初始值,并执行静态代码块中的代码。通过执行类构造方法<clinit>()来完成初始化工作。
关于类构造方法<clinit>():
- 方法合成: JVM 会自动收集类中所有类变量(静态变量)的赋值动作和**静态代码块(
static {})**中的语句,并将它们合并为一个特殊的类构造器方法,称为<clinit>()方法。 - 执行顺序: JVM 会按照源代码中静态变量赋值语句和静态代码块出现的顺序依次执行它们。
- 无显式调用:
<clinit>()方法不能在代码中显式调用,它是由 JVM 在类的初始化阶段隐式调用的。 - 父类先执行: 如果一个类存在父类,并且父类还没有被初始化,那么 JVM 会先执行父类的
<clinit>()方法,然后再执行当前类的<clinit>()方法。对于接口,不需要先执行父接口的<clinit>()方法,只有当接口中定义了静态变量的赋值操作时才会执行接口的<clinit>()方法。 - 线程安全性: JVM 会保证一个类的
<clinit>()方法在多线程环境中被正确地加锁同步,确保类的初始化过程只会被执行一次。如果多个线程同时去初始化同一个类,那么只会有一个线程能够执行该类的<clinit>()方法,其他线程将会阻塞等待,直到该类的初始化完成。
1.3.2 <clinit>()方法
java
public class InitializationDemo01 {
static {
System.out.println("static block 1");
}
public static int NUM1 = 2;
static {
System.out.println("static block 2");
}
}通过jclasslib bytecode viewer查看字节码文件,发现在方法下多了一个<clinit>()方法,这就是类构造方法:

并且查看字节码,发现其中代码的执行顺序是源代码中静态变量赋值语句和静态代码块出现的顺序依次执行它们
txt
0 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
3 ldc #13 <static block 1>
5 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
8 iconst_2
9 putstatic #21 <util/jvm/t2/InitializationDemo01.NUM1 : I>
12 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
15 ldc #27 <static block 2>
17 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
20 return1.3.3 类常量是否需要初始化问题
关于final修饰的类变量是否会在初始化阶段执行赋值操作,取决于final的类变量取值来源,如果需要执行代码获取值,则需要执行<clinit>()方法:
java
public class InitializationDemo01 {
public static final int NUM1 = 2;
public static final int NUM2 = new Random().nextInt();
public static final Integer NUM3 = 3;
public static final String STRING_1 = "hello1";
public static final String STRING_2 = new String("hello2");
}使用jclasslib bytecode viewer查看字节码文件:

txt
0 new #7 <java/util/Random>
3 dup
4 invokespecial #9 <java/util/Random.<init> : ()V>
7 invokevirtual #10 <java/util/Random.nextInt : ()I>
10 putstatic #14 <util/jvm/t2/InitializationDemo01.NUM2 : I>
13 iconst_3
14 invokestatic #20 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
17 putstatic #26 <util/jvm/t2/InitializationDemo01.NUM3 : Ljava/lang/Integer;>
20 new #30 <java/lang/String>
23 dup
24 ldc #32 <hello2>
26 invokespecial #34 <java/lang/String.<init> : (Ljava/lang/String;)V>
29 putstatic #37 <util/jvm/t2/InitializationDemo01.STRING_2 : Ljava/lang/String;>
32 return- 从字段类别看,只有
NUM1和STRING_1在编译后就确定了是常量值,即在初始化阶段不会赋初始值; - 从
<clinit>()方法看,在类构造方法中为NUM2、NUM3和STRING_2赋了初始值,虽然这些字段是类常量,但是仍然需要执行代码才能获取值。尤其注意的是NUM3,虽然从代码上看给NUM3赋的是字面量,但是由于自动装箱机值的存在,仍然执行了Integer.valueof()方法,所以仍然需要在类构造方法中执行代码。
综上所述,确定一个类常量是否会在类构造方法(初始化阶段)赋初始值,只需要确定类常量的值是否需要执行代码获得,如果需要执行代码,那么就需要执行类构造方法。
1.3.4 线程安全性
我们通过下面的例子说明类构造方法的线程安全性:
如果类构造方法是非线程安全性的,那么执行下面的代码后,会在控制台中输出A static block和B static block,但是实际控制台中没有任何输出,并且程序阻塞。说明程序发生了死锁,证明了类构造方法是线程安全的。
java
public class InitializationDemo01 {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
new A();
});
Thread thread2 = new Thread(() -> {
new B();
});
thread1.start(); // 触发A类的初始化过程
thread2.start(); // 触发B类的初始化过程
}
}
class A{
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new B(); // 触发B类的初始化过程
System.out.println("A static block");
}
}
class B{
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new A(); // 触发A类的初始化过程
System.out.println("B static block");
}
}1.3.5 类的主动使用与被动使用
JVM 并不会在类加载的链接阶段结束后就立即进行初始化,而是采用惰性初始化的策略,只有主动使用类时才会触发类的初始化阶段,主动使用类有以下几种情况:
当创建一个类的实例时,比如使用
new关键字、或使用反射、克隆、反序列化的方式;当调用类的静态方法时;
当使用类、接口的静态字段时(类常量字段特殊考虑,即
final修饰的类字段);当使用
java.lang.Class包中的方法反射类时,比如Class.forName("");当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发父类的初始化阶段;
当虚拟机启动时,用户需要指定一个主类(包含
main()方法的类),虚拟机会初始化主类;如果一个接口定义了
default方法,那么直接实现或间接实现该接口的类进行初始化时,该接口要在其之前被初始化;如果一个接口没有定义
default方法,那么实现该接口的类进行初始化时,该接口不会被初始化;如果某接口的子接口被初始化时,无论父接口有没有定义
default方法,父接口都不会被初始化;当初次调用
MethodHandle实例时,初始化该MethodHandle指向的方法所在的类;
被动使用类不会触发类的初始化阶段,被动使用有以下几种情况:
- 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化,即当通过子类访问父类的静态变量时,父类会初始化,子类不会初始化;
- 如果数组元素是引用类型,那么定义数组时,不会触发类的初始化;
- 引用常量不会触发此类或接口的初始化,因为常量在链接阶段就已经被显式赋值了;
- 调用
ClassLoader的loadClass()方法加载一个类,不会导致类的初始化;
接下来用代码演示上述情况:
首先准备几个类用于演示:
java
public class User {
public static int num1 = 1;
public static final int num2 = 2;
public static final Integer num3 = 3;
static {
System.out.println("User static block");
}
public static void sayHello(){
System.out.println("hello");
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}java
public class Parent {
static {
System.out.println("Parent static block");
}
public static int num = 100;
}java
public class Child extends Parent{
static {
System.out.println("Child static block");
}
}java
public interface ITest {
public static final Thread THREAD = new Thread(){
{
System.out.println("ITest initialization");
}
};
}java
public interface IChildTest extends ITest{
public static final Thread THREAD = new Thread(){
{
System.out.println("IChildTest initialization");
}
};
public static void staticMethod(){
System.out.println("IChildTest staticMethod");
}
}java
public interface ITestWithDefaultMethod {
public static final Thread THREAD = new Thread(){
{
System.out.println("ITestWithDefaultMethod initialization");
}
};
default void defaultMethod(){
System.out.println("ITestWithDefaultMethod defaultMethod");
}
}java
public interface IChildTestExtendsParentWithDefault extends ITestWithDefaultMethod{
public static final Thread THREAD = new Thread(){
{
System.out.println("IChildTestExtendsParentWithDefault initialization");
}
};
public static void staticMethod(){
System.out.println("IChildTestExtendsParentWithDefault staticMethod");
}
}java
public class ITestImpl implements ITest{
static {
System.out.println("ITestImpl static block");
}
}java
public class ITestWithDefaultMethodImpl implements ITestWithDefaultMethod{
static {
System.out.println("ITestWithDefaultMethodImpl static block");
}
}主动使用类的几种情况:
java
// 1. 当创建一个类的实例时,比如使用 new 关键字、或使用反射、克隆、反序列化的方式
// 使用new关键字
User user = new User();
// 使用反序列化(依赖Gson框架)
String jsonStr = "{\"name\":\"zs\"}";
User user = new Gson().fromJson(jsonStr, User.class);
// 2. 调用类的静态方法时
User.sayHello();
// 3. 使用类、接口的静态字段时(类常量字段特殊考虑,即final修饰的类字段)
System.out.println(User.num1); // 会初始化
System.out.println(User.num2); // 不会初始化
System.out.println(User.num3); // 会初始化
// 4. 当使用java.lang.Class包中的方法反射类时,比如Class.forName("")
Class.forName("com.lee.User");
// 5. 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发父类的初始化阶段
new Child();
// 输出结果如下:
// Parent static block
// Child static block
// 6. 当虚拟机启动时,用户需要指定一个主类(包含`main()`方法的类),虚拟机会初始化主类
public class InitializationDemo {
static {
System.out.println("InitializationDemo static block");
}
public static void main(String[] args) {
}
}
// 7. 如果一个接口定义了 default 方法,那么直接实现或间接实现该接口的类或子接口进行初始化时,该接口要在其之前被初始化;如果一个接口没有定义 default 方法,那么实现该接口的类或子接口进行初始化时,该接口不会被初始化;
// 7.1 一个接口没有定义default方法,子接口初始化,父接口不会被初始化
IChildTest.staticMethod();
// 输出:
// IChildTest initialization
// IChildTest staticMethod
// 7.2 一个接口有定义default方法,子接口初始化,父接口不会被初始化
IChildTestExtendsParentWithDefault.staticMethod();
// 输出:
// IChildTestExtendsParentWithDefault initialization
// IChildTestExtendsParentWithDefault staticMethod
// 7.3 一个接口没有定义default方法,实现类初始化时,接口不会被初始化
new ITestImpl();
// 输出:
// ITestImpl static block
// 7.4 一个接口有定义default方法,实现类初始化时,接口会被初始化
new ITestWithDefaultMethodImpl();
// 输出:
// ITestWithDefaultMethod initialization
// ITestWithDefaultMethodImpl static block被动使用类的情况
java
// 1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化,即当通过子类访问父类的静态变量时,父类会初始化,子类不会初始化;
System.out.println(Child.num);
// 输出:
// Parent static block
// 100
// 2. 如果数组元素是引用类型,那么定义数组时,不会触发类的初始化;
Parent[] parents = new Parent[10]; // 没有任何输出
// 3. 引用常量不会触发此类或接口的初始化,因为常量在链接阶段就已经被显式赋值了;
System.out.println(User.num2); // 不会初始化
// 4. 调用 ClassLoader 的 loadClass() 方法加载一个类,不会导致类的初始化;
Launcher.getLauncher().getClassLoader().loadClass("com.lee.Parent");2. 类的使用与卸载
2.1 类的使用
在完成类加载后,我们就可以正常使用类了,这一部分不再多说。
2.2 类的卸载
类的卸载是指被加载到方法区中的类模板不再被任何地方“引用”时,被 JVM 的垃圾回收器回收的过程。
类的卸载需要满足苛刻的条件,只有全部满足下面三个条件时才会被卸载:
- 该类的所有实例都已经被回收。 这意味着堆内存中不存在该类的任何对象实例(包括直接实例和子类实例)。
- 加载该类的
ClassLoader实例也已经被回收。 这是最关键的条件。只有当加载该类的类加载器本身不再被任何存活的对象引用时,该类才有可能被卸载。 - 该类对应的
java.lang.Class对象没有在任何地方被引用(例如,通过反射等方式)。
接下来以图示例子说明:要卸载方法区中的Sample类模板,即要保证没有引用指向类模板对象,即代表Sample类的Class对象需要被回收,而满足回收的条件就是没有其他地方引用它。在程序中,有三个地方会引用到Class对象,即要满足上面三个条件才可以使Class对象被回收,继而满足方法区中的类模板对象被回收。

需要注意的事项
- 无法强制卸载类: Java 语言规范没有提供任何直接强制卸载类的方法。类卸载完全由 JVM 的垃圾回收器决定。
- 类卸载的时机不确定: 即使满足了卸载条件,JVM 也不会立即进行卸载,具体的卸载时机取决于垃圾回收器的策略。
所以类卸载是需要满足严格条件才有可能达到的事,一般情况下,类是不会被卸载的。
3. 类加载器
3.1 概述
类加载器(Class Loader)负责通过各种方式将类的字节码加载到JVM内存中,转换为一个java.lang.Class对象实例,以便后续进行链接、初始化和使用。因此,类加载器只负责类的加载阶段。
类的加载分为两类:显式加载和隐式加载
- 显式加载:是指在代码中通过调用
ClassLoader加载Class对象,例如直接使用Class.forName("")或this.getClass().getClassLoader().loadClass()加载Class对象; - 隐式加载:是指不直接在代码中调用
ClassLoader加载对象,而是通过虚拟机自动把类字节码加载到内存中,例如当我们第一次创建对象new Order()时,虚拟机会自动把Order类加载到内存中。
类加载器的命名空间:类加载器的命名空间指的是该类加载器及其所有父类加载器所加载的所有类的集合。
类的唯一性:在JVM中,一个类是否“唯一”不仅仅取决于它的完全限定名 (Fully Qualified Name)(例如:com.example.MyClass),还取决于加载这个类的类加载器实例。这意味着同一个类(具有相同的完全限定名),如果被不同的类加载器实例加载,那么在JVM看来也是不同的类。
3.2 类加载器分类
在 Java 8 及其之前的版本中,类加载器主要可以分为以下几种:
1. 启动类加载器 (Bootstrap ClassLoader)
作用: 这是 JVM 最底层的类加载器,负责加载 JVM 自身运行所需的关键类,例如
java.lang.Object等。实现: 它是由 JVM 的原生代码实现的,而不是 Java 类。因此,在 Java 代码中通常无法直接获取到它的引用,返回
null。加载路径: 它从 JVM 预定的目录(通常是
<JAVA_HOME>/jre/lib目录下的rt.jar、resources.jar、sunrsasign.jar等,也可以从系统属性sun.boot.class.path中获取路径)中加载类。javaString bootClassPath = System.getProperty("sun.boot.class.path"); for (String path : bootClassPath.split(File.pathSeparator)) { System.out.println(path); }txtxxx/jre/lib/resources.jar xxx/jre/lib/rt.jar xxx/jre/lib/jsse.jar xxx/jre/lib/jce.jar xxx/jre/lib/charsets.jar xxx/jre/lib/jfr.jar xxx/jre/classes安全性:处于安全考虑,启动类加载器只加载包名为java、javax、sun等开头的类。
2. 扩展类加载器 (Extension ClassLoader)
作用: 负责加载 Java 扩展目录中的类。
实现: 它是一个由 Java 语言实现的类加载器,通常是
sun.misc.Launcher$ExtClassLoader。父类加载器: 它的父类加载器是启动类加载器。
加载路径: 它从 JVM 预定的扩展目录(通常是
<JAVA_HOME>/jre/lib/ext目录,或者由java.ext.dirs系统属性指定的目录)中加载类。java// 扩展类加载器加载路径 String extDir = System.getProperty("java.ext.dirs"); for (String path : extDir.split(File.pathSeparator)) { System.out.println(path); }
3. 系统类加载器 (System ClassLoader) / 应用类加载器 (Application ClassLoader)
作用: 负责加载应用程序 classpath 下的类。这是我们平时开发中最常用的类加载器。
实现: 它也是一个由 Java 语言实现的类加载器,通常是
sun.misc.Launcher$AppClassLoader。父类加载器: 它的父类加载器是扩展类加载器。
加载路径: 它从系统 classpath 中加载类,classpath 由环境变量
CLASSPATH指定,或者在启动 JVM 时通过-classpath或-cp参数指定,以及当前应用程序的类路径。java// 获取环境变量CLASSPATH的值 String classpath = System.getenv("CLASSPATH"); System.out.println(classpath); // 获取系统属性 String javaClassPath = System.getProperty("java.class.path"); for (String path : javaClassPath.split(File.pathSeparator)) { System.out.println(path); }
4. 自定义类加载器 (Custom ClassLoader)
- 作用: 开发者可以根据自己的需求创建自定义的类加载器,用于加载特定来源的类,例如从网络、数据库、加密文件中加载类,或者实现特定的加载策略。
- 实现: 通过继承
java.lang.ClassLoader类,并重写其findClass()方法来实现。 - 父类加载器: 自定义类加载器的父类加载器可以根据需要指定。如果没有显式指定,默认情况下它的父类加载器是系统类加载器。
关于类加载器的关系图如下:

注意:上面说的父类并不是Java中的继承关系,而是类加载器中有一个属性名为parent,我们可以通过getParent()获取父类加载器。
获取各类加载器的代码:
java
// 获取系统类加载器
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
// 获取扩展类加载器
ClassLoader extClassLoader = appClassLoader.getParent();
// 获取引导类加载器
ClassLoader bootClassLoader = extClassLoader.getParent();
System.out.println(appClassLoader); //sun.misc.Launcher$AppClassLoader@5ce65a89
System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@3d012ddd
System.out.println(bootClassLoader); //null我们也可以通过Class对象实例获取类加载器:
java
ClassLoader bootClassLoader = java.lang.String.class.getClassLoader(); // 获取启动类加载器
System.out.println(bootClassLoader); //null
ClassLoader appClassLoader = Order.class.getClassLoader(); // 获取系统类加载器
System.out.println(appClassLoader); //sun.misc.Launcher$AppClassLoader@5ce65a89关于从数组中获取类加载器,有以下情况:
java
String[] strArr = new String[10];
ClassLoader classLoader1 = strArr.getClass().getClassLoader();
System.out.println(classLoader1); // null: 说明strArr数组的类加载器是启动类加载器
Order[] orderArr = new Order[10];
ClassLoader classLoader2 = orderArr.getClass().getClassLoader();
System.out.println(classLoader2); // sun.misc.Launcher$AppClassLoader@5ce65a89: 说明orderArr数组的类加载器是系统类加载器
int[] intArr = new int[10];
ClassLoader classLoader3 = intArr.getClass().getClassLoader();
System.out.println(classLoader3); // null: 说明intArr数组没有类加载器即如果数组元素是引用类型,则数组的类加载器和数组元素类加载器相同;如果数组元素是基本数据类型,则数组没有类加载器。
我们还可以通过线程获取上下文加载器:
java
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println(contextClassLoader); // sun.misc.Launcher$AppClassLoader@5ce65a89线程上下文类加载器是与当前线程关联的类加载器。默认情况下,一个新的线程会继承创建它的父线程的上下文类加载器。如果最开始创建的是主线程,那么它的上下文类加载器通常是系统类加载器(Application ClassLoader)。
3.3 双亲委派机制
类加载器之间存在着一种层次关系,被称为双亲委派模型 (Parent-First Delegation Model)。加载类的工作流程遵循双亲委派模型:
- 当一个类加载器收到加载类的请求时,它不会首先自己去加载,而是将这个请求委派给它的父类加载器去完成。
- 每一层的类加载器都会重复这个过程,直到委派给最顶层的启动类加载器。
- 只有当父类加载器在自己的加载路径下找不到所需的类时,子类加载器才会尝试自己去加载。

双亲委派模型的优点:
安全性: 避免了核心类库被篡改。例如,
java.lang.String始终由启动类加载器加载,保证了核心类的唯一性和安全性。例如,我们在自己的项目中定义一个
java.lang.String类,然后创建一个测试类创建String对象:javapackage java.lang; public class String { public String(){ System.out.println("自定义String"); } }javapackage com.test; public class ClassLoaderTest04 { public static void main(String[] args) { String s = new String(); } }结果并没有打印出
自定义String这句话,是因为当程序需要加载String类时,首先会从系统类加载器往上找,最终找到启动类加载器,启动类加载器加载核心类库中的String,所以不会输出自定义String这句话。如果我们在
java.lang包下自定义一个类测试:javapackage java.lang; public class StringTest { public static void main(String[] args) { String s = new String(); } }程序会报错:
txtException in thread "main" java.lang.SecurityException: Prohibited package name: java.lang at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655) at java.lang.ClassLoader.defineClass(ClassLoader.java:754)在
java.lang.ClassLoader.preDefineClass()方法中添加了安全性校验,不允许我们的包以java开头:javaif ((name != null) && name.startsWith("java.")) { throw new SecurityException("Prohibited package name: " + name.substring(0, name.lastIndexOf('.'))); }避免类的重复加载: 当父类加载器已经加载过某个类时,子类加载器就不会再重复加载,保证了类的唯一性。
java// 隐式加载String类 String str = new String(); // 系统类加载器试图加载String类 Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("java.lang.String"); // String 类的加载器 ClassLoader classLoader = clazz.getClassLoader(); System.out.println(classLoader); // null: 说明是启动器类加载器
我们可以在程序启动时添加虚拟机参数 -XX:+TraceClassLoading 打印出程序加载了哪些类,例如:
[Opened xxx/jre/lib/rt.jar] [Loaded java.lang.Object from xxx/jre/lib/rt.jar] [Loaded java.io.Serializable from xxx/jre/lib/rt.jar] [Loaded java.lang.Comparable from xxx/jre/lib/rt.jar] [Loaded java.lang.CharSequence from xxx/jre/lib/rt.jar] [Loaded java.lang.String from xxx/jre/lib/rt.jar]
...
3.4 源码解析
Java自带的类加载器的继承(真正的继承)关系如下:

我们可以看到,类加载器的父类是抽象类ClassLoader,其中定义了一些方法:
loadClass(String name, boolean resolve):根据指定的全限定类名加载类,resolve参数表示是否解析(默认情况下为false)。resolveClass():这里的方法名称翻译为解析,但是实际触发的链接过程。如源码描述(misleadingly named):Links the specified class. This (misleadingly named) method may be used by a class loader to link a class. If the class c has already been linked, then this method simply returns. Otherwise, the class is linked as described in the "Execution" chapter of The Java Language Specification.
findClass(String name):通过指定的全限定类名找到类,这个方法包含两个步骤:- 通过指定的全限定类名从文件系统或其他地方加载类字节码到内存中的字节数组;
- 调用
defineClass()将类字节数组转换为类Class对象;
defineClass(String name, byte[] b, int off, int len):将类字节数组转换为类Class对象;
接下来就详细说说各个方法的源码(删减掉不重要的代码):
ClassLoader.loadCladd(String name, boolean resolve):
java
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
// 获取类加载锁
synchronized (getClassLoadingLock(name)) {
// 1. 校验类是否已经被加载了
Class<?> c = findLoadedClass(name);
if (c == null) {
// 2. 类没有被加载,委托给父加载器加载(双亲委派机制在这里体现)
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 由启动类加载器加载类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 如果父加载器没有加载该类,则需要自己加载
if (c == null) {
c = findClass(name);
}
}
// 如果为true,则触发链接过程
if (resolve) {
resolveClass(c);
}
return c;
}
}关键点
注意:双亲委派机制在类加载器的loadClass()方法中体现。
ClassLoader.resolveClass()
java
protected final void resolveClass(Class<?> c) {
if (c == null) {
throw new NullPointerException();
}
}这个方法很奇怪,只有一个简单的空指针检查,并没有执行具体的链接过程。
关键在于,真正的链接 (linking) 阶段的逻辑并非完全在这个 Java 方法中实现,而是委托给了 JVM 的底层实现(通常是 native code)。它的主要作用是作为一个触发点,告诉 JVM 需要对传入的 Class 对象 c 执行链接操作。
连接的实际工作在 JVM 内部完成, JVM 的实现(比如 HotSpot VM)会在 native code 中实现连接阶段的详细逻辑。当 resolveClass() 方法被调用时,JVM 会识别出这个请求,并执行相应的验证、准备和解析步骤。
ClassLoader.findClass()
java
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}在抽象类ClassLoader中并没有实现具体的findClass()方法,我们需要在子类中找到具体实现。
**URLClassLoader.loadClass()**源码实现(删减版)
java
protected Class<?> findClass(final String name)
{
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
final Class<?> result = defineClass(name, res);
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}在上面的实现中,主要做了三件事:
- 将类的全限定路径转换为文件路径
path; - 通过文件路径
path将类字节码文件加载为内存中的资源(字节数组) - 调用
defineClass()方法将字节数组转换为类Class对象;
ClassLoader.defineClass()
java
protected final Class<?> defineClass(String name,
byte[] b,
int off,
int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}其中的关键点是defineClass1,这是一个本地方法,将字节数组转换为Class对象。
java
static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len, ProtectionDomain pd, String source);注意,在preDefineClass()方法中校验了类名以及包名,例如包名不能以java开头。
以上就是ClassLoader中的一些方法。在AppClassLoader中重写了loadClass()方法,但是也没有破坏双亲委派机制。
3.5 自定义类加载器
在Java中,我们可以通过继承java.lang.ClassLoader的方式定义自己的类加载器,并重写findClass()方法:
java
public class MyClassLoader extends ClassLoader{
private static final String CLASS_SUFFIX = ".class";
// 类字节码文件存放的文件路径
private String basePath;
public MyClassLoader(String basePath){
this.basePath = basePath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
/**
* 从字节文件中加载到内存字节数组中
* @param className 类名
* @return 字节数组
* @throws IOException
*/
private byte[] loadClassData(String className) throws IOException {
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try {
String newClassName = className.replaceAll("\\.", "/");
String path = this.basePath + newClassName + CLASS_SUFFIX;
fis = new FileInputStream(path);
bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = fis.read(buffer);
while (len != -1){
bos.write(buffer, 0, len);
len = fis.read(buffer);
}
byte[] data = bos.toByteArray();
return data;
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
if(fis != null){
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if(bos != null) {
try {
bos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}然后我们就可以使用自定义类了:
java
String basePath = "/Users/xxx/src/";
MyClassLoader myClassLoader = new MyClassLoader(basePath);
Class<?> student = myClassLoader.loadClass("com.t3.Student");
System.out.println(student);只要文件路径下/Users/xxx/src/com/t3/存在着Student.class文件,那么上面的自定义类加载器就可以成功加载类。
注意点:
- 为什么不重写
loadClass()方法?为了不打破双亲委派机制,所以建议重写findClass()方法; - 自定义的类加载器的父加载器是系统类加载器;
3.6 打破双亲委派机制的情况
在JDK 9模块化出现之前,在JDK中存在三种破坏双亲委派机制的情况:
JDK 1.2之前 :由于双亲委派机制是在JDK 1.2提出的,所以在JDK 1.2之前自然不存在双亲委派机制;
线程上下文类加载器(TCCL,Thread Context Class Loader):线程上下文类加载器是与当前线程关联的类加载器。默认情况下,一个新的线程会继承创建它的父线程的上下文类加载器。如果最开始创建的是主线程,那么它的上下文类加载器通常是系统类加载器(Application ClassLoader)。我们可以理解线程上下文类加载器就是系统类加载器。当我们加载某个JDK中的类时(例如
java.sql.DriverManager),由于双亲委派机制的存在,最终是由启动类加载器加载DriverManager类,由于DriverManager依赖于具体的第三方实现,例如org.postgresql.Driver,所以启动器类加载器也需要加载org.postgresql.Driver类,但是org.postgresql.Driver不属于Java核心类,启动器无法加载,此时通过线程上下文类加载器获取系统类加载器,由系统类加载器来加载第三方实现类。流程如下:
代码及源码解释如下:
首先在我们的代码中加载
DriverManager:javaConnection connection = DriverManager.getConnection("");在
DriverManager类初始化方法中,代码如下:javapublic class DriverManager { static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() { //省略一些代码... AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { // 关键代码 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); // 省略一些代码... } }AccessController.doPrivileged(...): 这段代码在特权上下文中运行,允许执行一些受限的操作,例如访问系统属性和使用ServiceLoader在这段代码中,使用了SPI机制
ServiceLoader.load(Driver.class):javapublic static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }在第2行获取了线程上下文类加载器,即系统类加载器。后续跟踪第3行代码,最终源码如下:
javaprivate class LazyIterator implements Iterator<S> { Class<S> service; ClassLoader loader; Enumeration<URL> configs = null; Iterator<String> pending = null; String nextName = null; private LazyIterator(Class<S> service, ClassLoader loader) { this.service = service; this.loader = loader; } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction<S> action = new PrivilegedAction<S>() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } }即
Iterator<Driver> driversIterator = loadedDrivers.iterator();返回的具体实现类是LazyIterator,并且LazyIterator中的类加载器是系统类加载器。当调用迭代器的next()方法时,会调用nextService()方法,在上面的第21行代码使用系统类加载器加载第三方的具体实现类,例如org.postgresql.Driver。以上就是如何利用线程上下文类加载器破坏双亲委派机制的情况。
OSGi-模块化:OSGi 的核心目标是提供一个高度模块化的 Java 环境。每个模块(在 OSGi 中称为 Bundle)都应该是独立的、可插拔的,并且可以拥有自己的依赖。
OSGi 为每个 Bundle 创建了一个独立的类加载器。这些类加载器形成了一个复杂的图状结构,而不是简单的树状结构。OSGi 的类加载遵循以下原则,这与标准的双亲委派模型有显著不同:
- Bundle 私有命名空间: 每个 Bundle 都有自己的类加载器,形成了一个私有的命名空间。这意味着同一个类名在不同的 Bundle 中可以代表不同的类。
- 显式的依赖管理: Bundle 通过
Import-Package和Export-Package清单头显式地声明它们依赖哪些其他 Bundle 导出的包,以及它们自己导出哪些包给其他 Bundle 使用。 - 优先查找导入的包: 当一个 Bundle 的类加载器需要加载一个类时,它首先会检查该类是否属于自己导入的包。如果是,它会尝试从导出该包的 Bundle 中加载这个类,而不会首先委托给它的父类加载器。
- 父类加载器作为最后的手段: 只有当在导入的包中找不到所需的类时,Bundle 的类加载器才会委托给它的父类加载器(通常是创建该 Bundle 类加载器的 Framework ClassLoader)。
- Framework ClassLoader: OSGi 框架本身也有一个类加载器,负责加载框架自身的类和启动时的一些核心 Bundle。Bundle 的类加载器通常会委托给 Framework ClassLoader 作为其父加载器。
关键在于 优先查找导入的包 这一步。在标准的双亲委派模型中,加载请求总是先向上委托给父类加载器。但在 OSGi 中,一个 Bundle 的类加载器会首先查看它所依赖的其他 Bundle 是否导出了所需的类所在的包。如果找到了,它会直接从那个导出的 Bundle 加载类,而不会先去询问它的父类加载器是否已经加载过这个类。
4. Java 9中类加载器的调整
JDK 9 为了模块化的支持,对双亲委派模式做了一些改动:
扩展类加载器被平台类加载器(Platform ClassLoader)取代。 JDK 9 时基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数十个 JMOD 文件), 其中的 Java 类库就已天然地满足了可扩展的需求,那自然无须再保留
<JAVA_HOME>\lib\ext目录,此前使用这个目录或者 java.ext.dirs 系统变量来扩展 JDK 功能的机制已经没有继续存在的价值了。平台类加载器和应用程序类加载器都不再继承自 java.net.URLClassLoader。 现在启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader。
如果有程序直接依赖了这种继承关系,或者依赖了 URLClassLoader 类的特定方法,那代码很可能会在 JDK 9 及更高版本的 JDK 中崩溃。
启动类加载器现在是在 Java 虚拟机内部和 Java 类库共同协作实现的类加载器(以前是 C++实现)。 为了与之前的代码保持兼容,所有在获取启动类加载器的场景(譬如 Object.class.getClassLoader)中仍然会返回 null 来代替,而不会得到 BootClassLoader 的实例。
类加载的委派关系也发生了变动。 当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。
在 Java 模块化系统明确规定了三个类加载器负责各自加载的模块:
- 启动类加载器负责加载的模块
java
java.base java.security.sasl
java.datatransfer java.xml
java.desktop jdk.httpserver
java.instrument jdk.internal.vm.ci
java.logging jdk.management
java.management jdk.management.agent
java.management.rmi jdk.naming.rmi
java.naming jdk.net
java.prefs jdk.sctp
java.rmi jdk.unsupported- 平台类加载器负责加载的模块
java
java.activation* jdk.accessibility
java.compiler* jdk.charsets
java.corba* jdk.crypto.cryptoki
java.scripting jdk.crypto.ec
java.se jdk.dynalink
java.se.ee jdk.incubator.httpclient
java.security.jgss jdk.internal.vm.compiler*
java.smartcardio jdk.jsobject
java.sql jdk.localedata
java.sql.rowset jdk.naming.dns
java.transaction* jdk.scripting.nashorn
java.xml.bind* jdk.security.auth
java.xml.crypto jdk.security.jgss
java.xml.ws* jdk.xml.dom
java.xml.ws.annotation* jdk.zipfs- 应用程序类加载器负责加载的模块
java
jdk.aot jdk.jdeps
jdk.attach jdk.jdi
jdk.compiler jdk.jdwp.agent
jdk.editpad jdk.jlink
jdk.hotspot.agent jdk.jshell
jdk.internal.ed jdk.jstatd
jdk.internal.jvmstat jdk.pack
jdk.internal.le jdk.policytool
jdk.internal.opt jdk.rmic
jdk.jartool jdk.scripting.nashorn.shell
jdk.javadoc jdk.xml.bind*
jdk.jcmd jdk.xml.ws*
jdk.jconsole在Java 9 模块化出现之后,双亲委派机制有了改变:在委派给父加载器前,先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。
