JVM类加载
类文件结构
-
一个简单的HelloWorld.java
1
2
3
4
5
6JAVA
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
} -
编译后查看其二进制字节码文件的内容
1
2BASH
od -t xC target/classes/com/demo/HelloWorld.class -
根据JVM规范,类文件结构如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19LESS
ClassFile {
u4 magic; // 魔数,用于标识文件类型
u2 minor_version; // Java虚拟机的次版本号
u2 major_version; // Java虚拟机的主版本号
u2 constant_pool_count; // 常量池大小
cp_info constant_pool[constant_pool_count-1]; // 常量池数组
u2 access_flags; // 访问标识符,用于表示类或接口的访问控制
u2 this_class; // 当前类或接口的索引
u2 super_class; // 当前类的超类(父类)索引
u2 interfaces_count; // 接口数量
u2 interfaces[interfaces_count]; // 接口索引列表
u2 fields_count; // 字段数量
field_info fields[fields_count]; // 字段信息数组
u2 methods_count; // 方法数量
method_info methods[methods_count]; // 方法信息数组
u2 attributes_count; // 类或接口的附加属性数量
attribute_info attributes[attributes_count]; // 类或接口的附加属性信息数组
}
魔数
-
以下面的字节码文件,按顺序逐个进行分析
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
39PLAINTEXT
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14 -
0-3字节,表示它是否是
class
类型的文件- 0000000
ca fe ba be
00 00 00 34 00 22 0a 00 06 00 14 09
- 0000000
-
在Java中,所有的.class文件都以魔数
1
ca fe ba be
开头,这个魔数的前4个字节用于识别该文件是否为Java类文件,如果这个魔数不匹配,那么Java虚拟机将无法加载该文件。
-
关于
1
cafebabe
这个魔数的由来并没有具体的官方解释,但有一些有趣的猜测和传说。
- 一种说法是,这个魔数是由Java的创造者之一、现任谷歌高管James Gosling取的。据说Gosling是个爱好咖啡的人,他认为Java这个名字也与咖啡有关,所以他将cafebabe取作魔数来向咖啡致敬。另外,有一种传说认为这个魔数是来自于一个好莱坞电影中的经典台词,
cafe babe
(咖啡宝贝?)。 - 然而,无论是什么样的由来,cafebabe这个魔数现在已经成为Java世界中的一个标志,每一个Java程序员都能够轻松地辨认出这个魔数,这也是Java文件格式稳定性的一个体现。
- 一种说法是,这个魔数是由Java的创造者之一、现任谷歌高管James Gosling取的。据说Gosling是个爱好咖啡的人,他认为Java这个名字也与咖啡有关,所以他将cafebabe取作魔数来向咖啡致敬。另外,有一种传说认为这个魔数是来自于一个好莱坞电影中的经典台词,
-
版本
- 4-7字节,表示类的版本
00 34(52)
对应十进制为52,表示的是Java 8
常量池
Constant Type | Value |
---|---|
CONSTANT_Utf8 | 1 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_Class | 7 |
CONSTANT_String | 8 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_NameAndType | 12 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
- 8-9字节,表示常量池长度
- 0000000 ca fe ba be 00 00 00 34
00 22
0a 00 06 00 14 09 00 22(34)
,表示常量池有#1-#33
项,注意#0项不计入,也没有值
- 0000000 ca fe ba be 00 00 00 34
-
第#1项
0a 表示一个 Method 信息,00 06 和 00 14(20) 表示它引用了常量池中 #6 和 #20 项来获得这个方法的所属类
和方法名
- 0000000 ca fe ba be 00 00 00 34 00 22
0a 00 06 00 14
09
- 0000000 ca fe ba be 00 00 00 34 00 22
-
第#2项
09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项来获得这个成员变量的所属类
和成员变量名
- 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15
09
- 0000020
00 16 00 17
08 00 18 0a 00 19 00 1a 07 00 1b 07
- 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15
-
第#3项
08 表示一个字符串常量名称,00 18(24)表示它引用了常量池中 #24 项- 0000020 00 16 00 17
08 00 18
0a 00 19 00 1a 07 00 1b 07
- 0000020 00 16 00 17
-
第#4项
0a 表示一个 Method 信息,00 19(25) 和 00 1a(26) 表示它引用了常量池中 #25 和 #26项来获得这个方法的所属类
和方法名
- 0000020 00 16 00 17 08 00 18
0a 00 19 00 1a
07 00 1b 07
- 0000020 00 16 00 17 08 00 18
-
第#5项
07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项 - 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b `07` - 0000040 `00 1c` 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 291
2
3
4
5
6
7
8
9
07 表示一个 Class 信息,00 1b(27) 表示它引用了常量池中 #27 项
- 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a `07 00 1b` 07
6. ```
第#6项 -
第#7项
<init>1
2
3
4
5
01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是01 表示一个 utf8 串,00 03 表示长度,28 29 56 是1
2
3
4
5
- 0000040 00 1c 01 `00 06 3c 69 6e 69 74 3e` 01 00 03 28 29
8. ```
第#8项其实就是表示无参、无返回值 - 0000040 00 1c 01 00 06 3c 69 6e 69 74 3e `01 00 03 28 29` - 0000060 `56` 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e1
()V
-
第#9项
Code1
2
3
4
5
01 表示一个 utf8 串,00 04 表示长度,43 6f 64 65 是01 表示一个 utf8 串,00 0f(15) 表示长度,4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 是1
2
3
4
5
- 0000060 56 `01 00 04 43 6f 64 65` 01 00 0f 4c 69 6e 65 4e
10. ```
第#10项- 0000060 56 01 00 04 43 6f 64 `65 01 00 0f 4c 69 6e 65 4e` - 0000100 `75 6d 62 65 72 54 61 62 6c 65` 01 00 12 4c 6f 631
LineNumberTable
-
第#11项
LocalVariableTable1
2
3
4
5
01 表示一个 utf8 串,00 12(18) 表示长度,4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65是01 表示一个 utf8 串,00 04 表示长度,74 68 69 73 是1
2
3
4
5
6
- 0000100 75 6d 62 65 72 54 61 62 6c 65 `01 00 12 4c 6f 63`
- 0000120 `61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65` 01
12. ```
第#12项- 0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 `01` - 0000140 `00 04 74 68 69 73` 01 00 1d 4c 63 6e 2f 69 74 631
this
-
第#13项
Lcn/itcast/jvm/t5/HelloWorld;1
2
3
4
5
01 表示一个 utf8 串,00 1d(29) 表示长度,是01 表示一个 utf8 串,00 04 表示长度,74 68 69 73 是1
2
3
4
5
6
7
- 0000140 00 04 74 68 69 73 `01 00 1d 4c 63 6e 2f 69 74 63`
- 0000160 `61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f`
- 0000200 `57 6f 72 6c 64 3b` 01 00 04 6d 61 69 6e 01 00 16
14. ```
第#14项- 0000200 57 6f 72 6c 64 3b `01 00 04 6d 61 69 6e` 01 00 161
main
-
第#15项
([Ljava/lang/String;)V1
2
3
4
5
01 表示一个 utf8 串,00 16(22) 表示长度,是01 表示一个 utf8 串,00 04 表示长度,是1
2
3
4
5
6
7
8
9
其实就是参数为字符串数组,无返回值
- 0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e `01 00 16`
- 0000220 `28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72`
- 0000240 `69 6e 67 3b 29 56` 01 00 04 61 72 67 73 01 00 13
16. ```
第#16项- 0000240 69 6e 67 3b 29 56 `01 00 04 61 72 67 73` 01 00 131
args
-
第#17项
[Ljava/lang/String;1
2
3
4
5
01 表示一个 utf8 串,00 13(19) 表示长度,是01 表示一个 utf8 串,00 10(16) 表示长度,是1
2
3
4
5
6
7
- 0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 `01 00 13`
- 0000260 `5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69`
- 0000300 `6e 67 3b` 01 00 10 4d 65 74 68 6f 64 50 61 72 61
18. ```
第#18项- 0000300 6e 67 3b `01 00 10 4d 65 74 68 6f 64 50 61 72 61` - 0000320 `6d 65 74 65 72 73` 01 00 0a 53 6f 75 72 63 65 461
MethodParameters
-
第#19项
SourceFile1
2
3
4
5
01 表示一个 utf8 串,00 0a(10) 表示长度,是01 表示一个 utf8 串,00 0f(15) 表示长度,是1
2
3
4
5
6
- 0000320 6d 65 74 65 72 73 `01 00 0a 53 6f 75 72 63 65 46`
- 0000340 `69 6c 65` 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
20. ```
第#20项- 0000340 69 6c 65 `01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64` - 0000360 `2e 6a 61 76 61` 0c 00 07 00 08 07 00 1d 0c 00 1e1
HelloWorld.java
-
第#21项
名+类型1
2
3
4
5
6
7
0c 表示一个
07 表示一个 Class 信息,00 1d(29) 引用了常量池中 #29 项 - 0000360 2e 6a 61 76 61 0c 00 07 00 08 `07 00 1d` 0c 00 1e1
2
3
4
5
6
7
,00 07 00 08 引用了常量池中 #7 #8 两项
- 0000360 2e 6a 61 76 61 `0c 00 07 00 08` 07 00 1d 0c 00 1e
22. ```
第#22项 -
第#23项
名+类型1
2
3
4
5
6
7
0c 表示一个
01 表示一个 utf8 串,00 0f(15) 表示长度,是1
2
3
4
5
6
7
8
,00 1e(30) 00 1f (31)引用了常量池中 #30 #31 两项
- 0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d `0c 00 1e`
- 0000400 `00 1f` 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
24. ```
第#24项- 0000400 00 1f `01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64`1
hello world
-
第#25项
0c 表示一个1
2
3
4
5
6
7
8
9
07 表示一个 Class 信息,00 20(32) 引用了常量池中 #32 项
- 0000420 `07 00 20` 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
26. ```
第#26项,00 21(33) 00 22(34)引用了常量池中 #33 #34 两项 - 0000420 07 00 20 `0c 00 21 00 22` 01 00 1b 63 6e 2f 69 741
名+类型
-
第#27项
cn/itcast/jvm/t5/HelloWorld1
2
3
4
5
01 表示一个 utf8 串,00 1b(27) 表示长度,是01 表示一个 utf8 串,00 10(16) 表示长度,是1
2
3
4
5
6
7
- 0000420 07 00 20 0c 00 21 00 22 01 `00 1b 63 6e 2f 69 74`
- 0000440 `63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c`
- 0000460 `6f 57 6f 72 6c 64` 01 00 10 6a 61 76 61 2f 6c 61
28. ```
第#28项- 0000460 6f 57 6f 72 6c 64 `01 00 10 6a 61 76 61 2f 6c 61` - 0000500 `6e 67 2f 4f 62 6a 65 63 74` 01 00 10 6a 61 76 611
java/lang/Object
-
第#29项
java/lang/System1
2
3
4
5
01 表示一个 utf8 串,00 10(16) 表示长度,是01 表示一个 utf8 串,00 03 表示长度,是1
2
3
4
5
6
- 0000500 6e 67 2f 4f 62 6a 65 63 74 `01 00 10 6a 61 76 61`
- 0000520 `2f 6c 61 6e 67 2f 53 79 73 74 65 6d` 01 00 03 6f
30. ```
第#30项- 0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d `01 00 03 6f` - 0000540 `75 74` 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 721
out
-
第#31项
Ljava/io/PrintStream;1
2
3
4
5
01 表示一个 utf8 串,00 15(21) 表示长度,是01 表示一个 utf8 串,00 13(19) 表示长度,是1
2
3
4
5
6
- 0000540 75 74 `01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72`
- 0000560 `69 6e 74 53 74 72 65 61 6d 3b` 01 00 13 6a 61 76
32. ```
第#32项- 0000560 69 6e 74 53 74 72 65 61 6d 3b `01 00 13 6a 61 76` - 0000600 `61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d`1
java/io/PrintStream
-
第#33项
println1
2
3
4
5
01 表示一个 utf8 串,00 07 表示长度,是01 表示一个 utf8 串,00 15(21) 表示长度,是1
2
3
4
5
- 0000620 `01 00 07 70 72 69 6e 74 6c 6e` 01 00 15 28 4c 6a
34. ```
第#34项- 0000620 01 00 07 70 72 69 6e 74 6c 6e `01 00 15 28 4c 6a` - 0000640 `61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b` - 0000660 `29 56` 00 21 00 05 00 06 00 00 00 00 00 02 00 011
(Ljava/lang/String;)V
访问标识与继承信息
- 访问标识符:21表示class是一个类,公共的
- 0000660 29 56
00 21
00 05 00 06 00 00 00 00 00 02 00 01
- 0000660 29 56
- 当前类或接口的索引:05表示根据常量池中的#5找到本类的全限定名
- 0000660 29 56 00 21
00 05
00 06 00 00 00 00 00 02 00 01
- 0000660 29 56 00 21
- 当前类的超类(父类)索引:06表示根据常量池中的#6找到父类全限定名
- 0000660 29 56 00 21 00 05
00 06
00 00 00 00 00 02 00 01
- 0000660 29 56 00 21 00 05
- 接口数量:本类为0
- 0000660 29 56 00 21 00 05 00 06
00 00
00 00 00 02 00 01
- 0000660 29 56 00 21 00 05 00 06
Flag Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC | 0x0001 | Declared public; may be accessed from outside its package. |
ACC_FINAL | 0x0010 | Declared final; no subclasses allowed. |
ACC_SUPER | 0x0020 | Treat superclass methods specially when invoked by the invokespecial instruction. |
ACC_INTERFACE | 0x0200 | Is an interface, not a class. |
ACC_ABSTRACT | 0x0400 | Declared abstract; must not be instantiated. |
ACC_SYNTHETIC | 0x1000 | Declared synthetic; not present in the source code. |
ACC_ANNOTATION | 0x2000 | Declared as an annotation type. |
ACC_ENUM | 0x4000 | Declared as an enum type. |
Field信息
- 字段数量(成员变量数量),本类为0
- 0000660 29 56 00 21 00 05 00 06 00 00
00 00
00 02 00 01
- 0000660 29 56 00 21 00 05 00 06 00 00
Method信息
-
方法数量:本类为2,构造方法和main方法
- 0000660 29 56 00 21 00 05 00 06 00 00 00 00
00 02
00 01
- 0000660 29 56 00 21 00 05 00 06 00 00 00 00
-
一个方法由访问修饰符、名称、参数描述、方法苏属性数量、方法属性组成
-
00 01表示访问修饰符(本类中为public)
- 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02
00 01
- 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02
-
00 07表示引用了常量池中的#07项作为方法名称
- 0000700
00 07
00 08 00 01 00 09 00 00 00 2f 00 01 00 01
- 0000700
-
00 08表示引用了常量池中的#08项作为方法参数描述
- 0000700 00 07
00 08
00 01 00 09 00 00 00 2f 00 01 00 01
- 0000700 00 07
-
01表示引方法属性数量,本方法是1
- 0000700 00 07 00 08
00 01
00 09 00 00 00 2f 00 01 00 01
- 0000700 00 07 00 08
-
00 09表示引用常量池#09项,发现是
1
code
属性
- 0000700 00 07 00 08 00 01
00 09
00 00 00 2f 00 01 00 01
- 0000700 00 07 00 08 00 01
-
00 00 00 2f表示此属性的长度是47
- 0000700 00 07 00 08 00 01 00 09
00 00 00 2f
00 01 00 01
- 0000700 00 07 00 08 00 01 00 09
-
00 01表示操作数栈最大深度
- 0000700 00 07 00 08 00 01 00 09 00 00 00 2f
00 01
00 01
- 0000700 00 07 00 08 00 01 00 09 00 00 00 2f
-
00 01表示局部变量最大槽(slot)数
- 0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01
00 01
- 0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01
-
00 00 00 05表示字节码长度,本例为5
- 0000720
00 00 00 05
2a b7 00 01 b1 00 00 00 02 00 0a 00
- 0000720
-
2a b7 00 01 b1 是字节码指令
- 0000720 00 00 00 05
2a b7 00 01 b1
00 00 00 02 00 0a 00
- 0000720 00 00 00 05
-
00 00 00 02 表示方法细节属性数量,本例为2
- 0000720 00 00 00 05 2a b7 00 01 b1
00 00 00 02
00 0a 00
- 0000720 00 00 00 05 2a b7 00 01 b1
-
00 0a表示引用了常量池#10项,发现是LineNumberTable属性
- 0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02
00 0a
00 - 00 00 00 06表示此属性的总长度,本例是6
- 0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a
00
- 0000740
00 00 06
00 01 00 00 00 04 00 0b 00 00 00 0c 00
- 0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a
- 00 01表示LineNumberTable长度
- 0000740 00 00 06
00 01
00 00 00 04 00 0b 00 00 00 0c 00
- 0000740 00 00 06
- 00 00 表示字节码行号
- 0000740 00 00 06 00 01
00 00
00 04 00 0b 00 00 00 0c 00
- 0000740 00 00 06 00 01
- 00 04表示Java源码行号
- 0000740 00 00 06 00 01 00 00
00 04
00 0b 00 00 00 0c 00
- 0000740 00 00 06 00 01 00 00
- 0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02
-
00 0b表示引用了常量池#11项,发现是LocalVariableTable属性
-
0000740 00 00 06 00 01 00 00 00 04
00 0b
00 00 00 0c 00 -
00 00 00 0c 表示此属性总长度,本例为12
- 0000740 00 00 06 00 01 00 00 00 04 00 0b
00 00 00 0c
00
- 0000740 00 00 06 00 01 00 00 00 04 00 0b
-
00 01 表示
1
LocalVariableTable
长度
- 0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c
00
- 0000760
01
00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
- 0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c
-
00 00 表示局部变量生命周期开始,相对于字节码的偏移量
-
- 0000760 01
00 00
00 05 00 0c 00 0d 00 00 00 09 00 0e 00
- 0000760 01
-
-
00 05 表示局部变量覆盖的范围长度
- 0000760 01 00 00
00 05
00 0c 00 0d 00 00 00 09 00 0e 00
- 0000760 01 00 00
-
00 0c 表示局部变量的名称,引用常量池#12项
- 0000760 01 00 00 00 05
00 0c
00 0d 00 00 00 09 00 0e 00
- 0000760 01 00 00 00 05
-
00 0d 表示局部变量的类型,本例引用了常量池 #13 项,是
1
Lcn/itcast/jvm/t5/HelloWorld;
- 0000760 01 00 00 00 05 00 0c
00 0d
00 00 00 09 00 0e 00
- 0000760 01 00 00 00 05 00 0c
-
00 00 表述局部变量占有的槽位(slot)编号,本例是0
- 0000760 01 00 00 00 05 00 0c 00 0d
00 00
00 09 00 0e 00
- 0000760 01 00 00 00 05 00 0c 00 0d
-
-
00 09代表访问修饰符(本类中是 public static)
-
00 0e 代表引用了常量池 #14 项作为方法名称
-
00 0f 代表引用了常量池 #15 项作为方法参数描述
-
00 02 代表方法属性数量,本方法是 2
-
其余代表方法属性(属性1)
-
00 09 表示引用了常量池 #09 项,发现是
Code
属性 -
00 00 00 37 表示此属性的长度是 55
-
00 02 表示
操作数栈
最大深度 -
00 01 表示
局部变量表
最大槽(slot)数 -
00 00 00 05 表示字节码长度,本例是 9
-
b2 00 02 12 03 b6 00 04 b1 是字节码指令
-
00 00 00 02 表示方法细节属性数量,本例是 2
-
00 0a 表示引用了常量池 #10 项,发现是
1
LineNumberTable
属性
- 00 00 00 0a 表示此属性的总长度,本例是 10
- 00 02 表示
LineNumberTable
长度 - 00 00 表示
字节码
行号 00 06 表示java 源码
行号 - 00 08 表示
字节码
行号 00 07 表示java 源码
行号
-
-
00 0b 表示引用了常量池 #11 项,发现是
LocalVariableTable
属性- 00 00 00 0c 表示此属性的总长度,本例是 12
- 00 01 表示
LocalVariableTable
长度 - 00 00 表示局部变量生命周期开始,相对于字节码的偏移量
- 00 09 表示局部变量覆盖的范围长度
- 00 10 表示局部变量名称,本例引用了常量池 #16 项,是
args
- 00 11 表示局部变量的类型,本例引用了常量池 #17 项,是
[Ljava/lang/String;
- 00 00 表示局部变量占有的槽位(slot)编号,本例是 0
-
0000760 01 00 00 00 05 00 0c 00 0d 00 00
00 09 00 0e 00
-
0001000
0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
-
0001020
09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
-
0001040
00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
-
0001060
00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
-
红色代表方法属性(属性2)
- 00 12 表示引用了常量池 #18 项,发现是
MethodParameters
属性 - 00 00 00 05 表示此属性的总长度,本例是 5
- 01 参数数量
- 00 10 表示引用了常量池 #16 项,是
args
- 00 00 访问修饰符
- 0001100
00 12 00 00 00 05 01 00 10 00 00
00 01 00 13 00
- 00 12 表示引用了常量池 #18 项,发现是
附加属性
-
00 01 表示附加属性数量
-
00 13 表示引用了常量池 #19 项,即
SourceFile
-
00 00 00 02 表示此属性的长度
-
00 14 表示引用了常量池 #20 项,即
1
HelloWorld.java
- 0001100 00 12 00 00 00 05 01 00 10 00 00
00 01 00 13 00
- 0001120
00 00 02 00 14
分
- 0001100 00 12 00 00 00 05 01 00 10 00 00
字节码指令
入门
-
在上一小节,有两个字节码指令,我们没有细说,那现在就来具体看看
-
一个是
1
public cn.itcast.jvm.t5.HelloWorld();
构造方法的字节码指令
1
2PLAINTEXT
2a b7 00 01 b1- 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
- b7 => invokespecial 预备调用构造方法,哪个方法呢?
- 00 01 引用常量池中 #1 项,即
Method java/lang/Object."<init>":()V
- b1 表示返回
-
另一个是
1
public static void main(java.lang.String[]);
主方法的字节码指令
1
2PLAINTEXT
b2 00 02 12 03 b6 00 04 b1- b2 => getstatic 用来加载静态变量,哪个静态变量呢?
- 00 02 引用常量池中 #2 项,即
Field java/lang/System.out:Ljava/io/PrintStream;
- 12 => ldc 加载参数,哪个参数呢?
- 03 引用常量池中 #3 项,即
String hello world
- b6 => invokevirtual 预备调用成员方法,哪个方法呢?
- 00 04 引用常量池中 #4 项,即
Method java/io/PrintStream.println:(Ljava/lang/String;)V
- b1 表示返回
-
详情请参考官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5
javap工具
-
自己分析类文件结构太麻烦了,Oracle提供了javap工具来反编译class文件
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
56
57
58
59
60
61
62
63
64
65
66PLAINTEXT
$ javap -v HelloWorld.class
Classfile /D:/Workspace/JVM/demo/target/classes/com/demo/HelloWorld.class
Last modified 2023-4-5; size 551 bytes
MD5 checksum 1389d939c65ba536eb81d1a5c61d99be
Compiled from "HelloWorld.java"
public class com.demo.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/demo/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/demo/HelloWorld;
#14 = Utf8 main
#15 = Utf8 (String;)V Ljava/lang/
#16 = Utf8 args
#17 = Utf8 String; Ljava/lang/
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/demo/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.demo.HelloWorld();
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
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
图解方法执行流程
-
原始Java代码
1
2
3
4
5
6
7
8
9
10
11
12JAVA
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
public class Demo_20 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
} -
编译后的字节码文件
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
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
95PLAINTEXT
$ javap -v Demo_20.class
Classfile /D:/Workspace/JVM/demo/target/classes/com/demo/Demo_20.class
Last modified 2023-4-7; size 601 bytes
MD5 checksum 0f9e41fb2a7334a69c89d2661540f4f1
Compiled from "Demo_20.java"
public class com.demo.Demo_20
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#25 // java/lang/Object."<init>":()V
#2 = Class #26 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
#6 = Class #31 // com/demo/Demo_20
#7 = Class #32 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/demo/Demo_20;
#15 = Utf8 main
#16 = Utf8 (String;)V Ljava/lang/
#17 = Utf8 args
#18 = Utf8 String; Ljava/lang/
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 Demo_20.java
#25 = NameAndType #8:#9 // "<init>":()V
#26 = Utf8 java/lang/Short
#27 = Class #33 // java/lang/System
#28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#29 = Class #36 // java/io/PrintStream
#30 = NameAndType #37:#38 // println:(I)V
#31 = Utf8 com/demo/Demo_20
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (I)V
{
public com.demo.Demo_20();
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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/demo/Demo_20;
public static void main(java.lang.String );
descriptor: (String;)V Ljava/lang/
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 6
line 11: 10
line 12: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
}
SourceFile: "Demo_20.java" -
main线程开始运行、分配栈帧内存
-
执行引擎开始执行字节码
- bipush 10
- istore_1
- ldc #3
- istore_2
- iload_1
- 将局部变量表slot 1的值加载到操作数栈中
- iload_2
- iadd
- istore_3
- getstatic #4
- iload_3
- invokevirtual #5
- return
- 完成main方法调用,弹出main栈帧
- 程序结束
分析 i++
-
目的:从字节码角度分析a++相关题目
-
原始Java代码
1
2
3
4
5
6
7
8
9JAVA
public class Demo_21 {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
} -
编译后的字节码文件
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
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
96PLAINTEXT
$ javap -v Demo_21.class
Classfile /D:/Workspace/JVM/demo/target/classes/com/demo/Demo_21.class
Last modified 2023-4-7; size 576 bytes
MD5 checksum 5bc962752b10ca4b57350ca9814ec5b0
Compiled from "Demo_21.java"
public class com.demo.Demo_21
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#22 // java/lang/Object."<init>":()V
#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #25.#26 // java/io/PrintStream.println:(I)V
#4 = Class #27 // com/demo/Demo_21
#5 = Class #28 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/demo/Demo_21;
#13 = Utf8 main
#14 = Utf8 (String;)V Ljava/lang/
#15 = Utf8 args
#16 = Utf8 String; Ljava/lang/
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 b
#20 = Utf8 SourceFile
#21 = Utf8 Demo_21.java
#22 = NameAndType #6:#7 // "<init>":()V
#23 = Class #29 // java/lang/System
#24 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
#25 = Class #32 // java/io/PrintStream
#26 = NameAndType #33:#34 // println:(I)V
#27 = Utf8 com/demo/Demo_21
#28 = Utf8 java/lang/Object
#29 = Utf8 java/lang/System
#30 = Utf8 out
#31 = Utf8 Ljava/io/PrintStream;
#32 = Utf8 java/io/PrintStream
#33 = Utf8 println
#34 = Utf8 (I)V
{
public com.demo.Demo_21();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/demo/Demo_21;
public static void main(java.lang.String );
descriptor: (String;)V Ljava/lang/
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
21: iload_1
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 18
line 8: 25
line 9: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args Ljava/lang/String;
3 30 1 a I
18 15 2 b I
}
SourceFile: "Demo_21.java" -
提示:
1
iinc
指令是直接在局部变量slot上进行运算,下面逐行分析字节码指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19PLAINTEXT
0: bipush 10 // 将一个byte压入操作数栈,此时就是将10压入操作数栈
2: istore_1 // 将操作数栈顶部数据弹出,存入局部变量表 slot 1
3: iload_1 // 将局部变量表slot 1的值加载到操作数栈中,也就是将10加载到栈中
4: iinc 1, 1 // 在当前局部变量上进行运算,自增1,此时局部变量 a = 11 至此 a++ 执行完毕
7: iinc 1, 1 // 在当前局部变量上进行运算,自增1,此时局部变量 a = 12
10: iload_1 // 将局部变量表slot 1的值加载到操作数栈中,也就是将12加载到栈中
11: iadd // 将栈内两个元素相加,10 + 12 = 22,将结果22加载到栈中
12: iload_1 // 将局部变量表slot 1的值加载到操作数栈中,也就是将12加载到栈中
13: iinc 1, -1 // 在当前局部变量上进行运算,自减1,此时局部变量 a = 11
16: iadd // 将栈内两个元素相加,22 + 12 = 34,结果为34
17: istore_2 // 将操作数栈顶部数据弹出,存入局部变量表slot 2
18: getstatic #2 // 下面就不分析了,就是输出a和b的值
21: iload_1
22: invokevirtual #3
25: getstatic #2
28: iload_2
29: invokevirtual #3
32: return -
那么最终的结果
a = 11
,b = 34
-
从字节码指令中,我们可以看出,
1
a++
和
1
++a
的区别为
a++
是先执行iload
,再执行iinc
++a
是先执行iinc
,再执行iload
条件判断指令
指令 | 助记符 | 含义 |
---|---|---|
0x99 | ifeq | 判断是否 == 0 |
0x9a | ifne | 判断是否 != 0 |
0x9b | iflt | 判断是否 < 0 |
0x9c | ifge | 判断是否 >= 0 |
0x9d | ifgt | 判断是否 > 0 |
0x9e | ifle | 判断是否 <= 0 |
0x9f | if_icmpeq | 两个int是否 == |
0xa0 | if_icmpne | 两个int是否 != |
0xa1 | if_icmplt | 两个int是否 < |
0xa2 | if_icmpge | 两个int是否 >= |
0xa3 | if_icmpgt | 两个int是否 > |
0xa4 | if_icmple | 两个int是否 <= |
0xa5 | if_acmpeq | 两个引用是否 == |
0xa6 | if_acmpne | 两个引用是否 != |
0xc6 | ifnull | 判断是否 == null |
0xc7 | ifnonnull | 判断是否 != null |
-
原始Java代码
1
2
3
4
5
6
7
8
9
10
11JAVA
public class Demo_22 {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
} -
编译后的字节码文件
1
2
3
4
5
6
7
8
9
10
11PLAINTEXT
0: iconst_0 // 将整数常量值0(int类型)压入操作数栈中。
1: istore_1 // 将栈顶数据存入局部变量表 slot 1
2: iload_1 // 将局部变量表slot 1的值压入操作数栈
3: ifne 12 // 判断不等于0,成立跳转至12行,不成立则执行下一行
6: bipush 10 // 将10压入操作数栈
8: istore_1 // 将栈顶数据存入局部变量表 slot 1
9: goto 15 // 跳转至第15行
12: bipush 20 // 将20压入操作数栈,对应 a = 20
14: istore_1 // 将栈顶数据存入局部变量表 slot 1
15: return
循环控制指令
-
原始Java代码
1
2
3
4
5
6
7
8
9JAVA
public class Demo_23 {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
} -
编译后的字节码文件
1
2
3
4
5
6
7
8
9PLAINTEXT
0: iconst_0 // 将整数常量值0(int类型)压入操作数栈中。
1: istore_1 // 将栈顶数据存入局部变量表 slot 1
2: iload_1 // 将局部变量表slot 1的值压入操作数栈
3: bipush 10 // 将10压入操作数栈
5: if_icmpge 14 // 判断 i >= 10 ,成立则跳转到14行,不成立则执行下一行
8: iinc 1, 1 // i自增
11: goto 2 // 跳转到第2行
14: return -
再比如
1
do while
循环
1
2
3
4
5
6
7
8
9JAVA
public class Demo_24 {
public static void main(String[] args) {
int i = 0;
do {
i++;
} while (i < 10);
}
} -
编译后的字节码文件
1
2
3
4
5
6
7
8PLAINTEXT
0: iconst_0 // 将整数常量值0(int类型)压入操作数栈中。
1: istore_1 // 将栈顶数据存入局部变量表 slot 1
2: iinc 1, 1 // i自增
5: iload_1 // 将局部变量表slot 1加载到操作数栈
6: bipush 10 // 将10加载到操作数栈
8: if_icmplt 2 // 判断 i < 10,成立则跳转到第2行,不成立执行下一行
11: return -
最后再来看看for循环
1
2
3
4
5
6
7
8JAVA
public class Demo_25 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
}
}
} -
编译后的字节码文件
1
2
3
4
5
6
7
8
9PLAINTEXT
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
注意到while和for的字节码,它们是一模一样的,这就是所谓的殊途同归
吧
判断结果
-
从字节码的角度来分析下面程序的运行结果
1
2
3
4
5
6
7
8
9
10
11
12JAVA
public class Demo_26 {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
System.out.println(x);
}
} -
最终x的结果是0
- 执行
x++
时,先执行iload_x
,将0加载到操作数栈中 - 然后执行iinc,将局部变量表中的x自增,此时局部变量表中的
x = 1
- 此时又执行了一个赋值操作,
istore_x
,将操作数栈中的0,重新赋给了局部变量表中的x,导致x为0
- 执行
-
下面是对应的字节码
1
2
3
4PLAINTEXT
10: iload_2
11: iinc 2, 1
14: istore_2
构造方法
-
<cinit>()V
JAVA public class Demo_27 { static int i = 10; static { i = 20; } static { i = 30; } public static void main(String[] args) { } }1
PLAINTEXT 0: bipush 10 2: putstatic #2 // Field i:I 5: bipush 20 7: putstatic #2 // Field i:I 10: bipush 30 12: putstatic #2 // Field i:I 15: return1
2
3
- 编译后的字节码文件1
2
3
4
5
6
7
- 编译器会按照从上至下的顺序,收集所有的static静态代码块和静态成员赋值的代码,合并成一个特殊的方法`<cinit>()V`
- `<cinit>()V`方法会在类加载的初始化阶段被调用
2. ```
<init>()V- 编译后的字节码文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25JAVA
public class Demo_28 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo_28(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo_28 demo = new Demo_28("s3", 30);
System.out.println(demo.a);
System.out.println(demo.b);
}
}- 编译器会按照从上至下的顺序,收集所有代码块和所有成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是会在最后1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22PLAINTEXT
0: aload_0
1: invokespecial #1 // super.<init>()V
4: aload_0
5: ldc #2 // <- "s1"
7: putfield #3 // -> this.a
10: aload_0
11: bipush 20 // <- 20
13: putfield #4 // -> this.b
16: aload_0
17: bipush 10 // <- 10
19: putfield #4 // -> this.b
22: aload_0
23: ldc #5 // <- "s2"
25: putfield #3 // -> this.a
28: aload_0 // ------------------------------
29: aload_1 // <- slot 1(a) "s3" |
30: putfield #3 // -> this.a |
33: aload_0 |
34: iload_2 // <- slot 2(b) 30 |
35: putfield #4 // -> this.b --------------------
38: return
方法调用
-
看一下几种不同方法调用对应的字节码指令,私有方法,final方法,公共方法,静态方法
-
其中静态方法包括对象调静态方法和类直接调静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17JAVA
public class Demo_29 {
public Demo_29(){}
private void test1(){}
private final void test2(){}
public void test3(){}
public static void test4(){}
public static void main(String[] args) {
Demo_29 demo = new Demo_29();
demo.test1();
demo.test2();
demo.test3();
demo.test4();
Demo_29.test4();
}
} -
编译后的字节码文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16PLAINTEXT
0: new #2 // class com/demo/Demo_29
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: aload_1
21: pop
22: invokestatic #7 // Method test4:()V
25: invokestatic #7 // Method test4:()V
28: return -
new #2是创建Demo_29对象,给对象分配内存,执行成功会将
对象引用
压入操作数栈 -
dup是赋值操作数栈顶的内容,本例为
1
对象引用
。那为什么需要两份引用呢?
- 一个是要配合
invokespecial
调用该对象的构造方法"<init>:()V"
,会消耗掉栈顶一个引用 - 另一个要配合
astore_1
赋值给局部变量
- 一个是要配合
-
final方法、私有方法、构造方法,都是由invokespecial指令来调用,属于静态绑定
-
普通成员方法是由invokevirtual调用,属于动态绑定,即支持多态
-
成员方法与静态方法调用的另一个区别是,执行方法前是否需要
对象引用
-
比较有意思的是,执行demo.test4()时,是通过
1
对象引用
调用的静态方法,可以看到在调用前执行了pop指令,把对象引用从操作数栈弹掉了,因为静态方法不需要对象引用来掉,通过这种方式,反而会增加两步无用的字节码指令
1
2
3
4PLAINTEXT
20: aload_1
21: pop
22: invokestatic #7 // Method test4:()V
-
多态的原理
-
原始Java代码
-
定义了一个抽象类Animal,还有其两个子类Cat和Dog
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
41JAVA
import java.io.IOException;
/**
* 添加VM参数:-XX:-UseCompressedOops -XX:-UseCompressedClassPointers
*/
public class Demo_30 {
public static void test(Animal animal) {
animal.eat();
System.out.println(animal);
}
public static void main(String[] args) throws IOException {
test(new Cat());
test(new Dog());
System.in.read();
}
}
abstract class Animal {
public abstract void eat();
public String toString() {
return "我是" + this.getClass().getSimpleName();
}
}
class Dog extends Animal {
public void eat() {
System.out.println("想啃大骨头");
}
}
class Cat extends Animal {
public void eat() {
System.out.println("想吃小鱼干");
}
}
-
-
运行代码
- 会停在System.in.read()方法上(当然你也可以直接打断点),运行jps命令获取进程id
-
运行HSDB工具
-
进入JDK安装目录,执行
1
2BASH
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB -
进入图形界面attach进程id
-
-
查找某个对象
-
打开Tools -> Find Object By Query,输入命令,点击Execute执行
1
2BASH
select d from com.demo.Dog d
-
-
查看对象内存结构
- 点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的16字节,前8字节是MarkWord,后8字节就是对象的Class指针,但现在看不到它的实际地址
-
查看对象Class的内存地址
-
查看类的vtable
-
方法1:ALT + R进入Inspector工具输入刚才的Class内存地址
-
无论通过哪种方法,都可以找到Dog Class的vtable长度为6,意思就是Dog类会有6个虚方法(多台相关的,final、static不会列入)
-
那么这6个虚方法都是谁呢?从Class的起始地址开始算,偏移
1
0x1b8
就是vtable的其实地址,进行计算得到
1
2
3
4
5PLAINTEXT
0x000001f1f48841a0
1b8 +
--------------------
0x000001f1f4884358 -
通过Windows -> Console进入命令行模式,执行如下命令,就得到了6个虚方法的入口地址
1
2
3
4
5
6
7
8BASH
mem 0x000001f1f4884358 6
0x000001f1f4884358: 0x000001f1f4481b10
0x000001f1f4884360: 0x000001f1f44815e8
0x000001f1f4884368: 0x000001f1f4883750
0x000001f1f4884370: 0x000001f1f4481540
0x000001f1f4884378: 0x000001f1f4481678
0x000001f1f4884380: 0x000001f1f4884148
-
-
验证方法地址
-
通过Tools -> Class Browser 查看每个类的方法定义,比较可知
1
2
3
4
5
6
7PLAINTEXT
0x000001f1f4481b10 -> Object -- protected void finalize() @0x000001f1f4481b10;
0x000001f1f44815e8 -> Object -- public boolean equals(java.lang.Object) @0x000001f1f44815e8;
0x000001f1f4883750 -> Animal -- public java.lang.String toString() @0x000001f1f4883750;
0x000001f1f4481540 -> Object -- public native int hashCode() @0x000001f1f4481540;
0x000001f1f4481678 -> Object -- protected native java.lang.Object clone() @0x000001f1f4481678;
0x000001f1f4884148 -> Dog -- public void eat() @0x000001f1f4884148; -
对号入座,发现
- eat()方法是Dog类自己的
- toString()方法是继承String类的
- finalize() ,equals(),hashCode(),clone() 都是继承 Object 类的
-
-
小结
- 当执行invokevirtual指令时
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际Class
- Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成号了
- 查表得到方法的具体地址
- 执行方法的字节码
- 当执行invokevirtual指令时
异常处理
-
try-catch
-
原始Java代码
1
2
3
4
5
6
7
8
9
10
11JAVA
public class Demo_31 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
} -
编译后的字节码文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18JAVA
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
Exception table:
from to target type
2 5 8 Class java/lang/Exception -
可以看到多出来一个
Exception table
的结构,[from to)
是左闭右开的检测范围,一旦这个范围内的字节码执行出现异常,则通过type
匹配异常类型,如果一致,进入target
所指示的行号,该例中是第8行,也就是执行catch代码块 -
第8行的字节码指令
astore_2
是将异常对象存入局部变量表的slot 2
的位置
-
-
多个catch块的情况
-
原始Java代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15JAVA
public class Demo_32 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (ArithmeticException e) {
i = 20;
} catch (NullPointerException e) {
i = 30;
} catch (Exception e) {
i = 40;
}
}
} -
编译后的字节码文件
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
48JAVA
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 26
8: astore_2
9: bipush 20
11: istore_1
12: goto 26
15: astore_2
16: bipush 30
18: istore_1
19: goto 26
22: astore_2
23: bipush 40
25: istore_1
26: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/NullPointerException
2 5 22 Class java/lang/Exception
LineNumberTable:
line 5: 0
line 7: 2
line 14: 5
line 8: 8
line 9: 9
line 14: 12
line 10: 15
line 11: 16
line 14: 19
line 12: 22
line 13: 23
line 15: 26
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/ArithmeticException;
16 3 2 e Ljava/lang/NullPointerException;
23 3 2 e Ljava/lang/Exception;
0 27 0 args -
因为异常出现时,只能进入一个
Exception table
的分支,所以局部变量表slot 2
位置被共用 -
[from, to)
的检测范围都相同,只不过target
的行号不同,对应三个catch块
-
-
multi-catch的情况
-
原始Java代码
1
2
3
4
5
6
7
8
9
10
11JAVA
public class Demo_33 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (NoSuchMethodError | IllegalAccessError | Exception e) {
e.printStackTrace();
}
}
} -
编译后的字节码文件
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
30JAVA
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 13
8: astore_2
9: aload_2
10: invokevirtual #5 // Method java/lang/Throwable.printStackTrace:()V
13: return
Exception table:
from to target type
2 5 8 Class java/lang/NoSuchMethodError
2 5 8 Class java/lang/IllegalAccessError
2 5 8 Class java/lang/Exception
LineNumberTable:
line 5: 0
line 7: 2
line 10: 5
line 11: 13
LocalVariableTable:
Start Length Slot Name Signature
9 4 2 e Ljava/lang/Throwable;
0 14 0 args -
[from, to)
的检测范围都相同,target
的行号也相同
-
-
finally
-
原始Java代码
1
2
3
4
5
6
7
8
9
10
11
12
13JAVA
public class Demo_34 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
} -
编译后的字节码文件
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
36JAVA
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1 // 0 -> i
2: bipush 10 // try ---------------------------
4: istore_1 // 10 -> i |
5: bipush 30 // |
7: istore_1 // 30 -> i |
8: goto 27 // return ------------------------
11: astore_2 // catch Exception -> e ----------
12: bipush 20 // |
14: istore_1 // 20 -> i |
15: bipush 30 // |
17: istore_1 // 30 -> i |
18: goto 27 // return ------------------------
21: astore_3 // catch any -> slot 3 -----------
22: bipush 30 // |
24: istore_1 // 30 -> i |
25: aload_3 // <- slot 3 |
26: athrow // throw -------------------------
27: return
Exception table:
from to target type
2 5 11 java/lang/Exception
2 5 21 any
11 15 21 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 28 0 args [Ljava/lang/String;
2 26 1 i I
12 3 2 e Ljava/lang/Exception; -
可以看到有3个
1
[from, to)
- 第一个
[2, 5)
是检测try块中是否有Exception异常,如果有则跳转至11行执行catch块 - 第二个
[2, 5)
是检测try块中是否有其他异常(非Exception异常),如果有则跳转至21行执行finally块 - 第三个
[11, 15)
是检测catch快中是否有其他异常,如果有则跳转至21行执行finally块
- 第一个
-
结论:finally中的代码被复制了三分,分别放进try流程、catch流程以及catch剩余的异常类型流程
-
关于finally的面试题
-
finally中出现了return
-
原始Java代码,先自己试着想一下最终的结果是啥:
20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15JAVA
public class Demo_35 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
private static int test() {
try {
return 10;
} finally {
return 20;
}
}
} -
编译后的字节码文件
-
注意这里要加上-p参数,才能显示私有方法的信息
1
2BASH
javap -v -p Demo_35.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22JAVA
private static int test();
descriptor: ()I
flags: ACC_PRIVATE, ACC_STATIC
Code:
stack=1, locals=2, args_size=0
0: bipush 10 // 将 int 10 压入栈顶
2: istore_0 // 将栈顶的 int 10 存入到局部变量 slot 0 中,并从栈顶弹出
3: bipush 20 // 将 int 20 压入栈顶
5: ireturn // 返回栈顶的 int 20
6: astore_1 // 捕获任何异常
7: bipush 20 // 将 int 20 压入栈顶
9: ireturn
Exception table:
from to target type
0 3 6 any
LineNumberTable:
line 11: 0
line 13: 3
StackMapTable: number_of_entries = 1
frame_type = 70 /* same_locals_1_stack_item */
stack = -
-
由于finally中的ireturn被插入了所有可能的流程,因此返回结果肯定以finally为准
-
至于字节码中的第二行,目前看似没啥用,先留个伏笔,等下个例子来讲解
-
之前的finally例子中,最后都会有一个athrow,这告诉我们,如果在finally中出现了return,那么就会吞掉异常,具体来看下面这个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16JAVA
public class Demo_36 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
private static int test() {
try {
int i = 1 / 0;
return 10;
} finally {
return 20;
}
}
} -
运行上面的代码,不会出现任何异常,输出20,
i = 1 / 0
那个异常被吞掉了
-
-
finally对返回值的影响
-
原始Java代码,还是先试着想想结果会输出什么:
10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16JAVA
public class Demo_37 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
private static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
} -
编译后的字节码文件
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
37JAVA
private static int test();
descriptor: ()I
flags: ACC_PRIVATE, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: bipush 10 // 将 10 放入栈顶
2: istore_0 // 10 -> i
3: iload_0 // <- i(10)
4: istore_1 // 将 i(10) 暂存至 slot 1,目的是为了固定返回值
5: bipush 20 // 将 20 放入栈顶
7: istore_0 // 20 -> i
8: iload_1 // 载入 slot 1 暂存的值 (10)
9: ireturn // 返回栈顶的值
10: astore_2
11: bipush 20
13: istore_0
14: aload_2
15: athrow
Exception table:
from to target type
3 5 10 any
LineNumberTable:
line 10: 0
line 12: 3
line 14: 5
line 12: 8
line 14: 10
line 15: 14
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I
StackMapTable: number_of_entries = 1
frame_type = 255 /* full_frame */
offset_delta = 10
locals =
stack = -
虽然在 finally 块中将 i 的值修改为 20,但是这不会影响 return 语句的返回值,因为在返回之前,i 的值已经被暂存到了 slot 1 中。在 finally 块中对 i 进行的修改不会影响 slot 1 中的值,因此 ireturn 指令返回的是 slot 1 中的值,即 10。
-
synchronized
-
synchronized代码块是对一个对象进行加锁操作,那么它是如何保障当synchronized代码块中出现了异常,还能正确的执行解锁操作呢?下面就从字节码的角度来分析一下底层原理
-
原始Java代码
1
2
3
4
5
6
7
8
9JAVA
public class Demo_38 {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
} -
编译后的字节码文件
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
40JAVA
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1 // lock引用 -> lock
8: aload_1 // <- lock (synchronized开始)
9: dup
10: astore_2 // lock引用 -> slot 2
11: monitorenter // monitorenter(lock引用)
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #4 // String ok
17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: aload_2 // <- slot 2(lock引用)
21: monitorexit // monitorexit(lock引用)
22: goto 30
25: astore_3 // any -> slot 3
26: aload_2 // <- slot 2(lock引用)
27: monitorexit // monitorexit(lock引用)
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable:
line 5: 0
line 6: 8
line 7: 12
line 8: 20
line 9: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object; -
[12, 22)
是监测的释放锁的流程,如果出现了异常,则跳转到25行,将异常信息存储到slot 3,同时再次尝试释放锁 -
[25, 28)
也是监测异常,如果有异常,
-
编译期处理
- 所谓
语法糖
,其实就是指Java编译器把*.java编译为*.class字节码的过程中,自动生成的和转换的一些代码,主要是为了减轻程序员的负担,算是Java编译器给我们的一个额外福利 - 下面的代码分析,借助了javap工具、idea的反编译功能、idea插件jclasslib等工具。另外,编译器转换的结果直接就是class字节码,只是为了便于阅读,给出了
几乎等价
的Java源码
默认构造器
-
如果一个类没有声明任何构造函数,Java 编译器会自动为该类生成一个无参构造函数。
1
2
3
4JAVA
public class Candy01 {
} -
编译成class后的代码
1
2
3
4
5
6
7
8JAVA
public class Candy1 {
// 这个无参构造是编译器帮助我们加上的
public Candy1() {
super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
}
}
自动拆装箱
-
这个特性是
1
JDK 5
开始加入的,
1
代码片段1:
1
2
3
4
5
6
7JAVA
public class Candy02 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
} -
但是这段代码在
1
JDK 5
之前是无法编译通过的,比如改写为如下形式,
1
代码片段2
1
2
3
4
5
6
7JAVA
public class Candy02 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
} -
显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间相互转换(尤其是集合类中的操作都是包装类型),因此这些转换的事情在
JDK 5
以后都由编译器在编译阶段完成。即代码片段1
都会在编译阶段转换成代码片段2
泛型集合取值
-
泛型也是
1
JDK 5
开始加入的特性,但Java在编译泛型后会执行
1
泛型擦除
的动作,即泛型信息在编译为字节码后就丢失了,实际的类型都当做Object类型来处理
1
2
3
4
5
6
7
8JAVA
public class Candy03 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10); // 实际调用的是 List.add(Object e)
Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
}
} -
所以在取值时,编译器真正生成的字节码中,还需要额外做一个类型转换的操作
1
2
3JAVA
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0); -
如果前面的x遍历修改为int基本类型,那么最终生成的字节码为
1
2JAVA
int x = (Integer)list.get(0).intValue(); -
还好这些麻烦事都不用自己做,要么叫语法糖呢
-
擦除的是字节码上的泛型信息,可以看到LocalVariableTypeTable仍然保留了方法参数泛型的信息
-
从下面字节码的第26行,我们可以清楚的看到add方法其实添加的是Object类型对象
-
从下面字节码的第30行,我们可以清楚的看到get方法的返回值也是Object类型对象
-
同时第31行是将类型强制转换为Integer
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
47JAVA
public com.demo.Candy03();
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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/demo/Candy03;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return
LineNumberTable:
line 8: 0
line 9: 8
line 10: 20
line 11: 31
LocalVariableTable:
Start Length Slot Name Signature
0 32 0 args [Ljava/lang/String;
8 24 1 list Ljava/util/List;
31 1 2 x Ljava/lang/Integer;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 24 1 list Ljava/util/List<Ljava/lang/Integer;>;
-
-
使用反射,能够获取到方法类型参数的泛型和方法返回值泛型的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22JAVA
public class Candy03 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method test = Candy03.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
System.out.println("原始类型 - " + parameterizedType.getRawType());
Type[] arguments = parameterizedType.getActualTypeArguments();
for (int i = 0; i < arguments.length; i++) {
System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
}
}
}
System.out.println("返回类型 - " + test.getGenericReturnType());
}
public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
return null;
}
} -
输出
1
2
3
4
5
6
7PLAINTEXT
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object
返回类型 - java.util.Set<java.lang.Integer>
可变参数
-
可变参数也是
1
JDK 5
开始加入的新特性,示例代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13JAVA
public class Candy04 {
public static void main(String[] args) {
foo("hello", "world");
}
private static void foo(String... args) {
String[] array = args;
System.out.println(array);
}
} -
可变参数String… args 其实是一个String[] args,同样Java编译器会在编译期间将上述代码转换为
1
2
3
4
5
6
7
8
9
10
11
12
13JAVA
public class Candy04 {
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
public static void foo(String[] args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
}- 注意:如果调用foo()时没有提供任何参数,那么则等价为foo(new String),创建了一个空的数组,而不是传一个null进去
foreach循环
-
仍然是JDK 5开始引入的语法糖,数组的循环
1
2
3
4
5
6
7
8
9JAVA
public class Candy05 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5}; // 数组的赋初值的简化,也是语法糖 new int[]{1, 2, 3, 4, 5}
for (int a : array) {
System.out.println(a);
}
}
} -
会被编译器转换为
1
2
3
4
5
6
7
8
9
10
11JAVA
public class Candy05 {
public Candy05() {
}
public static void main(String[] args) {
int[] array = new int[]{1, 2, 3, 4, 5};
for(int i = 0; i < array.length; ++i) {
int e = array[i];
System.out.println(e);
}
} -
而集合的循环
1
2
3
4
5
6
7
8
9JAVA
public class Candy06 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer integer : list) {
System.out.println(integer);
}
}
} -
实际上会被编译器转换为对迭代器的调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14JAVA
public class Candy06 {
public Candy06() {
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator iterator = list.iterator();
while (iterator.hasNext()){
Integer next = (Integer) iterator.next();
System.out.println(next);
}
}
}- foreach循环写法,能够配合数组,以及所有实现了Iterable接口的集合类一起使用,其中Iterable用来获取集合的迭代器Iterator
switch字符串
-
从
1
JDK 7
开始,switch可以作用于字符串和枚举类,这个功能其实也是语法糖,例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15JAVA
public class Candy07 {
public static void choose(String str) {
switch (str) {
case "hello": {
System.out.println("h");
break;
}
case "world": {
System.out.println("w");
break;
}
}
}
}- 注意:swtich配合Spring和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码,自然就清楚了
-
会被编译器转换为
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
28JAVA
public class Candy07 {
public Candy07() {
}
public static void choose(String str) {
byte x = -1;
switch (str.hashCode()) {
case 99162322: // hello 的 hashCode
if (str.equals("hello")) {
x = 0;
}
break;
case 113318802: // world 的 hashCode
if (str.equals("world")) {
x = 1;
}
}
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
} -
可以看到,执行了两边switch,第一遍是根据字符串的hashCode和queals将字符串转换为相应byte类型,第二遍才是利用byte进行比较
-
那为什么第一遍既要比较hashCode又利用equals比较呢?
-
hashCode是为了提高效率,减少可能的比较
-
而equals是为了防止哈希冲突,例如
1
BM
和
1
C.
这两个字符串的hashCode值都是2123,例如下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13JAVA
public static void choose(String str) {
switch (str) {
case "BM": {
System.out.println("h");
break;
}
case "C.": {
System.out.println("w");
break;
}
}
} -
会被编译器转换为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20JAVA
public static void choose(String str) {
byte x = -1;
switch (str.hashCode()) {
case 2123: // hashCode 值可能相同,需要进一步用 equals 比较
if (str.equals("C.")) {
x = 1;
} else if (str.equals("BM")) {
x = 0;
}
default:
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
-
switch枚举
-
switch枚举的例子,原始代码
1
2
3
4JAVA
enum Sex {
MALE, FEMALE
}1
2
3
4
5
6
7
8
9
10
11JAVA
public static void foo(Sex sex){
switch (sex){
case MALE:
System.out.println("男");
break;
case FEMALE:
System.out.println("女");
break;
}
} -
转换后的代码
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
30JAVA
public class Candy08 {
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
// 数组大小即为枚举元素个数,里面存储case用来对比的数字
static int[] map = new int[2];
static {
map[Sex.MALE.ordinal()] = 1;
map[Sex.FEMALE.ordinal()] = 2;
}
}
public static void foo(Sex sex) {
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
}
}
枚举类
-
JDK 7 新增了枚举类,以前面的性别枚举为例
1
2
3
4JAVA
enum Sex {
MALE, FEMALE
} -
转换后的代码
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
34JAVA
public final class Sex extends Enum<Sex> {
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*
* @param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is
* assigned
*/
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values() {
return $VALUES.clone();
}
public static Sex valueOf(String name) {
return Enum.valueOf(Sex.class, name);
}
} -
Sex
被声明为一个final
类,它继承了Enum<Sex>
类,Enum是Java中定义枚举的抽象类。MALE和FEMALE是Sex类的两个枚举值,它们被定义为静态常量。 -
除此之外,还有一个私有的、
final
的Sex
类型数组$VALUES
,它用于存储Sex类的所有枚举值。在类的静态块中,$VALUES
数组被初始化为一个包含MALE
和FEMALE
的数组。 -
构造函数
Sex(String name, int ordinal)
是私有的,这意味着无法在类的外部使用这个构造函数来创建Sex
的实例。只有Java编译器生成的代码才能调用这个构造函数来创建Sex的实例。 -
values()
和valueOf(String name)
是从Enum类继承的两个静态方法。values()
方法返回一个包含Sex类所有枚举值的数组,valueOf(String name)
方法返回指定名称的枚举值。 -
当我们使用MALE或者FEMALE时,其实底层调用的是
Enum.valueOf(Sex.class, "MALE")
和Enum.valueOf(Sex.class, "FEMALE")
try-with-resources
-
JDK 7 开始新增了对需要关闭的自愿处理的特殊语法 try-with-resources
1
2
3
4
5
6JAVA
try (资源变量 = 创建资源对象) {
} catch() {
} -
其中资源对象需要实现AutoCloseable接口,例如InputStream、OutputStream、Connection、Statement、ResultSet等接口都实现了AuthCloseable接口,使用try-with-resources 可以不用写finally语句块,编译器会帮助我们生成关闭资源代码,例如
1
2
3
4
5
6
7
8
9
10JAVA
public class Candy09 {
public static void main(String[] args) {
try (InputStream is = new FileInputStream("d:\\tmp.test")) {
System.out.println(is);
} catch (IOException e) {
e.printStackTrace();
}
}
} -
会被编译器转换为
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
37JAVA
public class Candy09 {
public Candy09() {
}
public static void main(String[] args) {
try {
InputStream is = new FileInputStream("d:\\tmp.txt");
Throwable t = null;
try {
System.out.println(is);
} catch (Throwable e1) {
// t 是我们代码出现的异常
t = e1;
throw e1;
} finally {
// 判断了资源不为空
if (is != null) {
// 如果我们代码有异常
if (t != null) {
try {
is.close();
} catch (Throwable e2) {
// 如果 close 出现异常,作为被压制异常添加
t.addSuppressed(e2);
}
} else {
// 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
is.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
} -
为什么要设计一个addSuppressed(Throwable e)(添加被压制异常)的方法呢?
-
这是为了防止异常信息的丢失
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15JAVA
public class Test {
public static void main(String[] args) {
try (MyResource resource = new MyResource()) {
int i = 1/0;
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyResource implements AutoCloseable {
public void close() throws Exception {
throw new Exception("close 异常");
}
} -
输出如下,两个异常信息都不会丢失
1
2
3
4
5
6PLAINTEXT
java.lang.ArithmeticException: / by zero
at com.demo.Test.main(Test.java:6)
Suppressed: java.lang.Exception: close 异常
at com.demo.MyResource.close(Test.java:14)
at com.demo.Test.main(Test.java:7)
-
方法重写时的桥接方法
-
方法重写时,对返回值分两种情况
-
父类与子类的返回值完全一致
-
子类返回值可以是父类返回值的子类(比较绕口,直接看下面的例子来理解)
1
2
3
4
5
6
7
8
9
10
11
12
13JAVA
class A {
public Number m() {
return 1;
}
}
class B extends A {
// 父类A方法的返回值是Number类型,子类B方法的返回值是Integer类型,Integer是Number的子类
public Integer m() {
return 2;
}
}-
那么对于子类,编译器会做如下处理
1
2
3
4
5
6
7
8
9
10
11
12JAVA
class B extends A {
public Integer m() {
return 2;
}
// 此方法才是真正重写了父类 public Number m() 方法
public synthetic bridge Number m() {
// 调用 public Integer m()
return m();
}
} -
其中的桥接方法比较特殊,仅对Java虚拟机课件,并且与原来的public Integer m()没有命名冲突
-
-
匿名内部类
-
原始Java代码
1
2
3
4
5
6
7
8
9
10
11JAVA
public class Candy10 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
public void run() {
System.out.println("ok");
}
};
}
} -
转换后代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15JAVA
// 额外生成的类
final class Candy10$1 implements Runnable {
Candy10$1() {
}
public void run() {
System.out.println("ok");
}
}
public class Candy10 {
public static void main(String[] args) {
Runnable runnable = new Candy10$1();
}
} -
对于匿名内部类,它的底层实现是类似于普通内部类的,只不过没有命名而已。在生成匿名内部类的class文件时,Java编译器会自动为该类生成一个类名,在原始类名上加后缀
$1
,如果有多个匿名内部类,则$2
、$3
以此类推 -
引用局部变量的匿名内部类,原始Java代码
1
2
3
4
5
6
7
8
9
10
11JAVA
public class Candy11 {
public static void test(final int x){
Runnable runnable = new Runnable() {
public void run() {
System.out.println("ok" + x);
}
};
}
} -
转换后代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16JAVA
// 额外生成的类
final class Candy11$1 implements Runnable {
int val$x;
Candy11$1(int x) {
this.val$x = x;
}
public void run() {
System.out.println("ok:" + this.val$x);
}
}
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}- 注意:这也解释了为什么匿名内部类引用局部变量时,局部变量必须为final的
- 因为在创建Candy$11对象时,将x的值赋给了val$x属性,所以x不应该再发生变化了
- 如果变化,那么$val$x属性没有机会再跟着一起变化
- 注意:这也解释了为什么匿名内部类引用局部变量时,局部变量必须为final的
类加载阶段
加载
- 将类的字节码载入方法区中,内部采用C++的instanceKlass描述Java类,它的重要field有
- _java_mirror:Java的类镜像,例如对String来说,就是String.class,作用是把klass暴露给Java使用
- _super:父类
- _fields:成员变量
- _methods:方法
- _constants:常量池
- _class_loader:类加载器
- _vtable:需方发表
- _itable:接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
- instanceKlass这样的
元数据
是存储在方法区(1.8后是在元空间内),但_java_mirror是存储在堆中 - 可以通过HSDB工具查看
- instanceKlass这样的
链接
-
验证
-
验证类是否符合JVM规范,安全性检查
-
使用支持二进制的编辑器修改HelloWorld.class的魔数
1
ca fe ba be
,在控制台运行后悔报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16BASH
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 3405691578 in class file cn/itcast/jvm/t5/HelloWorld
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
-
-
准备
-
为static变量分配空间,设置默认值
- static变量在
JDK 7
之前存储于instanceKlass末尾,从JDK 7
开始,存储于_java_mirror末尾 - static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果static遍历是final的基本类型以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果static遍历是final的,但属于引用类型,那么赋值也会在初始化阶段完成
- static变量在
-
原始Java代码
1
2
3
4
5
6
7
8JAVA
public class Load01 {
static int a;
static int b = 10;
static final int c = 20;
static final String d = "Hello";
static final Object e = new Object();
} -
编译后的字节码文件
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
52JAVA
static int a;
descriptor: I
flags: ACC_STATIC
static int b;
descriptor: I
flags: ACC_STATIC
static final int c;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 20
static final java.lang.String d;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String Hello
static final java.lang.Object e;
descriptor: Ljava/lang/Object;
flags: ACC_STATIC, ACC_FINAL
public com.demo.Load01();
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 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/demo/Load01;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: bipush 10
2: putstatic #2 // Field b:I
5: new #3 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putstatic #4 // Field e:Ljava/lang/Object;
15: return
LineNumberTable:
line 7: 0
line 10: 5 -
变量a和b都是静态变量,但是只有变量b被赋予了初始值10,赋值操作在初始化阶段体现,也就是在 static 块中实现
-
变量c和d都被声明为静态final变量,它们的值在编译时就已经确定了,分别是20和”Hello”,并且在字节码中使用了 ConstantValue 指令来指定这些常量的值。
-
变量e也是静态final变量,但它是一个引用类型变量,因此在初始化阶段才会被赋值,也就是在 static 块中实现。
-
-
解析
-
将常量池中的符号引用解析为直接引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17JAVA
public class Load02 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader classloader = Load02.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
Class<?> c = classloader.loadClass("com.demo.C");
// new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D {
} -
默认情况下,类的加载都是懒惰式的,如果用到了类C,没有用到类D的话,那么类D是不会主动加载的
-
使用loadClass方法不会导致类的解析和初始化
- 可以看到类D现在是
UnresolvedClass
,也就是未经解析的类,在常量池中仅仅是一个符号
- 可以看到类D现在是
-
使用new C()的方式会导致类的解析和初始化
- 可以看到此时类D已经加载成功了,同时在类C的常量池中也可以解析类D的地址
-
初始化
-
初始化即调用
<cinit>()V
方法,虚拟机ui保证这个类的构造方法
的线程安全 -
发生的时机:总的来说,类的初始化是懒惰的
-
main方法所在的类,总会被首先初始化
1
2
3
4
5
6
7
8
9
10JAVA
public class Load03 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
}
}-
控制台会输出
1
2PLAINTEXT
main init
-
-
首次访问这个类的静态变量或静态方法时,会进行初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17JAVA
public class Load03 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(A.a);
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}-
控制台输出
1
2
3
4PLAINTEXT
main init
a init
0
-
-
子类初始化,如果父类还没未初始化,则父类也会进行初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25JAVA
public class Load03 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(B.c);
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}-
控制台输出
1
2
3
4
5PLAINTEXT
main init
a init
b init
false
-
-
默认的Class.forName会导致初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17JAVA
public class Load03 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("com.demo.A");
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}-
控制台输出
1
2
3JAVA
main init
a init
-
-
new对象会导致初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17JAVA
public class Load03 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
new A();
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}-
控制台输出
1
2
3JAVA
main init
a init
-
-
-
不会导致类初始化的情况
-
访问类的 static final 静态常量(基本类型和字符串) 不会触发初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25JAVA
public class Load03 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(B.b);
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}-
控制台输出
1
2
3PLAINTEXT
main init
5.0
-
-
调用类对象.class不会触发初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25JAVA
public class Load03 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(B.class);
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}-
控制台输出
1
2
3PLAINTEXT
main init
class com.demo.B
-
-
类加载器的loadClass方法不会触发初始化
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
26JAVA
public class Load03 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
classLoader.loadClass("com.demo.B");
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}-
控制台输出
1
2PLAINTEXT
main init
-
-
Class.forName的参数2为false时(initalize = false),不会触发初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18JAVA
public class Load03 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class.forName("com.demo.A", false, classLoader);
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}-
控制台输出
1
2PLAINTEXT
main init
-
-
练习
-
从字节码分析,使用a、b、c这三个常量,是否会导致E初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16JAVA
public class Load04 {
public static void main(String[] args) {
System.out.println(E.a);
System.out.println(E.b);
System.out.println(E.c);
}
}
class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;
static {
System.out.println("init E");
}
}-
结论:a和b不会导致E的初始化,c会导致E的初始化
-
a和b是基本类型和字符串常量,而c是包装类型,其底层还需要调用Integer.valueOf()方法来装箱,只能推迟到初始化阶段运行,字节码如下
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
47JAVA
{
public static final int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 10
public static final java.lang.String b;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String hello
public static final java.lang.Integer c;
descriptor: Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
com.demo.E();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/demo/E;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: bipush 20
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: putstatic #3 // Field c:Ljava/lang/Integer;
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #5 // String init E
13: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return
LineNumberTable:
line 13: 0
line 15: 8
line 16: 16
}
-
-
典型应用 -> 完成懒惰初始化的单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15JAVA
public final class Singleton {
private Singleton() {
}
// 内部类中保存单例
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
} -
以上的实现特点是:
- 懒惰实例化
- 初始化时的线程安全是有保障的
类加载器
- 以
JDK 8
为例
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 上级为 | Application |
- 当JVM需要加载一个类时,它会首先委托父类加载器去加载这个类,如果父类加载器无法加载这个类,就会由当前类加载器来加载。如果所有的父类加载器都无法加载这个类,那么就会抛出ClassNotFoundException异常。
引导类加载器
-
Bootstrap ClassLoader
JAVA public class Load05 { public static void main(String[] args) throws ClassNotFoundException { Class<?> aClass = Class.forName("java.lang.Object"); System.out.println(aClass.getClassLoader()); } }1
2
3
是所有类加载器中最早的一个,负责加载JRE/lib下的核心类库,如java.lang.Object、java.lang.String等。JAVA public class Tmp { static { System.out.println("classpath Tmp init"); } }1
2
3
4
5
6
7
- 输出的结果是null,因为引导类加载器是由JVM的实现者用C/C++等语言编写的,而不是由Java编写的。在Java虚拟机的实现中,引导类加载器不是Java对象,也没有对应的Java类,因此它的ClassLoader属性为null。
- 编写一个Tmp类JAVA public class Load06 { public static void main(String[] args) throws ClassNotFoundException { Class<?> aClass = Class.forName("com.demo.load.Tmp"); System.out.println(aClass.getClassLoader()); } }1
2
3
- 加载Tmp类,并获取classLoaderPLAINTEXT classpath Tmp init sun.misc.Launcher$AppClassLoader@18b4aac21
2
3
- 输出如下,可以看到此时是由应用类加载器加载的ext Tmp init1
2
3
- 那我们现在写一个同名的Tmp类,将输出内容改为JAVA public class Tmp { static { System.out.println("ext Tmp init"); } }1
BASH $ jar -cvf tmp.jar com/demo/load/Tmp.class 已添加清单 正在添加: Tmp.class(输入 = 479) (输出 = 321)(压缩了 32%)1
2
3
- 将其打成一个jar包,放到JAVA_HOME/jre/ext目录下PLAINTEXT ext Tmp init sun.misc.Launcher$ExtClassLoader@29453f441
2
3
- 重新执行Load06,输出结果如下JAVA protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }1
2
3
4
5
6
7
8
9
10
11
- 此时就是从扩展类加载器加载的Tmp类了,因为当JVM需要加载一个类时,它会首先委托父类加载器去加载这个类
## 双亲委派机制
- 所谓双亲委派机制,就是指调用类加载器的loadClass方法时,查找类的规则
感觉这个`双亲`翻译成`上级`更合适,因为它们之间并没有继承关系
- 我们来看看ClassLoader中的loadClass()方法的源码JAVA protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 检查类是否已经被加载 Class<?> c = findLoadedClass(name); if (c == null) { try { // 如果类没有被加载,则委托给父ClassLoader加载 if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果父ClassLoader加载失败,则在自身查找类 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } }1
2
3
- 精简一下逻辑,双亲委派的核心思路如下JAVA Class.forName("com.mysql.jdbc.Driver");1
2
3
4
5
## 线程上下文类加载器
- 我们在使用JDBC时,都需要加载Driver驱动,但是我们好像并没有显示的调用Class.forName来加载Driver类JAVA public class DriverManager { private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); ··· static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } ··· }1
2
3
- 那么实际上是如何加载这个驱动的呢?让我们来追踪一下源码,这里只看最核心的部分JAVA System.out.println(DriverManager.class.getClassLoader());1
2
3
- 我们试着输出一下DirverManager的类加载器是谁JAVA private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // If the driver is packaged as a Service Provider, load it. // Get all the drivers through the classloader // exposed as a java.sql.Driver.class service. // ServiceLoader.load() replaces the sun.misc.Providers() // 1. 使用 ServiceLoader 机制加载驱动,即 SPI AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); /* Load these drivers, so that they can be instantiated. * It may be the case that the driver class may not be there * i.e. there may be a packaged driver with the service class * as implementation of java.sql.Driver but the actual class * may be missing. In that case a java.util.ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. * * Adding a try catch block to catch those runtime errors * if driver not available in classpath but it's * packaged as service and that service is there in classpath. */ try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); // 2. 使用 jdbc.drivers 定义的驱动名加载驱动 if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }1
2
3
4
5
6
7
- 输出的结果是`null`,那么说明它是由`Bootstrap ClassLoader`加载的,那么按理说应该是去`JAVA_HOMT/jre/lib`下搜索驱动类。
- 但`JAVA_HOMT/jre/lib`显然没有mysql-connector-java-5.7.31.jar包,在DriverManager的静态代码块中,是如何正确加载`com.mysql.jdbc.Driver`的呢?
- 继续来看看loadInitialDrivers()方法的源码JAVA ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class); Iterator<接口类型> iter = allImpls.iterator(); while(iter.hasNext()){ iter.next(); }1
2
3
4
5
6
7
8
9
10
- 先看2,它最后是使用的Class.forName完成类的加载和初始化,关联的是应用类加载器,因此可以顺利完成驱动类的加载
- 在看1,它就是大名鼎鼎的Service Provider Interface(SPI)
- 约定如下,在jar包的META-INF/services包下,以接口全限定名名为文件,文件内容是实现类名称
[![img](https://imgse.com/i/ppqEGM8)](https://imgse.com/i/ppqEGM8)
- 这样就可以使用如下代码遍历来得到实现类面向接口编程 + 解耦1
2
3
- 体现的是JAVA public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }1
2
3
4
5
6
7
8
9
10
的思想,在下面的一些框架中都运用了此思想
- JDBC
- Servlet初始化器
- Spring容器
- Dubbo(对SPI进行了扩展)
- 接着看ServiceLoader.load方法JAVA private 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 boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; } 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 boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } 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); } } public void remove() { throw new UnsupportedOperationException(); } }1
2
3
- 线程上下文类加载器是当前线程使用的类加载器,默认就是应用类加载器,它内部又是由Class.forName调用了线程上下文类加载器完成类加载,具体代码在ServiceLoader的内部类LazyIterator中JAVA package myclasspath; public class Tmp { static { System.out.println("init myclasspath.Tmp"); } public static void main(String[] args) { System.out.println(); } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
## 自定义类加载器
- 先来思考一下:什么时候需要自定义类加载器
1. 自定义类加载器可用于加载非 Classpath 路径中的类文件,例如外部配置文件夹、网络资源或其他自定义路径。这种需求在一些动态扩展或插件化的场景中比较常见。
2. 在应用程序中使用的类可以通过接口来使用,而不是直接引用类。这种做法可以减少应用程序之间的依赖,从而提高代码的灵活性和可维护性。同时,这种做法也使得框架的设计更加清晰和可扩展。
3. 在Tomcat容器中,每个Web应用程序都使用自己的类加载器,从而避免了不同Web应用程序之间的类冲突问题。
- 步骤
1. 继承ClassLoader类
2. 遵从双亲委派机制,重写findClass方法
- 注意不要重写loadClass方法,否则不会走双亲委派机制
3. 读取类文件的字节码
4. 调用父类的defineClass方法来加载类
5. 使用者调用类加载器的loadClass方法
- 示例
1. 准备一个Tmp类,编译后将其.class文件放至D盘根目录下JAVA class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String path = "D:\\myclasspath\\" + name + ".class"; try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); Files.copy(Paths.get(path), outputStream); byte[] bytes = outputStream.toByteArray(); return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException("类文件未找到:" + e); } } }1
2
3
2. 自定义MyClassLoader类JAVA public class Load07 { public static void main(String[] args) throws Exception { MyClassLoader classLoader = new MyClassLoader(); Class<?> aClass = classLoader.loadClass("myclasspath.Tmp"); aClass.newInstance(); } }1
2
3
3. 调用自定义的类加载器loadClass方法来加载Tmp类PLAINTEXT init myclasspath.Tmp1
2
3
- 控制台输出如下,成功加载Tmp类JAVA public class JIT1 { public static void main(String[] args) { for (int i = 0; i < 200; i++) { long start = System.nanoTime(); for (int j = 0; j < 1000; j++) { new Object(); } long end = System.nanoTime(); System.out.printf("%d\t%d\n", i, (end - start)); } } }1
2
3
4
5
6
7
8
9
# 运行期优化
## 即时编译
### 分层编译(TieredComlilation)
- 先来举个例子PLAINTEXT 0 28300 1 27700 2 28500 3 26400 4 26400 5 26700 6 27200 7 27800 8 26200 9 26000 10 26200 11 28500 12 42900 13 26900 14 26900 15 26000 16 28300 17 25500 18 28500 19 26500 20 26100 21 27300 22 26600 23 26100 24 28300 25 25000 26 26400 27 26000 28 26500 29 26700 30 26400 31 26400 32 26100 33 26600 34 26300 35 26300 36 37600 37 26400 38 26000 39 28500 40 31700 41 43700 42 27000 43 26200 44 25600 45 30400 46 26400 47 26200 48 33800 49 26700 50 27700 51 26300 52 34100 53 26300 54 37400 55 33700 56 25100 57 28200 58 26000 59 41300 60 33500 61 26500 62 26300 63 26200 64 26500 65 26100 66 26300 67 26500 68 28800 69 26400 70 27100 71 27700 72 26500 73 16300 74 7000 75 8900 76 8800 77 13500 78 8300 79 9000 80 11900 81 9300 82 11700 83 9400 84 7700 85 10200 86 8800 87 6100 88 7300 89 7000 90 7200 91 5800 92 7100 93 7800 94 6800 95 5900 96 7300 97 6800 98 6900 99 5900 100 6800 101 8100 102 6700 103 6100 104 6700 105 6900 106 6700 107 5700 108 7100 109 13000 110 7000 111 6000 112 6700 113 7300 114 6700 115 6000 116 6700 117 6700 118 11400 119 5900 120 7000 121 6900 122 8400 123 6700 124 10100 125 9900 126 11500 127 8300 128 6700 129 7000 130 7000 131 6900 132 7500 133 6800 134 7800 135 7400 136 7000 137 7000 138 7000 139 7000 140 7100 141 7100 142 11400 143 10100 144 6800 145 7100 146 6800 147 6700 148 7000 149 6600 150 6600 151 6800 152 6700 153 9400 154 5700 155 7100 156 6600 157 7100 158 6000 159 7800 160 11800 161 6800 162 5800 163 6700 164 6600 165 7100 166 6800 167 7900 168 7000 169 10100 170 6900 171 6600 172 7200 173 10000 174 6700 175 51100 176 14900 177 300 178 300 179 300 180 300 181 300 182 300 183 300 184 200 185 300 186 200 187 200 188 300 189 300 190 300 191 300 192 300 193 300 194 300 195 300 196 300 197 200 198 300 199 3001
2
3
- 输出结果如下:解释执行(Interpreter) - 在0层,JVM使用解释器来直接解释Java字节码,并执行程序。这种方式简单但效率较低,因为解释器需要逐条解释字节码指令,并执行它们,每次执行时都需要对字节码进行解析 2. ``` 1层1
2
3
4
5
6
7
- 可以看到循环到73次附近时,速度明显加快了,循环到178次时,速度又明显加快了,这是为什么呢?
- JVM将执行状态分为5个层次
1. ```
0层:使用C1即时编译器编译执行(带基本的profilling) - 在2层,JVM会收集一些基本的执行状态数据,即profilling。例如方法的调用次数、循环的回边次数等,然后根据这些数据来决定哪些代码块需要被编译执行。这种方式可以更加精确地编译热点代码,从而提高程序的执行速度 4. ``` 3层1
2
3
4
5
6
7
:使用C1即时编译器编译执行(不带profilling)
- 在1层,JVM会使用即时编译器(JIT)将Java字节码编译成本地机器码,然后直接执行机器码。这种方式相比于解释器,可以提供更高的执行速度。C1即时编译器适合编译执行热点代码,即被频繁执行的代码
3. ```
2层:使用C2即时编译器编译执行 - 在4层,JVM会使用更高级别的即时编译器(C2)来对代码进行优化,包括对循环、分支和递归等结构的优化。C2编译器的编译时间比C1场,但编译出来的代码执行速度更快。 profilling是指在运行过程中手机一些程序执行的状态数据,例如`方法的调用次数`、`循环的回边次数`1
2
3
4
5
6
7
:使用C1即时编译器编译执行(带完全的profilling)
- 在3层,JVM会收集更加详细的执行状态数据,例如内联调用的次数、方法的参数类型等,以便更好地优化代码。这种方式可以进一步提高程序的执行速度,但同时也会增加编译的开销
5. ```
4层 -
即时编译器(JIT)和解释器的区别
- 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- JIT是将一些字节码编译为机器码,并存入
Code Cache
,下次遇到相同的代码,直接执行
,无需再次编译 - 解释器是将字节码解释为针对所有平台都通用的机器码
- JIT会根据平台类型,生成平台特定的机器码
-
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成
机器码
,而是采用解释器执行
的方法运行; -
另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成
机器码
,以达到理想的运行速度
。 -
执行效率上简单比较一下:
Interceptor < C1 < C2
-
上面代码中最后的耗时都在
1
300
附近,这是C2即时编译器做了逃逸分析,因为上面的代码中,我们仅仅是
1
创建
了Object对象,而并没有
1
使用
它,也就是没有
1
逃逸
出当前作用域
- 在进行逃逸分析时,JVM会分析对象是否可能被线程外的代码引用,如果对象不会逃逸出当前方法的作用域,那么JVM会将对象的分配
优化为栈上分配
,从而避免了堆内存的分配和垃圾回收的压力。
- 在进行逃逸分析时,JVM会分析对象是否可能被线程外的代码引用,如果对象不会逃逸出当前方法的作用域,那么JVM会将对象的分配
-
将对象分配在栈上的
1
优点
是:
快速分配和回收
:栈内存的分配和回收都非常快,比堆内存要快得多。如果对象可以在栈上分配,那么它的分配和回收都可以更快,从而提高程序的性能。减少垃圾回收
:在Java中,对象的分配和回收是由垃圾回收器来完成的。如果对象可以在栈上分配,那么它就不会对堆内存的使用和垃圾回收产生影响,从而可以减少垃圾回收的频率和时间,提高程序的性能。
-
我们可以添加VM参数
1
-XX:-DoEscapeAnalysis
关闭逃逸分析,然后再次执行代码,观察耗时情况
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
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
201PLAINTEXT
0 28100
1 28100
2 26400
3 26700
4 26700
5 26600
6 26600
7 26300
8 26400
9 26400
10 26500
11 26700
12 25900
13 39200
14 26700
15 26400
16 26600
17 35300
18 26400
19 26800
20 28600
21 28100
22 28700
23 28100
24 29900
25 33800
26 31300
27 29700
28 28500
29 26700
30 30900
31 30100
32 26700
33 30300
34 29700
35 26200
36 26200
37 28700
38 26800
39 29700
40 28600
41 30100
42 30700
43 28300
44 34000
45 26400
46 26100
47 28800
48 26800
49 28000
50 37800
51 27600
52 33700
53 36600
54 26900
55 25900
56 35500
57 26100
58 26100
59 26300
60 26000
61 29800
62 27600
63 30800
64 26900
65 26800
66 27100
67 11800
68 6800
69 7500
70 8500
71 7100
72 6900
73 6900
74 6800
75 6800
76 11300
77 8800
78 10200
79 10500
80 8400
81 6800
82 8400
83 7100
84 6700
85 7000
86 8100
87 6700
88 6700
89 7000
90 9100
91 12700
92 13000
93 11100
94 7700
95 5700
96 6900
97 8600
98 7100
99 7400
100 6700
101 13100
102 20000
103 9600
104 7100
105 7200
106 6900
107 6000
108 6900
109 6700
110 6800
111 7000
112 6700
113 6900
114 9500
115 6100
116 7200
117 7000
118 7000
119 7000
120 6600
121 6800
122 7100
123 6100
124 6900
125 6800
126 7100
127 7100
128 11700
129 11400
130 10300
131 10500
132 27200
133 11800
134 13200
135 73400
136 33800
137 8200
138 7500
139 6400
140 6200
141 6200
142 6200
143 13100
144 7400
145 6600
146 7100
147 6000
148 6200
149 6000
150 5200
151 6100
152 6000
153 6000
154 5200
155 9600
156 8800
157 6300
158 5600
159 6700
160 6200
161 7100
162 5800
163 6500
164 6200
165 6100
166 6000
167 6100
168 6200
169 6100
170 5900
171 7100
172 7900
173 6400
174 6400
175 6100
176 6300
177 6300
178 6300
179 6100
180 6900
181 6100
182 6500
183 5900
184 6300
185 6100
186 6300
187 6300
188 6100
189 6200
190 9100
191 8500
192 6300
193 6100
194 6000
195 6100
196 6300
197 6100
198 6200
199 5900
方法内联(Inlining)
-
方法内联
1
2
3
4
5
6JAVA
private static int square(int i) {
return i * i;
}
System.out.println(square(9)); -
如果发现square是热点方法,且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝,病粘贴到调用者的位置
1
2JAVA
System.out.printl (9 * 9); -
还能够进行常量折叠(constant folding)的优化
1
2JAVA
System.out.println(81); -
下面来验证一下,还是输出耗时
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
28JAVA
0 81 25200
1 81 24100
2 81 19800
3 81 20000
4 81 20200
···
73 81 19600
74 81 20000
75 81 9000
76 81 2300
77 81 2300
78 81 3400
···
267 81 3900
268 81 51900
269 81 15900
270 81 100
271 81 0
272 81 0
273 81 100
274 81 100
275 81 0
276 81 0 -
最后耗时为0,就是进行了常量折叠的优化
-
我们可以添加VM参数
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
打印内联信息,可以看到我们的square方法被标记为了热点代码
-
同时也可以禁止某个方法的内联
1
-XX:CompileCommand=dontinline,*JIT2.square
,不能进行常量折叠优化了,速度不会到达0
1
2
3
4
5
6
7
8
9JAVA
···
495 81 3300
496 81 3700
497 81 3000
498 81 2900
499 81 2900
字段优化
-
添加如下依赖
1
2
3
4
5
6
7
8
9
10
11
12XML
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.32</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.32</version>
<scope>provided</scope>
</dependency> -
编写基准测试代码
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
53JAVA
public class Benchmark1 {
int[] elements = randomInts(1_000);
private static int[] randomInts(int size) {
Random random = ThreadLocalRandom.current();
int[] values = new int[size];
for (int i = 0; i < size; i++) {
values[i] = random.nextInt();
}
return values;
}
public void test1() {
for (int i = 0; i < elements.length; i++) {
doSum(elements[i]);
}
}
public void test2() {
int[] local = this.elements;
for (int i = 0; i < local.length; i++) {
doSum(local[i]);
}
}
public void test3() {
for (int element : elements) {
doSum(element);
}
}
static int sum = 0;
static void doSum(int x) {
sum += x;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(Benchmark1.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
} -
@Warmup
注解表示在基准测试运行之前需要进行预热,以使JVM达到最佳运行状态。在这个例子中,预热进行了2次,每次持续1秒钟。 -
@Measurement
注解表示运行5次基准测试,每次持续1秒钟。 -
@State
注解定义了Benchmark1类的实例作用域为Scope.Benchmark,表示这个类的实例可以在不同的测试方法之间共享,并保持在整个基准测试运行期间的状态。 -
这个类包含了三个测试方法:test1、test2和test3。这些测试方法执行相同的操作,即对数组elements中的所有元素进行求和操作,但使用不同的方法来访问数组中的元素。test1使用了数组索引,test2使用了本地数组变量,而test3使用了foreach循环。
-
启用doSum的方法内联
1
,测试结果如下
1
2
3
4
5PLAINTEXT
Benchmark Mode Cnt Score Error Units
Benchmark1.test1 thrpt 5 2830851.513 ± 68534.850 ops/s
Benchmark1.test2 thrpt 5 2844317.417 ± 8097.137 ops/s
Benchmark1.test3 thrpt 5 2849940.840 ± 7190.091 ops/s -
我们这里重点关注的是Score,现在开启了doSum的内联,这三种遍历方式的性能没有显著差异
-
那现在禁用doSum方法的内联
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
,测试结果如下1
2
3
4
5PLAINTEXT
Benchmark Mode Cnt Score Error Units
Benchmark1.test1 thrpt 5 313751.710 ± 34874.348 ops/s
Benchmark1.test2 thrpt 5 388759.125 ± 90456.387 ops/s
Benchmark1.test3 thrpt 5 394614.041 ± 50721.161 ops/s -
这三种遍历方式的性能与之前相比,都下降了一个数量级,test2和test3的性能差异不大,test1的性能明显要差一点,这是为什么呢?
-
因为doSum方法是否内联,会影响elements成员变量的读取的优化
-
如果doSum方法内联了,那么刚刚的test1方法会被优化成下面的样子(伪代码)
1
2
3
4
5
6
7
8JAVA
@Benchmark
public void test1() {
// elements.length 首次读取会缓存起来 -> int[] local
for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
sum += elements[i]; // 1000 次取下标 i 的元素 <- local
}
} -
如果doSum方法被内联,则循环中的每次对elements数组的访问都可以被优化,编译器可以将数组长度的读取操作提到循环外部,将elements数组的引用保存在本地变量中,从而避免了循环中每次访问数组引用的开销。这样,循环中只需要进行一次数组长度的读取,以及1000次对数组元素的访问操作,可以节省1999次对数组引用的访问。
-
如果doSum方法没有被内联,则循环中的每次对elements数组的访问都需要通过方法调用来完成,这会导致每次循环中都需要进行一次对数组引用的读取操作,因此不能进行上述优化。
-
反射优化
-
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17JAVA
import java.lang.reflect.Method;
public class Reflect1 {
public static void foo() {
System.out.println("foo...");
}
public static void main(String[] args) throws Exception {
Method foo = Reflect1.class.getMethod("foo");
for (int i = 0; i <= 16; i++) {
System.out.printf("%d\t", i);
foo.invoke(null);
}
System.in.read();
}
} -
首先定义了一个名为
foo
的静态方法,该方法只是简单地输出一条字符串。 -
然后在main方法中,使用
Reflect1
类的getMethod
方法获取名为foo
的Method
对象,以便之后进行反射调用。 -
接着使用循环调用反射方法,循环次数从0到16,每次循环都调用反射获取的Method对象的
invoke
方法,传入null
作为静态方法的调用者。因为foo
方法是静态方法,所以调用者可以为null。 -
最后使用
System.in.read()
方法暂停程序的运行,以便我们可以观察程序的输出结果。
1 | JAVA |
-
前15次调用使用的是NativeMethodAccessorImpl实现的MethodAccessor,该实现类使用JNI调用底层的C/C++代码实现方法调用。由于NativeMethodAccessorImpl的实现开销较大,因此前15次的反射调用的性能相对较差。
1
2JAVA
private static int inflationThreshold = 15; -
而第16次调用则采用了GeneratedMethodAccessor1实现的MethodAccessor,这个实现类通常是使用Java字节码动态生成的,因此方法调用的性能比NativeMethodAccessorImpl更好。这是因为在第15次调用时,生成了一个新的MethodAccessorImpl实现类(MethodAccessorGenerator),并在下一次方法调用时使用该实现类,即第16次调用。