# 类加载详解
# class 文件内容
JAVA 代码不能直接运行,需要先编译为 .class
文件才可以,如下代码
package cn.mrcode.demo.boodadmin.jvm;
/**
* @author mrcode
* @date 2023/3/14 21:12
*/
public class JVMTest2 {
private static final String CONST_FIELD = "AAA";
private static String staticField;
private String field;
public String add() {
return staticField + field;
}
public static void main(String[] args) {
new JVMTest2().add();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 运行 javac 编译之后,就能得到一个 JVMTest2.class 文件
$ javac JVMTest2.java
2
3
编译出来的文件是无法直接打开查看的,可以使用反编译工具反编译后查看, 比如 jdk 自带的 javap
# 查看 javap 的用法
mrcode-2 boodadmin/jvm » javap -help 1 ↵
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
// 显示附加信息和显示所有类和成员 使用 javap
mrcode-2 boodadmin/jvm » javap -v -p JVMTest2
警告: 二进制文件JVMTest2包含cn.mrcode.demo.boodadmin.jvm.JVMTest2
// 描述信息:最后修改时间、MD5 校验值,从那个文件编译出来的
Classfile /Users/mrcode/Documents/GitHub/demo/bood-admin/src/main/java/cn/mrcode/demo/boodadmin/jvm/JVMTest2.class
Last modified 2023-3-14; size 667 bytes
MD5 checksum b35f72d9ae5ddeb2f9882e0b8644964e
Compiled from "JVMTest2.java"
// 描述信息,使用什么版本的 jdk 编译出来的
public class cn.mrcode.demo.boodadmin.jvm.JVMTest2
minor version: 0
major version: 52 // 52 表示 JDK8
flags: ACC_PUBLIC, ACC_SUPER
// 常量池
Constant pool:
#1 = Methodref #11.#28 // java/lang/Object."<init>":()V
#2 = Class #29 // java/lang/StringBuilder
#3 = Methodref #2.#28 // java/lang/StringBuilder."<init>":()V
#4 = Fieldref #8.#30 // cn/mrcode/demo/boodadmin/jvm/JVMTest2.staticField:Ljava/lang/String;
#5 = Methodref #2.#31 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#6 = Fieldref #8.#32 // cn/mrcode/demo/boodadmin/jvm/JVMTest2.field:Ljava/lang/String;
#7 = Methodref #2.#33 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#8 = Class #34 // cn/mrcode/demo/boodadmin/jvm/JVMTest2
#9 = Methodref #8.#28 // cn/mrcode/demo/boodadmin/jvm/JVMTest2."<init>":()V
#10 = Methodref #8.#35 // cn/mrcode/demo/boodadmin/jvm/JVMTest2.add:()Ljava/lang/String;
#11 = Class #36 // java/lang/Object
#12 = Utf8 CONST_FIELD
#13 = Utf8 Ljava/lang/String;
#14 = Utf8 ConstantValue
#15 = String #37 // AAA
#16 = Utf8 staticField
#17 = Utf8 field
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 add
#23 = Utf8 ()Ljava/lang/String;
#24 = Utf8 main
#25 = Utf8 ([Ljava/lang/String;)V
#26 = Utf8 SourceFile
#27 = Utf8 JVMTest2.java
#28 = NameAndType #18:#19 // "<init>":()V
#29 = Utf8 java/lang/StringBuilder
#30 = NameAndType #16:#13 // staticField:Ljava/lang/String;
#31 = NameAndType #38:#39 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#32 = NameAndType #17:#13 // field:Ljava/lang/String;
#33 = NameAndType #40:#23 // toString:()Ljava/lang/String;
#34 = Utf8 cn/mrcode/demo/boodadmin/jvm/JVMTest2
#35 = NameAndType #22:#23 // add:()Ljava/lang/String;
#36 = Utf8 java/lang/Object
#37 = Utf8 AAA
#38 = Utf8 append
#39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#40 = Utf8 toString
{
// 字段信息
private static final java.lang.String CONST_FIELD;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: String AAA
private static java.lang.String staticField;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC
private java.lang.String field;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE
// 方法信息
public cn.mrcode.demo.boodadmin.jvm.JVMTest2();
descriptor: ()V // 描述符
flags: ACC_PUBLIC // 访问权限
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
public java.lang.String add();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: getstatic #4 // Field staticField:Ljava/lang/String;
10: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
13: aload_0
14: getfield #6 // Field field:Ljava/lang/String;
17: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
23: areturn
LineNumberTable:
line 13: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: new #8 // class cn/mrcode/demo/boodadmin/jvm/JVMTest2
3: dup
4: invokespecial #9 // Method "<init>":()V
7: invokevirtual #10 // Method add:()Ljava/lang/String;
10: pop
11: return
LineNumberTable:
line 17: 0
line 18: 11
}
SourceFile: "JVMTest2.java"
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
一个 class 文件大致有几部分:描述信息、常量池、字段信息、方法信息
这些信息其实很难看明白是什么,不过可以借助 JDK 官方的 JVM 规范文档来查看 (opens new window),比如方法中的 invokespecial
指令,直接在文档中搜索
# 拓展阅读
Class 文件是 Java 虚拟机可执行代码的一种格式,它包含了Java 类或接口的结构信息,具体包括以下内容:
- 魔数(Magic Number):Class 文件的前 4 个字节是魔数,用于标识该文件是否为有效的 Class 文件,其值为 0xCAFEBABE。
- 版本信息(Version Information):Class 文件的接下来 2 个字节表示 Class 文件的版本号,分别是次版本号和主版本号。
- 常量池(Constant Pool):接下来的部分是常量池,它是 Class 文件的重要组成部分,包含了类或接口的常量池信息,例如字符串、字段和方法的符号引用、字面量等。常量池的数量和内容由编译器生成。
- 访问标志(Access Flags):Class 文件的接下来 2 个字节表示访问标志,用于描述该类的访问级别和特性,例如 public、final、abstract 等。
- 类索引、父类索引和接口索引集合(Class Index, Superclass Index and Interface Indexes):接下来 4 个字节分别表示该类在常量池中的索引、父类在常量池中的索引和接口在常量池中的索引集合。
- 字段表(Fields):接下来的部分包含了该类的字段信息,例如字段名、修饰符、类型和初始值等。
- 方法表(Methods):接下来的部分包含了该类的方法信息,例如方法名、参数、修饰符、字节码等。
- 属性表(Attributes):接下来的部分包含了附加于该类、字段或方法上的属性信息,例如源文件名、行号表、注解等。
总之,Class 文件是 Java 类或接口在编译后的二进制文件,它包含了类或接口的结构信息,Java 虚拟机可以通过解析该文件来执行 Java 程序。
# 类加载过程
这是一个常规的类加载过程,不一定都是这个过程 类加载是 Java 程序运行的基础,其过程包括以下步骤:
# 1. 加载(Loading):
类加载的第一步是通过类加载器将字节码加载到内存中。类加载器通常从文件系统、网络或其他来源获取字节码。
- 读取类的二进制流
- 转为方法区数据结构,并存储到方法区
- 在 Java 堆中产生 java.lang.Class 对象
# 2. 验证(Verification):
在类加载过程中,Java 虚拟机会对字节码进行验证,以确保其符合 Java 虚拟机规范和安全要求。验证的过程包括:
- 文件格式验证(以下是一部分)
- 是否以 0xCAFEBABE 开头、
- 版本号是否合理
- 元数据验证(以下是一部分)
- 是否有父类
- 是否继承了 final 类(final 类不能被继承,继承的话就是有问题)
- 非抽象类是否实现了所有抽象方法
- 字节码验证
- 运行检查
- 栈数据类型和操作码操作参数吻合(比如栈空间只有 2 字节,但其实却大于 2 字节,此时就认为这个字节码是有问题的)
- 跳转指令指向合理的位置
- 符号引用验证
- 常量池中描述类是否存在
- 访问的方法或字段是否存在,且有足够的权限
如果你认为你的代码是安全的,可以通过 -Xverify:none
参数关闭验证;比如可以在 idea 的 vm 参数文件中启用这个选项,可以加快编译速度
可以使用十六进制编辑器打开文件,比如 sublime 编辑器,打开后如下所示
sublime text 打开的 JVMTest2.class 文件内容```bash
cafe babe 0000 0034 0029 0a00 0b00 1c07
001d 0a00 0200 1c09 0008 001e 0a00 0200
1f09 0008 0020 0a00 0200 2107 0022 0a00
0800 1c0a 0008 0023 0700 2401 000b 434f
4e53 545f 4649 454c 4401 0012 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 0100
0d43 6f6e 7374 616e 7456 616c 7565 0800
2501 000b 7374 6174 6963 4669 656c 6401
0005 6669 656c 6401 0006 3c69 6e69 743e
0100 0328 2956 0100 0443 6f64 6501 000f
4c69 6e65 4e75 6d62 6572 5461 626c 6501
0003 6164 6401 0014 2829 4c6a 6176 612f
6c61 6e67 2f53 7472 696e 673b 0100 046d
6169 6e01 0016 285b 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 2956 0100 0a53
6f75 7263 6546 696c 6501 000d 4a56 4d54
6573 7432 2e6a 6176 610c 0012 0013 0100
176a 6176 612f 6c61 6e67 2f53 7472 696e
6742 7569 6c64 6572 0c00 1000 0d0c 0026
0027 0c00 1100 0d0c 0028 0017 0100 2563
6e2f 6d72 636f 6465 2f64 656d 6f2f 626f
6f64 6164 6d69 6e2f 6a76 6d2f 4a56 4d54
6573 7432 0c00 1600 1701 0010 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 0100 0341
4141 0100 0661 7070 656e 6401 002d 284c
6a61 7661 2f6c 616e 672f 5374 7269 6e67
3b29 4c6a 6176 612f 6c61 6e67 2f53 7472
696e 6742 7569 6c64 6572 3b01 0008 746f
5374 7269 6e67 0021 0008 000b 0000 0003
001a 000c 000d 0001 000e 0000 0002 000f
000a 0010 000d 0000 0002 0011 000d 0000
0003 0001 0012 0013 0001 0014 0000 001d
0001 0001 0000 0005 2ab7 0001 b100 0000
0100 1500 0000 0600 0100 0000 0700 0100
1600 1700 0100 1400 0000 3000 0200 0100
0000 18bb 0002 59b7 0003 b200 04b6 0005
2ab4 0006 b600 05b6 0007 b000 0000 0100
1500 0000 0600 0100 0000 0d00 0900 1800
1900 0100 1400 0000 2800 0200 0100 0000
0cbb 0008 59b7 0009 b600 0a57 b100 0000
0100 1500 0000 0a00 0200 0000 1100 0b00
1200 0100 1a00 0000 0200 1b
:::warning
细节非常的多,建议不用过多的关注,只要知道是验证 java 类就行了
:::
### 3. 准备(Preparation):
在准备阶段,Java 虚拟机为类的静态变量分配内存,并设置默认初始值。静态变量的初始化不包括执行类构造函数。
```bash
// final static 修饰的变量,直接赋值为用户定义的值,比如
private final static int value=123 // 直接赋值 123
// 对于 static 变量,如
private static int value=123 // 此时 value=0,而不是 123
2
3
4
5
6
7
8
9
10
# 4. 解析(Resolution):
解析阶段将常量池中的符号引用替换为直接引用。常量池中的符号引用是一种直接引用的符号名称,例如类名、方法名和字段名。Java 虚拟机在解析阶段将这些符号引用转换为对应的直接引用,以便在运行时快速访问类、方法和字段。 比如下图的 method、field 引用就是符号引用,解析的时候把他们转换成实际的内存地址引用
# 5. 初始化(Initialization):
在初始化阶段,Java 虚拟机执行类的构造函数,并初始化类的静态变量。初始化阶段是类加载的最后一步。如果类没有被初始化,则不会执行其中的任何代码。
详细如下:
执行 <clinit>
方法,clinit 方法由编译器自动收集类里面所有静态变量的赋值动作以及静态语句块合并而成,也叫类构造器方法:
- 初始化的顺序和源文件中的顺序一致
比如下面的代码,最终输出是 1,保证线程的安全性,JVM 会将 静态变量 和 静态代码块合并成一个 <clinit>
里面执行
package cn.mrcode.demo.boodadmin.jvm;
/**
* @author mrcode
* @date 2023/3/14 22:08
*/
public class JVMTest3 {
static int i = 0;
static {
i = 1;
}
public static void main(String[] args) {
System.out.println(i);
}
}
// 如果把他们的顺序互换,同样不会混乱,最终结果是 0
static {
i = 1;
}
static int i = 0;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 子类的
<clinit>
被调用前,会先调用父类的<clinit>
package cn.mrcode.demo.boodadmin.jvm;
import java.sql.SQLOutput;
/**
* @author mrcode
* @date 2023/3/14 22:14
*/
public class JVMTest5 {
static {
System.out.println("JVMTest5 静态块");
}
{
System.out.println("JVMTest5 构造块");
}
public JVMTest5() {
System.out.println("JVMTest5 构造方法");
}
public static void main(String[] args) {
//
new Sub();
}
}
class Super {
static {
System.out.println("Super 静态块");
}
public Super() {
System.out.println("Super 构造块");
}
}
class Sub extends Super {
static {
System.out.println("Sub 静态块");
}
public Sub() {
System.out.println("Sub 构造方法");
}
{
System.out.println("Sub 构造块");
}
}
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
输出如下
# 只输出了 静态块,因为我们没有 new JVMTest5 类
JVMTest5 静态块
# 先输出了 Super 静态块是因为,new Sub,要先调用父类的 <clinit>
Super 静态块
# 然后是子类的静态块
Sub 静态块
# 静态块完成后,再父类的构造块
Super 构造块
# 然后再是子类的
Sub 构造块
Sub 构造方法
2
3
4
5
6
7
8
9
10
11
- JVM 会保证 clinit 方法的线程安全性
# 6. 使用(Usage):
在类加载完成后,Java程序可以使用该类创建对象、调用方法和访问字段。 总体来说,类加载是一个动态的过程,根据程序的需要在需要时进行类的加载、验证、准备、解析和初始化。
# 类加载过程/使用/卸载拓展阅读
### 1. 加载(Loading) 加载(Loading)是类加载的第一步,在此步骤中,Java 虚拟机使用类加载器将字节码加载到内存中,为后续的验证、准备、解析和初始化步骤做准备。下面是加载步骤的详细说明:
- 类的加载器接收类的二进制字节流,通常是通过读取存储在文件系统、JAR 包或网络中的类文件来获得。
- 类加载器根据字节流创建一个 Java.lang.Class 对象,并将其保存在 Java 虚拟机的方法区中。类的加载器将该类的全限定名作为 Java.lang.Class 对象的一个标识符,以便在 Java 虚拟机中唯一地标识该类。
- 类加载器在加载类时,可能需要加载其他类或接口,这些被引用的类或接口也会通过类加载器进行加载。
- 类加载器在加载类时,会使用双亲委派模型,即将加载请求委派给父类加载器来处理。如果父类加载器无法加载该类,则该类的加载器会尝试自己加载该类。如果该类的加载器无法加载该类,则会抛出ClassNotFoundException 异常。
总之,加载是类加载过程中的第一步,它是将字节码加载到内存中,为后续的验证、准备、解析和初始化步骤做准备。加载过程使用类加载器,遵循双亲委派模型,保证 Java 程序的安全性和稳定性。
# 2. 验证(Verification)
验证(Verification)是类加载的第二步,在此步骤中,Java 虚拟机会对已经加载到内存中的字节码进行验证,以确保它符合 Java 虚拟机规范和安全性要求。下面是验证步骤的详细说明:
- 文件格式验证:Java 虚拟机首先验证字节码文件是否符合 Class 文件格式规范,包括文件头、常量池、类字段和方法的数量、类型和修饰符等。
- 元数据验证:Java 虚拟机会检查字节码文件中的元数据信息是否正确,例如父类、实现的接口、字段和方法的名称、类型和修饰符等。
- 字节码验证:Java 虚拟机会检查字节码是否符合 Java 虚拟机规范,例如跳转指令、异常处理、类型转换等,以确保它是合法且安全的。
- 符号引用验证:Java 虚拟机会检查字节码中的符号引用是否正确,例如字段和方法的引用是否存在、是否具有正确的访问权限等。
验证步骤是确保 Java 程序在执行时不会出现安全问题和错误的关键步骤。如果字节码无法通过验证,则会抛出 VerifyError 异常,此时 Java 虚拟机将无法继续执行该程序。总之,验证是类加载过程中的第二步,它用于确保字节码符合 Java 虚拟机规范和安全性要求,保障 Java 程序的正确性和安全性。
# 3. 准备(Preparation)
准备(Preparation)是类加载的第三步,在此步骤中,Java 虚拟机为类变量(静态变量)分配内存,并设置初始值。下面是准备步骤的详细说明:
- 在方法区中为类变量分配内存。类变量分配在方法区中,与 Java 对象不同,Java 对象分配在堆中。
- 设置默认值。对于类变量,Java 虚拟机会根据其类型设置默认值。例如,对于整型变量,Java 虚拟机会将其默认值设置为 0,对于布尔型变量,Java 虚拟机会将其默认值设置为 false。
准备步骤在类加载的初始化阶段之前完成,它确保了类变量的初始值是正确的。总之,准备是类加载过程中的第三步,它为类变量分配内存并设置默认值,为后续的初始化步骤做准备。
# 4. 解析(Resolution)
解析(Resolution)是类加载的第四个步骤,在此步骤中,Java 虚拟机将符号引用转换为直接引用。下面是解析步骤的详细说明:
- 类或接口的解析:Java 虚拟机会尝试将常量池中的类或接口符号引用解析为直接引用,即找到该类或接口在方法区中的实际地址。
- 字段解析:Java 虚拟机会将常量池中的字段符号引用解析为直接引用,即找到该字段在对象或类中的实际地址。
- 方法解析:Java 虚拟机会将常量池中的方法符号引用解析为直接引用,即找到该方法在方法区中的实际地址。
解析步骤是将符号引用转换为直接引用的过程,它将类、字段和方法符号引用解析为实际地址,使得 Java 程序可以正常执行。如果解析失败,则会抛出 NoSuchFieldError 或 NoSuchMethodError 异常。总之,解析是类加载过程中的第四步,它将常量池中的符号引用解析为直接引用,使得 Java 程序可以正常执行。
# 5. 初始化(Initialization)
初始化(Initialization)是类加载的最后一步,在此步骤中,Java 虚拟机执行类的初始化代码,包括静态初始化器和静态变量的赋值操作。下面是初始化步骤的详细说明:
- 执行静态初始化器:如果类中包含静态初始化器(使用 static 代码块定义),Java 虚拟机会在类初始化时执行该代码块。静态初始化器可以用于对静态变量进行赋值或执行一些初始化操作。
- 执行静态变量赋值操作:Java 虚拟机会按照代码中静态变量定义的顺序,依次执行静态变量的赋值操作。如果一个静态变量被赋值为另一个静态变量的值,那么被赋值的变量需要等待另一个变量的赋值完成后才能执行。
初始化步骤是类加载的最后一步,它执行静态初始化器和静态变量的赋值操作,完成类的初始化。如果初始化过程中发生了异常,则会抛出 ExceptionInInitializerError 异常,并且该类将不会被正常使用。总之,初始化是类加载过程中的最后一步,它执行静态初始化器和静态变量的赋值操作,完成类的初始化。
# 6. 使用(Usage)
使用(Usage)步骤是类加载完成后,Java 程序可以正常使用该类的阶段。在使用阶段,Java 虚拟机可以通过类的对象或类的静态变量来访问该类的方法和字段。下面是使用步骤的详细说明:
- 创建类的实例:在使用阶段,可以通过 new 操作符来创建类的实例,并调用实例方法。
- 访问静态变量:在使用阶段,可以通过类名来访问类的静态变量,并调用类的静态方法。
使用步骤是类加载完成后,Java 程序可以正常使用该类的阶段。在使用阶段,Java 虚拟机可以通过类的对象或类的静态变量来访问该类的方法和字段。使用步骤是 Java 程序的最终目的,它通过访问类的实例方法和静态变量,实现了 Java 程序的功能。
# 7. 类卸载
类卸载是指在 Java 虚拟机中,当一个类不再被引用时,将其从方法区中卸载的过程。类卸载是 Java 虚拟机内存管理的一部分,它确保不再使用的类所占用的内存可以被垃圾回收器回收。 Java 虚拟机在类卸载时,会执行以下操作:
- 判断类的实例是否存在:如果一个类的实例仍然存在,那么该类就不能被卸载。因此,在进行类卸载前,Java 虚拟机会先判断该类的实例是否存在。
- 判断类的类加载器是否存在:如果一个类的类加载器仍然存在,那么该类也不能被卸载。因为类加载器加载的类和类加载器本身是有生命周期的,如果一个类加载器被卸载,那么由该类加载器加载的类也应该被卸载。
- 执行类的卸载:如果一个类的实例不存在,并且它的类加载器也不存在,那么该类就可以被卸载了。在卸载类之前,Java 虚拟机会先执行该类的
finalize()
方法,进行资源的清理和释放操作。
需要注意的是,类卸载并不是 Java 虚拟机的必须操作。Java 虚拟机规范并没有要求虚拟机必须支持类的卸载。不同的 Java 虚拟机实现可能会有不同的类卸载策略,或者干脆不支持类卸载。
总之,类卸载是指在 Java 虚拟机中,当一个类不再被引用时,将其从方法区中卸载的过程。Java 虚拟机在类卸载时,会先判断类的实例是否存在,然后判断类的类加载器是否存在,最后执行类的卸载,并在卸载前执行该类的finalize()
方法,进行资源的清理和释放操作。