JVM - 类加载机制
# 类加载机制
写好的源代码,需要编译后加载到虚拟机才能运行。java 源文件编译成 class 文件后,jvm 通过类加载器把 class 文件加载到虚拟机,然后经过类连接(类连接又包括验证、准备、解析三个阶段),最后经过初始化,字节码就可以被解释执行了。对于一些热点代码,虚拟机还存在一道即时编译,会把字节码编译成本地平台相关的机器码,以提高热点代码的执行效率。
装载、验证、准备、初始化这几个阶段的顺序是确定的,类型的加载过程必须按照这种顺序开始,而解析阶段可以在初始化阶段之后再开始,一般是在第一次使用到这个对象时才会开始解析。这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段,比如发现引用了另一个类,那么就会先触发另一个类的加载过程。
接下来通过如下类和代码来详细分析下类加载的过程:
package com.lyyzoo.jvm.test01;
public class Person<T> {
public static final String SEX_MAN = "Male";
public static final String SEX_WOMAN = "Female";
static {
System.out.println("Person static init");
System.out.println("SEX_MAN: " + SEX_MAN);
}
public void sayHello(T str) {
System.out.println("Person say hello: " + str);
}
}
/////////////////////////////////////////////////////////////////////
package com.lyyzoo.jvm.test01;
import java.io.Serializable;
public class User extends Person<String> implements Serializable {
private static final long serialVersionUID = -4482416396338787067L;
// 静态常量
public static final String FIELD_NAME = "username";
public static final int AGE_MAX = 100;
// 静态变量
private static String staticName = "Rambo";
private static int staticAge = 20;
// 类属性
private String name = "兰博";
private int age = 25;
// 静态代码块
static {
System.out.println("user static init");
System.out.println("staticName=" + staticName);
System.out.println("staticAge=" + staticAge);
}
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
// 实例方法
public void printInfo() {
System.out.println("name:" + name + ", age:" + age);
}
// 静态方法
public static void staticPrintInfo() {
System.out.println("FIELD_NAME:" + FIELD_NAME + ", AGE_MAX:" + AGE_MAX);
}
// 泛型方法重载
@Override
public void sayHello(String str) {
super.sayHello(str);
System.out.println("User say hello: " + str);
}
// 方法将抛出异常
public int willThrowException() {
int i = 0;
try {
int r = 10 / i;
return r;
} catch (Exception e) {
System.out.println("catch exception");
return i;
} finally {
System.out.println("finally handle");
}
}
}
/////////////////////////////////////////////////////////////////////
package com.lyyzoo.jvm.test01;
public class Main {
public static void main(String[] args) {
System.out.println("FIELD_NAME: " + User.FIELD_NAME);
User.staticPrintInfo();
User user = new User();
user.printInfo();
}
}
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
# 类编译和 Class 文件结构
# class 文件结构
*.java
文件被编译成 *.class
文件的过程,这个编译一般称为前端编译,主要使用 javac 来完成前端编译。Java class文件是8位字节的二进制流,数据项按顺序存储在class文件中,相邻的项之间没有任何间隔,这样可以使class文件紧凑。class 文件主要包含 版本信息、常量池、类型索引、字段表、方法表、属性表
等信息。
将 User 类编译成 class 文件后,再通过 javap
反编译 class 文件,可以看到一个 class 文件大体包含的结构:
说明:用“【】”标识的是手动添加的注释
Mechrevo@hello-world MINGW64 /e/repo-study/test-concurrent/target/classes/com/lyyzoo/jvm/test01
【javap -v 命令反编译 Class】
$ javap -v User.class
Classfile /E:/repo-study/test-concurrent/target/classes/com/lyyzoo/jvm/test01/User.class
Last modified 2020-9-3; size 2389 bytes
【魔数】
MD5 checksum ec5a961c2a46926522bafddcb3204fb9
Compiled from "User.java"
public class com.lyyzoo.jvm.test01.User extends com.lyyzoo.jvm.test01.Person<java.lang.String> implements java.io.Serializable
【版本号】
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
【常量池】
Constant pool:
#1 = Methodref #29.#76 // com/lyyzoo/jvm/test01/Person."<init>":()V
#2 = String #77 // 兰博
#3 = Fieldref #14.#78 // com/lyyzoo/jvm/test01/User.name:Ljava/lang/String;
#4 = Fieldref #14.#79 // com/lyyzoo/jvm/test01/User.age:I
#5 = Fieldref #80.#81 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Class #82 // java/lang/StringBuilder
#7 = Methodref #6.#76 // java/lang/StringBuilder."<init>":()V
#8 = String #83 // name:
#9 = Methodref #6.#84 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#10 = String #85 // , age:
#11 = Methodref #6.#86 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#12 = Methodref #6.#87 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#13 = Methodref #88.#89 // java/io/PrintStream.println:(Ljava/lang/String;)V
#14 = Class #90 // com/lyyzoo/jvm/test01/User
#15 = String #91 // FIELD_NAME:username, AGE_MAX:100
#16 = Methodref #29.#92 // com/lyyzoo/jvm/test01/Person.sayHello:(Ljava/lang/Object;)V
#17 = String #93 // User say hello:
#18 = String #94 // finally handle
#19 = Class #95 // java/lang/Exception
#20 = String #96 // catch exception
#21 = Class #97 // java/lang/String
#22 = Methodref #14.#98 // com/lyyzoo/jvm/test01/User.sayHello:(Ljava/lang/String;)V
#23 = String #99 // Rambo
#24 = Fieldref #14.#100 // com/lyyzoo/jvm/test01/User.staticName:Ljava/lang/String;
#25 = Fieldref #14.#101 // com/lyyzoo/jvm/test01/User.staticAge:I
#26 = String #102 // user static init
#27 = String #103 // staticName=
#28 = String #104 // staticAge=
#29 = Class #105 // com/lyyzoo/jvm/test01/Person
#30 = Class #106 // java/io/Serializable
#31 = Utf8 serialVersionUID
#32 = Utf8 J
#33 = Utf8 ConstantValue
#34 = Long -4482416396338787067l
#36 = Utf8 FIELD_NAME
#37 = Utf8 Ljava/lang/String;
#38 = String #107 // username
#39 = Utf8 AGE_MAX
#40 = Utf8 I
#41 = Integer 100
#42 = Utf8 staticName
#43 = Utf8 staticAge
#44 = Utf8 name
#45 = Utf8 age
#46 = Utf8 <init>
#47 = Utf8 ()V
#48 = Utf8 Code
#49 = Utf8 LineNumberTable
#50 = Utf8 LocalVariableTable
#51 = Utf8 this
#52 = Utf8 Lcom/lyyzoo/jvm/test01/User;
#53 = Utf8 (Ljava/lang/String;I)V
#54 = Utf8 MethodParameters
#55 = Utf8 printInfo
#56 = Utf8 staticPrintInfo
#57 = Utf8 sayHello
#58 = Utf8 (Ljava/lang/String;)V
#59 = Utf8 str
#60 = Utf8 willThrowException
#61 = Utf8 ()I
#62 = Utf8 r
#63 = Utf8 e
#64 = Utf8 Ljava/lang/Exception;
#65 = Utf8 i
#66 = Utf8 StackMapTable
#67 = Class #90 // com/lyyzoo/jvm/test01/User
#68 = Class #95 // java/lang/Exception
#69 = Class #108 // java/lang/Throwable
#70 = Utf8 (Ljava/lang/Object;)V
#71 = Utf8 <clinit>
#72 = Utf8 Signature
#73 = Utf8 Lcom/lyyzoo/jvm/test01/Person<Ljava/lang/String;>;Ljava/io/Serializable;
#74 = Utf8 SourceFile
#75 = Utf8 User.java
#76 = NameAndType #46:#47 // "<init>":()V
#77 = Utf8 兰博
#78 = NameAndType #44:#37 // name:Ljava/lang/String;
#79 = NameAndType #45:#40 // age:I
#80 = Class #109 // java/lang/System
#81 = NameAndType #110:#111 // out:Ljava/io/PrintStream;
#82 = Utf8 java/lang/StringBuilder
#83 = Utf8 name:
#84 = NameAndType #112:#113 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#85 = Utf8 , age:
#86 = NameAndType #112:#114 // append:(I)Ljava/lang/StringBuilder;
#87 = NameAndType #115:#116 // toString:()Ljava/lang/String;
#88 = Class #117 // java/io/PrintStream
#89 = NameAndType #118:#58 // println:(Ljava/lang/String;)V
#90 = Utf8 com/lyyzoo/jvm/test01/User
#91 = Utf8 FIELD_NAME:username, AGE_MAX:100
#92 = NameAndType #57:#70 // sayHello:(Ljava/lang/Object;)V
#93 = Utf8 User say hello:
#94 = Utf8 finally handle
#95 = Utf8 java/lang/Exception
#96 = Utf8 catch exception
#97 = Utf8 java/lang/String
#98 = NameAndType #57:#58 // sayHello:(Ljava/lang/String;)V
#99 = Utf8 Rambo
#100 = NameAndType #42:#37 // staticName:Ljava/lang/String;
#101 = NameAndType #43:#40 // staticAge:I
#102 = Utf8 user static init
#103 = Utf8 staticName=
#104 = Utf8 staticAge=
#105 = Utf8 com/lyyzoo/jvm/test01/Person
#106 = Utf8 java/io/Serializable
#107 = Utf8 username
#108 = Utf8 java/lang/Throwable
#109 = Utf8 java/lang/System
#110 = Utf8 out
#111 = Utf8 Ljava/io/PrintStream;
#112 = Utf8 append
#113 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#114 = Utf8 (I)Ljava/lang/StringBuilder;
#115 = Utf8 toString
#116 = Utf8 ()Ljava/lang/String;
#117 = Utf8 java/io/PrintStream
#118 = Utf8 println
{
【字段表集合】
public static final java.lang.String FIELD_NAME;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String username
public static final int AGE_MAX;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 100
【方法表】
public com.lyyzoo.jvm.test01.User();
【描述符索引】
descriptor: ()V
【访问标志】
flags: ACC_PUBLIC
【方法体代码指令】
Code:
【方法栈大小】
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/lyyzoo/jvm/test01/Person."<init>":()V
4: aload_0
5: ldc #2 // String 兰博
7: putfield #3 // Field name:Ljava/lang/String;
10: aload_0
11: bipush 25
13: putfield #4 // Field age:I
16: return
【属性表,方法局部变量】
LineNumberTable:
line 27: 0
line 17: 4
line 18: 10
line 28: 16
【本地变量表,方法入参】
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 this Lcom/lyyzoo/jvm/test01/User;
public com.lyyzoo.jvm.test01.User(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method com/lyyzoo/jvm/test01/Person."<init>":()V
4: aload_0
5: ldc #2 // String 兰博
7: putfield #3 // Field name:Ljava/lang/String;
10: aload_0
11: bipush 25
13: putfield #4 // Field age:I
16: aload_0
17: aload_1
18: putfield #3 // Field name:Ljava/lang/String;
21: aload_0
22: iload_2
23: putfield #4 // Field age:I
26: return
LineNumberTable:
line 30: 0
line 17: 4
line 18: 10
line 31: 16
line 32: 21
line 33: 26
LocalVariableTable:
Start Length Slot Name Signature
【可以看出,对象实例方法的第一个参数始终都是 this,这也是为什么我们可以在方法内调用 this 的原因】
0 27 0 this Lcom/lyyzoo/jvm/test01/User;
0 27 1 name Ljava/lang/String;
0 27 2 age I
MethodParameters:
Name Flags
name
age
public void printInfo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #6 // class java/lang/StringBuilder
6: dup
7: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
10: ldc #8 // String name:
12: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_0
16: getfield #3 // Field name:Ljava/lang/String;
19: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: ldc #10 // String , age:
24: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
27: aload_0
28: getfield #4 // Field age:I
31: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
34: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
37: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
40: return
LineNumberTable:
line 37: 0
line 38: 40
LocalVariableTable:
Start Length Slot Name Signature
0 41 0 this Lcom/lyyzoo/jvm/test01/User;
public static void staticPrintInfo();
descriptor: ()V
【访问标志】
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #15 // String FIELD_NAME:username, AGE_MAX:100
5: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 42: 0
line 43: 8
【注意,静态方法第一个参数就不再是 this 了】
public void sayHello(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokespecial #16 // Method com/lyyzoo/jvm/test01/Person.sayHello:(Ljava/lang/Object;)V
5: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
8: new #6 // class java/lang/StringBuilder
11: dup
12: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
15: ldc #17 // String User say hello:
17: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_1
21: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
LineNumberTable:
line 48: 0
line 49: 5
line 50: 30
LocalVariableTable:
Start Length Slot Name Signature
【第一个参数为 this】
0 31 0 this Lcom/lyyzoo/jvm/test01/User;
0 31 1 str Ljava/lang/String;
MethodParameters:
Name Flags
str
public int willThrowException();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: iload_1
5: idiv
6: istore_2
7: iload_2
8: istore_3
9: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
12: ldc #18 // String finally handle
14: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
17: iload_3
18: ireturn
19: astore_2
20: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
23: ldc #20 // String catch exception
25: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: iload_1
29: istore_3
30: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
33: ldc #18 // String finally handle
35: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: iload_3
39: ireturn
40: astore 4
42: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
45: ldc #18 // String finally handle
47: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
50: aload 4
52: athrow
【方法异常表】
Exception table:
from to target type
2 9 19 Class java/lang/Exception
2 9 40 any
19 30 40 any
40 42 40 any
LineNumberTable:
line 54: 0
line 56: 2
line 57: 7
line 62: 9
line 57: 17
line 58: 19
line 59: 20
line 60: 28
line 62: 30
line 60: 38
line 62: 40
line 63: 50
LocalVariableTable:
Start Length Slot Name Signature
7 12 2 r I
20 20 2 e Ljava/lang/Exception;
0 53 0 this Lcom/lyyzoo/jvm/test01/User;
2 51 1 i I
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class com/lyyzoo/jvm/test01/User, int ]
stack = [ class java/lang/Exception ]
frame_type = 84 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
public void sayHello(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
【重载泛型方法时,会多出 ACC_BRIDGE、ACC_SYNTHETIC 两个标志,ACC_BRIDGE代表是jvm自动生成的桥接方法,ACC_SYNTHETIC代表是jvm生成的不可见方法】
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #21 // class java/lang/String
5: invokevirtual #22 // Method sayHello:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/lyyzoo/jvm/test01/User;
MethodParameters:
Name Flags
str synthetic
【静态代码块】
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: ldc #23 // String Rambo
2: putstatic #24 // Field staticName:Ljava/lang/String;
5: bipush 20
7: putstatic #25 // Field staticAge:I
10: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
13: ldc #26 // String user static init
15: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
18: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
21: new #6 // class java/lang/StringBuilder
24: dup
25: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
28: ldc #27 // String staticName=
30: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: getstatic #24 // Field staticName:Ljava/lang/String;
36: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
39: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
42: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
45: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
48: new #6 // class java/lang/StringBuilder
51: dup
52: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
55: ldc #28 // String staticAge=
57: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
60: getstatic #25 // Field staticAge:I
63: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
66: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
69: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
72: return
LineNumberTable:
line 13: 0
line 14: 5
line 22: 10
line 23: 18
line 24: 45
line 25: 72
}
Signature: #73 // Lcom/lyyzoo/jvm/test01/Person<Ljava/lang/String;>;Ljava/io/Serializable;
SourceFile: "User.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
我们也可以安装 jclasslib Bytecode viewer 插件,就可以在IDEA中清晰地看到 Class 包含的信息:
# 魔数与Class文件信息
魔数
唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动。
Minior version
是次版本号,Major version
是主版本号。Java的版本号是从45
开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加 1,所以 jdk1.8 的 Major version 是 52
。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。
Access flags
用于识别类或者接口层次的访问信息,比如这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型 等等。
# 常量池
虚拟机把常量池组织为入口列表,常量池中的许多入口都指向其他的常量池入口(比如引用了其它类),而且 class 文件中的许多条目也会指向常量池中的入口。列表中的第一项索引值为1,第二项索引值为2,以此类推。虽然没有索引值为0的入口,但是 constant_pool_count
会把这一入口也算进去,比如上面的 Constant pool count 为 119,而常量池实际的索引值最大为 118。
常量池主要存放两大类常量:字面量和符号引用。
字面量
:字面量主要是文本字符串、final 常量值、类名和方法名的常量等。符号引用
:符号引用对java动态连接起着非常重要的作用。主要的符号引用有:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。
常量池中每一项都是一个表,常量表主要有如下 17 种常量类型。
常量池的项目类型:
再理解下符号引用和直接应用:
符号引用
:java 文件在前端编译期间,class 文件并不知道它引用的那些类、方法、字段的具体地址,不能被class文件中的字节码直接引用。因此使用符号引用来代替,运行时再动态连接到具体引用上。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。直接引用
:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。在运行时,Java虚拟机从常量池获得符号引用,然后在运行时解析引用项的实际地址。
比如看 sayHello 这个方法,首先要调用 super.sayHello,即父类 Person 的 sayHello 方法,那么第三个指令就会在常量池寻找 #16
这个索引,然后可以从常量池找到这个方法的相关信息,再通过 #29
找到 Person 类信息。
# 类索引、父类索引与接口索引
Class文件中由这三项数据来确定该类型的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
它们各自指向一个类型为 CONSTANT_Class_info
的常量表,通过 CONSTANT_Class_info 常量中的索引值可以找到定义在 CONSTANT_Utf8_info
类型的常量中的全限定名字符串。
# 字段表
字段表用于描述接口或者类中声明的变量。Java语言中的“字段”包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
描述符:
descriptor
是描述符,描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。- 对于数组类型,每一维度将使用一个前置的
[
字符来描述,如一个定义为java.lang.String[][]
类型的二维数组将被记录成[[Ljava/lang/String;
,一个整型数组int[]
将被记录成[I
。 - 用描述符来描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号
()
之内。
描述符标识字符含义:
比如从构造方法的描述符 <Ljava/lang/String;I)V>
可以看出,方法的参数包括对象类型 java.lang.String、基本类型 int,返回值为 void。
# 方法表
方发表与字段表类似,方发表用于描述方法的访问标志、名称索引、描述符索引、属性表集合、代码指令等。
1、异常表
如果方法表有异常捕获的话,还会有异常表。当方法抛出异常时,就会从异常表查找能处理的异常处理器。
2、重载多出的方法
如果父类方法在子类中被重写,那方法表中就会包含父类方法的信息,如果重写泛型方法,还会出现编译器自动添加的桥接方法。
因为泛型编译后的实际类型为 Object
,如果子类泛型不是 Object,那么编译器会自动在子类中生成一个 Object 类型的桥接方法。桥接方法的内部会先做类型转换检查,然后调用重载的方法。因为我们在声明变量时一般是声明的超类,实际类型为子类,而超类方法的参数是 Object 类型的,因此就会调用到桥接方法,进而调用子类重载后的方法。
而且,当我们通过反射根据方法名获取方法时,要注意泛型重载可能获取到桥接方法,此时可以通过 method.isBridge() 方法判断是否是桥接方法。
3、类构造器和实例构造器
方法表还包括实例构造方法 <init>
和类构造方法 <clinit>
。<init>
就是对应的实例构造器。<clinit>
是编译时将类初始化的代码搜集在一起形成的类初始化方法,如静态变量赋值、静态代码块。
初始化阶段会调用类构造器 <clinit>
来初始化类,因此其一定是线程安全的,是由虚拟机来保证的。这种机制我们可以用来实现安全的单例模式,枚举类的初始化也是在 <clinit>
方法中初始化的。
# 属性表
属性表集合主要是为了正确识别 Class 文件而定义的一些属性,如 Code、Deprecated、ConstantValue、Exceptions、SourceFile 等等。
每一个属性,它的名称都要从常量池中引用一个 CONSTANT_Utf8_info
类型的常量来表示。
# 类加载
# 类初始化的时机
类和接口被加载的时机因不同的虚拟机可能不同,但类初始化的触发时机有且仅有六种
情况:
- 当创建某个类的实例,如 new、反射、克隆、反序列化
- 当调用某个类的静态方法时
- 当使用某个接口或类的静态字段,或者赋值时(final 修饰的常量除外,它在编译期把结果放入常量池中了)
- 使用
java.lang.reflect
包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化 - 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化(接口除外)
- 当虚拟机启动时,会先初始化要执行的主类(包含main()方法的那个类)
这六种情况称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。
触发接口初始化的情况:
- 类初始化时并不会触发其实现的接口的初始化,接口初始化时也不会要求父接口初始化
- 在接口所声明的非常量字段被使用时,该接口才会被初始化
- 如果接口定义了 default 方法,那子类重写了这个方法,就会先触发接口的初始化
1、主动初始化
从输出可以看出,对 final
常量的引用不会触发类的初始化,调用静态方法时触发了类的初始化,同时,一定会先触发父类的初始化,而且类只会被初始化一次。
注意初始化的顺序是按代码的顺序从上到下初始化:
2、被动初始化
如下被动引用不会触发类的初始化
- 通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化
- 通过数组定义来引用类,不会触发此类的初始化。但是会触发一个
[com.lyyzoo.jvm.test01.User
类型的初始化,即一维数组类型 - 引用类的常量不会触发类的初始化。常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
3、不难判断,例子中定义的类的加载顺序如下
# 加载
在加载阶段,Java虚拟机必须完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
这个二进制流可以从 Class 文件中获取,从JAR包、WAR包中获取,从网络中获取,实时生成、还可以从加密文件中获取,在加载时再解密(防止Class文件被反编译)。这个加载是由类加载器加载进虚拟机的,非数组类型可以使用内置的引导类加载器来加载,也可以使用开发人员自定义的类加载器来加载,我们可以自己控制字节流的获取方式。而数组类型本身不通过类加载器加载,它是由虚拟机直接在内存中构造出来的。
加载阶段会把 Class 常量池中的各项常量存放到运行时常量池中
(下图中的常量池只挑选了部分常量来展示)。加载阶段的最终产品就是 Class 类的实例对象,它成为程序与方法区内部数据结构之间的入口
,可以通过这个 Class 实例来获得类的信息、方法、字段、类加载器等等。
在装载过程中,虚拟机还会确认装载类的所有超类是否都被装载了,根据 super class
项解析符号引用,这就会导致超类的装载、连接和初始化。
# 验证
这一阶段的目的是确保 Class 文件的字节流中包含的信息符合 《Java虚拟机规范》 的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证阶段会完成下面四个阶段的检验:
文件格式验证
:保证输入的字节流能正确地解析并存储于方法区之内,通过这个阶段的验证之后,这段字节流会进入Java虚拟机内存的方法区中进行存储,后面的验证就是基于方法区的存储结构而进行了。元数据验证
:对类的元数据信息进行语义校验,如这个类是否有父类(除 java.lang.Object 外,所有的类都有父类)、是否继承了 final 的类、实现了 final 的方法等。字节码验证
:通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。符号引用验证
:最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。
# 准备
准备阶段是为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,初始值是指这个数据类型的零值
,而赋值的过程是放在 <clinit>
方法中,在初始化阶段执行的。注意实例变量是在创建实例对象时才初始化值的
。
基本数据类型的零值:
准备阶段还会为常量字段(final 修饰的常量,即字段表中有 ConstantValue 属性的字段)分配内存并直接赋值为定义的字面值。
User 类经过准备阶段后:
# 解析
解析过程就是根据符号引用查找到实体,再把符号引用替换成一个直接引用的过程。因为所有的符号引用都保存在常量池中,所以这个过程常被称作常量池解析。
1、静态解析与动态连接
所有方法调用的目标方法在 Class 文件里面都是一个常量池中的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在运行期间用到时转化为直接引用,这部分称为动态连接。
静态解析的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。这类方法包含 静态方法、私有方法、实例构造器、父类方法以及被 final 修饰的方法,这5种方法调用会在类加载的时候就把符号引用解析为该方法的直接引用(有可能是在初始化的时候去解析的)。
动态连接这个特性给Java带来了更强大的动态扩展能力,比如使用运行时对象类型,因为要到运行期间才能确定具体使用的类型。这也使得Java方法调用过程变得相对复杂,某些调用需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。
2、符号引用解析
对于符号引用类型如 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等,会查找到对应的类型数据、方法地址、字段地址的直接引用,然后将符号引用替换为直接引用
。
对于 CONSTANT_String _info
类型指向的字面量,虚拟机会检查字符串常量池中是否已经有相同字符串的引用,有则替换为这个字符串的引用,否则在堆中创建一个新的字符串对象,并将对象的引用放到字符串常量池中,然后替换常量池中的符号引用
。
对于数值类型的常量,如 CONSTANT_Long_info、CONSTANT_Integer_info,并不需要解析,虚拟机会直接使用那些常量值
。
# 初始化
直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,初始化阶段就是执行类构造器 <clinit>
方法的过程。
1、<clinit>
方法
<clinit>
方法是由编译器自动收集类中的所有类变量的赋值语句和静态代码块合并产生的,代码执行的顺序就是源文件中的顺序。- Java虚拟机会保证在子类的
<clinit>
方法执行前,父类的<clinit>
方法会先执行完毕,即先初始化直接超类。 <clinit>
方法对于类或接口来说不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>
方法。- 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成
<clinit>
方法。 - 执行接口的
<clinit>
方法不需要先执行父接口的<clinit>
方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的<clinit>
方法。 - Java虚拟机会保证一个类的
<clinit>
方法在多线程环境中被正确地加锁同步,<clinit>
一定是线程安全的。
2、User 类初始化后
一个类被装载、连接和初始化完成后,它就随时可以使用了。程序可以访问它的静态字段,调用它的静态方法,或者创建它的实例。
# 即时编译
初始化完成后,类在调用执行过程中,执行引擎会把字节码转为机器码,然后在操作系统中才能执行。在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是即时编译。
最初,虚拟机中的字节码是由解释器 Interpreter
完成编译的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,即时编译器(JIT)会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后保存到内存中,这样可以减少解释器的中间损耗,获得更高的执行效率。如果没有即时编译,每次运行相同的代码都会使用解释器编译。
# 类加载器
# 类加载器子系统
在Java虚拟机中,负责查找并装载类型的那部分被称为类加载器子系统。类加载器子系统会负责整个类加载的过程:装载、验证、准备、解析、初始化。
1、类加载器类型
Java 虚拟机有两种类加载器,启动类加载器和用户自定义类加载器
启动类加载器
:是Java虚拟机实现的一部分,启动类加载器主要用来加载受信任的 Java API 的 Class 文件。用户自定义类加载器
:是Java程序的一部分,用户自定义的类加载器都是java.lang.ClassLoader
的子类实例,开发人员可以自己控制字节流的加载方式。
2、类唯一性
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间,由不同的类加载器加载的类将被放在虚拟机内部的不同命名空间中。比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义
,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。这就是有时候我们测试代码时发现明明是同一个Class,却报强转失败之类的错误。
# 双亲委派模型
Java 1.8 之前采用三层类加载器、双亲委派的类加载架构。三层类加载器包括启动类加载器、扩展类加载器、应用程序类加载器。
1、三层类加载器
启动类加载器(Bootstrap ClassLoader)
:负责将 $JAVA_HOME/lib 或者 -Xbootclasspath 参数指定路径下面的文件(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载) 加载到虚拟机内存中。它用来加载 Java 的核心库,是用原生代码实现的,并不继承自 java.lang.ClassLoader,启动类加载器无法直接被 java 代码引用。扩展类加载器(Extension ClassLoader)
:负责加载 $JAVA_HOME/lib/ext 目录中的文件,或者 java.ext.dirs 系统变量所指定的路径的类库,它用来加载 Java 的扩展库。应用程序类加载器(Application ClassLoader)
:一般是系统的默认加载器,也称为系统类加载器,它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般 Java 应用的类都是由它来完成加载的,可以通过 ClassLoader.getSystemClassLoader() 来获取它。
2、双亲委派模型
除了启动类加载器之外,所有的类加载器都有一个父类加载器。应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。一般来说,开发人员自定义的类加载器的父类加载器一般是应用程序类加载器。
双亲委派模型:类加载器在尝试去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,如果父类加载器没有,继续寻找父类加载器,依次类推,如果到启动类加载器都没找到才从自身查找。这个类加载过程就是双亲委派模型。
首先要明白,Java 虚拟机判定两个 Java 类是否相同,不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样
。只有两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。不同类加载器加载的类之间是不兼容的。
双亲委派模型就是为了保证 Java 核心库的类型安全的
。所有 Java 应用都至少需要引用 java.lang.Object 类,也就是说在运行的时候,java.lang.Object 这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成或者自己定义了一个 java.lang.Object 类的话,很可能就存在多个版本的 java.lang.Object 类,而这些类之间是不兼容的。通过双亲委派模型,对于 Java 核心库的类加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。有了双亲委派模型,就算自己定义了一个 java.lang.Object 类,也不会被加载。
3、ClassLoader
类加载器之间的父子关系一般不是以继承的关系来实现的,通常是使用组合、委托关系来复用父加载器的代码。ClassLoader 中有一个 parent 属性来表示父类加载器,如果 parent 为 null,就会调用本地方法直接使用启动类加载器来加载类。类加载器在成功加载某个类之后,会把得到的 java.lang.Class 类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。
# 线程上下文类加载器
线程上下文类加载器可通过 java.lang.Thread
中的方法 getContextClassLoader()
获得,可以通过 setContextClassLoader(ClassLoader cl)
来设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)
方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是应用程序类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。线程上线文类加载器使得父类加载器可以去请求子类加载器完成类加载的行为,这在一定程度上是违背了双亲委派模型的原则。
# 对象及其生命周期
# 实例化对象
1、实例化一个类有四种途径
- 明确地使用
new
操作符 - 调用 Class 或者 java.lang.reflcct.Constructor 对象的
newInstance()
方法 - 调用任何现有对象的
clone()
方法 - 通过
java.io.ObjectInputStream
类的getObject()
方法反序列化
2、实例化对象的过程
① 当虚拟机要实例化一个对象时,首先从常量池中找到这个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就会触发相应的类加载过程。
② 在类加载检查通过后,虚拟机将为新生对象分配内存,为对象分配空间就是把一块确定大小的内存块从Java堆中划分出来。
③ 内存分配完成之后,虚拟机必须将分配到的内存空间(不包括对象头)都初始化为零值。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
④ 接下来,虚拟机还要对对象进行必要的设置,例如这个对象的类型信息、元数据地址、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
⑤ 最后开始执行对象的构造函数,即Class文件中的 <init>
方法,按照开发人员的意图对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。
# 对象的内存布局
在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
1、对象头
对象头主要由两部分组成:Mark Word
和 类型指针
,如果是数组对象,还会包含一个数组长度。
Mark Word
:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。synchronized 锁升级就依赖锁标志、偏向线程等锁信息,垃圾回收新生代对象转移到老年代则依赖于GC分代年龄。类型指针
:对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。数组长度
:有了数组长度,虚拟机就可以通过普通Java对象的元数据信息确定Java对象的大小,如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。
这三部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特。64 位虚拟机中,为了节约内存可以使用选项 +UseCompressedOops
开启指针压缩,某些数据会由 64位压缩至32位。
2、实例数据
实例数据部分是对象真正存储的有效信息,即对象的各个字段数据,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。
3、对齐填充
对齐填充仅仅起着占位符的作用,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是 8字节 的整数倍,就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被设计成正好是8字节的倍数,因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
4、计算对象占用内存大小
从上面的内容可以看出,一个对象对内存的占用主要分两部分:对象头和实例数据。在64位机器上,对象头中的 Mark Word 和类型指针各占 64 比特,就是16字节。实例数据部分,可以根据类型来判断,如 int 占 4 个字节,long 占 8 个字节,字符串中文占3个字节、数字或字母占1个字节来计算,就大概能计算出一个对象占用的内存大小。当然,如果是数组、Map、List 之类的对象,就会占用更多的内存。
# 对象访问定位
创建对象后,这个引用变量会压入栈中,即一个 reference
,它是一个指向对象的引用,这个引用定位的方式主要有两种:使用句柄访问对象和直接指针访问对象。
1、通过句柄访问对象
使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
使用句柄来访问的最大好处就是 reference 中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。
2、通过直接指针访问对象
如果使用直接指针访问的话,Java堆中对象的内存布局就必须放置访问类型数据的相关信息(Mark Word 中记录了类型指针),reference 中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。
使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,HotSpot 虚拟机主要就是使用这种方式进行对象访问。
# 垃圾收集
当对象不再被程序所引用时,它所使用的堆空间就需要被回收,以便被后续的新对象所使用。JVM 的内存分配管理机制会自动帮我们回收无用的对象,它知道如何确定对象不再被引用,什么时候去回收这些垃圾对象,使用什么回收策略来回收更高效,以及如何管理内存,这部分就是JVM的垃圾收集相关的内容了。
# 文章来源
作者:bojiangzhou 链接:https://juejin.cn/post/6917256143160999950 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。