黑马JVM课程——类加载(三)
类文件结构
参考文档:Chapter 4. The class File Format (oracle.com)
以一个简单的 HelloWorld..java 为例:
1 | public class HelloWorld { |
- 执行
javac -parameters -d . HelloWorld.java
编译- 使用
-parameters
参数,可以在编译只保留方法参数的名称信息
- 使用
编译之后的class文件以16进制打开如下所示:
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[root@localhost ~]# od -t xC HelloWorld.class
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09
0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07
0000040 00 18 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 04 6d 61 69
0000120 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67
0000140 2f 53 74 72 69 6e 67 3b 29 56 01 00 10 4d 65 74
0000160 68 6f 64 50 61 72 61 6d 65 74 65 72 73 01 00 04
0000200 61 72 67 73 01 00 0a 53 6f 75 72 63 65 46 69 6c
0000220 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64 2e 6a
0000240 61 76 61 0c 00 07 00 08 07 00 19 0c 00 1a 00 1b
0000260 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 07 00
0000300 1c 0c 00 1d 00 1e 01 00 21 63 6f 6d 2f 65 78 61
0000320 6d 70 6c 65 2f 64 65 6d 6f 31 2f 74 65 73 74 2f
0000340 48 65 6c 6c 6f 57 6f 72 6c 64 01 00 10 6a 61 76
0000360 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 01 00 10
0000400 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d
0000420 01 00 03 6f 75 74 01 00 15 4c 6a 61 76 61 2f 69
0000440 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b 01 00
0000460 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74
0000500 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 01 00
0000520 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00
0000560 00 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1d
0000600 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00
0000620 01 00 0a 00 00 00 06 00 01 00 00 00 07 00 09 00
0000640 0b 00 0c 00 02 00 09 00 00 00 25 00 02 00 01 00
0000660 00 00 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 01
0000700 00 0a 00 00 00 0a 00 02 00 00 00 09 00 08 00 0a
0000720 00 0d 00 00 00 05 01 00 0e 00 00 00 01 00 0f 00
0000740 00 00 02 00 10
0000745使用
javap -v HelloWorld.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
66
67
68Classfile /E:/project/java/demo2/src/main/java/com/example/demo1/test/com/example/demo1/test/HelloWorld.class
Last modified 2022-8-20; size 485 bytes
MD5 checksum fceb30d0e6ac220c6aad48757e3affde
Compiled from "HelloWorld.java"
public class com.example.demo1.test.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // hello world
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #23 // com/example/demo1/test/HelloWorld
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 MethodParameters
#14 = Utf8 args
#15 = Utf8 SourceFile
#16 = Utf8 HelloWorld.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = Utf8 hello world
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#23 = Utf8 com/example/demo1/test/HelloWorld
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (Ljava/lang/String;)V
{
public com.example.demo1.test.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
4: return
LineNumberTable:
line 7: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: 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 9: 0
line 10: 8
MethodParameters:
Name Flags
args
}
SourceFile: "HelloWorld.java"根据JVM规范,类文件结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18ClassFile{
u4 magic;//魔数
u2 minor_version;//小版本号
u2 major_version;//注版本号
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];//类中附加信息
}
魔数
0-3字节,表示他是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09
版本
4-7字节,表示类的版本,00 34(16进制,10进制为52,jdk8)表示jdk8
0-3字节,表示他是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09
常量池
常量池信息对照表
Content Type | Value |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fielaref | 9 |
CONSTANT_Methidref | 10 |
CONSTANT_InterfaceMetodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
常量池长度
8~9字节,表示常量池的长度,00 1f(31)表示常量池有#1-#30项,注意,#0项不计入,也没有值
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09
#1
第#1项,0a(10)表示一个Method信息,00 06和 00 11(17) 表示它引用了常量池中#6和#17项来获得这个方法的【所属类】和【方法名】
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09
#2
第#2项,09 表示一个Field信息,向后数4个字节,00 12(18)和 00 13(19) 表示它引用了常量池中#18和#19项来获得这个方法的【所属类】和【成员变量名】
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09
0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07
#3
第#3项,08 表示一个字符串常量名称,向后数2个字节,00 14(20), 表示它引用了常量池中#20项
0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07
#4
第#4项,0a(10)表示一个Method信息,00 15(21)和 00 16(22) 表示它引用了常量池中#21和#22项来获得这个方法的【所属类】和【方法名】
0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07
#5
第#5项,07 表示一个Class信息,00 17(23) 表示它引用了常量池中#23项
0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07
#6
第#6项,07 表示一个Class信息,00 18(24) 表示它引用了常量池中#24项
0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07
0000040 00 18 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
#7
第#7项,01 表示一个UTF8串,00 06表示字符串的长度,【3c 69 6e 69 74 3e】=【<init>】
0000040 00 18 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
#8
第#8项,01 表示一个UTF8串,00 03表示字符串的长度,【28 29 56】=【()V】
0000040 00 18 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
#9
第#9项,01 表示一个UTF8串,00 04表示字符串的长度,【43 6f 64 65】=【Code】
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
#10
第#10项,01 表示一个UTF8串,00 0f(15)表示字符串的长度,【4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65】=【LineNumberTable】
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 04 6d 61 69
#11
第#11项,01 表示一个UTF8串,00 04表示字符串的长度,【6d 61 69 6e】=【main】
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 04 6d 61 69
0000120 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67
#12
第#12项,01 表示一个UTF8串,00 16(22)表示字符串的长度,【28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56】=【([Ljava/lang/String;)V】
0000120 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67
0000140 2f 53 74 72 69 6e 67 3b 29 56 01 00 10 4d 65 74
#13
第#13项,01 表示一个UTF8串,00 10(16)表示字符串的长度,【4d 65 74 68 6f 64 50 61 72 61 6d 65 74 65 72 73】=【MethodParameters】
0000140 2f 53 74 72 69 6e 67 3b 29 56 01 00 10 4d 65 74
0000160 68 6f 64 50 61 72 61 6d 65 74 65 72 73 01 00 04
#14
第#14项,01 表示一个UTF8串,00 04(16)表示字符串的长度,【61 72 67 73】=【args】
0000160 68 6f 64 50 61 72 61 6d 65 74 65 72 73 01 00 04
0000200 61 72 67 73 01 00 0a 53 6f 75 72 63 65 46 69 6c
#15
第#15项,01 表示一个UTF8串,00 0a(10)表示字符串的长度,【53 6f 75 72 63 65 46 69 6c 65】=【SourceFile】
0000200 61 72 67 73 01 00 0a 53 6f 75 72 63 65 46 69 6c
0000220 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64 2e 6a
#16
第#16项,01 表示一个UTF8串,00 0f(15)表示字符串的长度,【48 65 6c 6c 6f 57 6f 72 6c 64 2e 6a 61 76 61】=【HelloWorld.java】
0000220 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64 2e 6a
0000240 61 76 61 0c 00 07 00 08 07 00 19 0c 00 1a 00 1b
#17
第#17项,0c(12) 表示一个NameAndType【名+类型】,00 07和00 08表示引用了常量池中的#7和#8项【<init>:()V】
0000240 61 76 61 0c 00 07 00 08 07 00 19 0c 00 1a 00 1b
#18
第#18项,07 表示一个Class信息,00 19(25) 表示它引用了常量池中#25项
0000240 61 76 61 0c 00 07 00 08 07 00 19 0c 00 1a 00 1b
#19
第#19项,0c(12) 表示一个NameAndType【名+类型】,00 1a(26)和00 1b(27)表示引用了常量池中的#26和#27项
0000240 61 76 61 0c 00 07 00 08 07 00 19 0c 00 1a 00 1b
#20
第#20项,01 表示一个UTF8串,00 0b(11)表示字符串的长度,【68 65 6c 6c 6f 20 77 6f 72 6c 64】=【hello world】
0000260 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 07 00
#21
第#21项,07 表示一个Class信息,00 1c(28) 表示它引用了常量池中#28项
0000260 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 07 00
0000300 1c 0c 00 1d 00 1e 01 00 21 63 6f 6d 2f 65 78 61
#22
第#22项,0c(12) 表示一个NameAndType【名+类型】,00 1d(29)和00 1e(30)表示引用了常量池中的#29和#30项
0000300 1c 0c 00 1d 00 1e 01 00 21 63 6f 6d 2f 65 78 61
#23
第#23项,01 表示一个UTF8串,00 21(33)表示字符串的长度,【63 6f 6d 2f 65 78 61 6d 70 6c 65 2f 64 65 6d 6f 31 2f 74 65 73 74 2f 48 65 6c 6c 6f 57 6f 72 6c 64】=【com/example/demo1/test/HelloWorld】
0000300 1c 0c 00 1d 00 1e 01 00 21 63 6f 6d 2f 65 78 61
0000320 6d 70 6c 65 2f 64 65 6d 6f 31 2f 74 65 73 74 2f
0000340 48 65 6c 6c 6f 57 6f 72 6c 64 01 00 10 6a 61 76
#24
第#24项,01 表示一个UTF8串,00 10(16)表示字符串的长度,【6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74】=【java/lang/Object】
0000340 48 65 6c 6c 6f 57 6f 72 6c 64 01 00 10 6a 61 76
0000360 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 01 00 10
#25
第#25项,01 表示一个UTF8串,00 10(16)表示字符串的长度,【6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d】=【java/lang/System】
0000360 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 01 00 10
0000400 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d
#26
第#26项,01 表示一个UTF8串,00 03(16)表示字符串的长度,【6f 75 74】=【out】
0000420 01 00 03 6f 75 74 01 00 15 4c 6a 61 76 61 2f 69
#27
第#27项,01 表示一个UTF8串,00 15(21)表示字符串的长度,【4c 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b】=【Ljava/io/PrintStream;】
0000420 01 00 03 6f 75 74 01 00 15 4c 6a 61 76 61 2f 69
0000440 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b 01 00
#28
第#28项,01 表示一个UTF8串,00 13(19)表示字符串的长度,【6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d】=【java/io/PrintStream】
0000440 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b 01 00
0000460 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74
0000500 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 01 00
#29
第#29项,01 表示一个UTF8串,00 07表示字符串的长度,【70 72 69 6e 74 6c 6e】=【println】
0000500 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 01 00
#30
第#30项,01 表示一个UTF8串,00 15(21)表示字符串的长度,【28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56】=【(Ljava/lang/String;)V】
0000500 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 01 00
0000520 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00
访问标识与继承信息
Flag Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC | 0x0001 | 表示是公开的,public |
ACC_FINAL | 0x0010 | 表示是final的 |
ACC_SUPER | 0x0020 | 表示是一个类 |
ACC_INTERFACE | 0x0200 | 表示是一个接口 |
ACC_ABSTRACT | 0x0400 | 表示是一个抽象的 |
ACC_SYNTHETIC | 0x1000 | 表示是人工合成的,不是原生的 |
ACC_ANNOTATION | 0x2000 | 表示是一个注解 |
ACC_ENUM | 0X4000 | 表示是一个枚举 |
修饰符
00 21 表示(0x0001)+(0x0020),表示该class是一个公共的类
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00
全限定名
00 05 表示全限定名的位置,常量池中#5项的位置【com/example/demo1/test/HelloWorld】
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00
父类全限定名
00 06 表示父类全限定名的位置,常量池中#6项的位置【java/lang/Object】
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00
接口信息
00 00 表示该类继承的接口数量,现在表示没有继承接口,如果继承了接口,后面就会跟随接口的描述信息
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00
成员变量信息(Field)
00 00 表示该类的成员变量的数量,现在表示没有成员变量,如果有成员变量,后面会跟随成员变量的信息
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00
FieldType | Type | Interpretation |
---|---|---|
B | byte | 字节类型 |
C | char | 字符类型 |
D | double | 双精度浮点类型 |
F | float | 单精度浮点类型 |
I | int | 整型 |
J | long | 长整型 |
L ClassName; | reference | 引用类型,例如【Ljava/io/PrintStream;】 |
S | short | 短整数 |
Z | boolean | 布尔类型 |
[ | reference | 数组类型 |
方法信息(Method)
方法数量
00 02 表示该类中的方法数量,当前类里面是有2个方法,【无参构造,main方法】
0000560 00 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1d
方法组成
一个方法由访问修饰符,名称,参数描述,方法属性数量,方法属性组成
第一个方法的详细解析
- 蓝色部分的 00 01 表示方法的访问修饰,当前表示是一个公共方法
- 绿色部分的 00 07 表示要查询常量池中的第#7项得到方法的名字【<init>】
- 黄色部分的 00 08 表示要查询常量池中的#8项获得方法的参数和返回值信息【()V】(无参,返回值是void)
- 紫色部分的 00 01 表示方法的属性数量,当前是1个
- 剩下的红色部分代表方法属性
- 00 09表示方法的属性名称,需要查询常量池中的#9 【Code】
- 00 00 00 1d 表示该属性占据的字节码长度是29
- 00 01 表示【操作数栈】最大深度
- 00 01 表示【局部变量表】最大槽(slot)数
- 00 00 00 05 表示【Code】属性内的(代码)字节码长度
- 2a b7 00 01 b1 方法内的具体代码
- 00 00 00 01 表示方法细节属性数量(code属性的有些子属性)
- 00 0a 表示引用了常量池中的#10项,发现是【LineNumberTable】属性(将字节码行号和java代码行号对应)
- 00 00 00 06 【LineNumberTable】属性的字节码总长度,6
- 00 01 【LineNumberTable】属性的长度(有几对)
- 00 00 00 07,00 00 表示【字节码】行号,00 04 表示【java源码】行号
0000560 00 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1d
0000600 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00
0000620 01 00 0a 00 00 00 06 00 01 00 00 00 07 00 09 00
第二个方法的详细解析
- 蓝色部分的 00 09 表示方法的访问修饰,相当于是(08+01)08是静态,当前表示是一个静态公共方法
- 绿色部分的 00 0b (11)表示要查询常量池中的第#11项得到方法的名字【main】
- 黄色部分的 00 0c (12) 表示要查询常量池中的#12项获得方法的参数和返回值信息【([Ljava/lang/String;)V】(如此是引用数组类型,返回值是void)
- 紫色部分的 00 02 表示方法的属性数量,当前是2个
- 剩下的红色部分代表方法属性
- 00 09表示方法的第一个属性名称,需要查询常量池中的#9 【Code】
- 00 00 00 25 表示该属性的占据的字节码长度是37
- 00 02 表示【操作数栈】最大深度2
- 00 01 表示【局部变量表】最大槽(slot)数
- 00 00 00 09 表示【Code】属性内的(代码)字节码长度
- b2 00 02 12 03 b6 00 04 b1 方法内的具体代码
- 00 00 00 01 表示方法细节属性数量(code属性的有些子属性)
- 00 0a 表示引用了常量池中的#10项,发现是【LineNumberTable】属性(将字节码行号和java代码行号对应)
- 00 00 00 0a 【LineNumberTable】属性的字节码总长度,10
- 00 02 【LineNumberTable】属性的长度(有几对)
- 00 00 00 09,00 00 表示【字节码】行号,00 09 表示【java源码】行号
- 00 08 00 0a,00 08 表示【字节码】行号,00 0a (10) 表示【java源码】行号
- 00 0d(13)表示方法的第一个属性名称,需要查询常量池中的#13 【MethodParameters】
- 00 00 00 05 表示该属性的占据的字节码长度是5
- 01 参数的数量
- 00 0e (14) 参数的名字,引用了常量池中的#14项,【args】
- 00 00 访问修饰符
- 00 09表示方法的第一个属性名称,需要查询常量池中的#9 【Code】
0000620 01 00 0a 00 00 00 06 00 01 00 00 00 07 00 09 00
0000640 0b 00 0c 00 02 00 09 00 00 00 25 00 02 00 01 00
0000660 00 00 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 01
0000700 00 0a 00 00 00 0a 00 02 00 00 00 09 00 08 00 0a
0000720 00 0d 00 00 00 05 01 00 0e 00 00 00 01 00 0f 00
附加属性
- 00 01 表示附加属性的数量
- 00 0f(15) 表示引用了常量池中的#15项【SourceFile】
- 00 00 00 02 表示此属性的长度
- 00 10(16) 表示引用了常量池#16项,【HelloWorld.java】
0000720 00 0d 00 00 00 05 01 00 0e 00 00 00 01 00 0f 00
0000740 00 00 02 00 10
0000745
字节码指令
入门
可参考文章:
上面的方法编译后,留下了两组字节码指令没有讲解
构造方法中的5个字节码指令
1 | 2a b7 00 01 b1 |
- 2a =>
aload_0
,从局部变量0中装载引用类型值入栈,及this
,做为下面的invokespecial
构造方法调用的参数 - b7 =>
invokespecial
,编译时方法绑定调用方法(预备调用构造方法),调用那个方法? - 00 01 引用常量池中 #1 项,即【Method java/lang/Object.<init>:()V】
- b1 =>return, viod函数返回
main方法中的9个字节码指令
1 | 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 =>
invokvirtual
,运行时方法绑定调用方法(预备调用成员方法),调用那个方法? - 00 04 引用常量池中#4项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
- b1 =>return, viod函数返回
javap 工具
- jdk提供了
javap
工具来进行反编译,具体操作方式,见上面内容
图解方法执行流程
原始java代码
1 | public class Test1 { |
编译后的字节码文件
1 | Classfile /E:/project/java/demo2/src/main/java/com/example/demo1/test/com/example/demo1/test/Test1.class |
常量池载入运行时常量池
一些比较小的数字,不会存储在常量池中,而是跟方法的字节码存在一起,比如
int a = 10;
中10就是存在字节码中的
而b
的值32768
则是存在常量池中的
方法字节码载入方法区
方法区域中的字节码,会载入到方法区
main 线程开始运行,分配栈帧内存
(stack=2, locals=4)
执行引擎开始执行字节码
bipush 10
- 将一个
byte
压入操作数栈(操作数栈宽度是4,byte
是1,不足的部分会补足),类似的指令还有sipush
将一个short
压入操作数栈,(长度会补足4个字节)ldc
将一个int
压入操作数栈ldc2_w
将一个long
压入操作数栈,(long
为8字节,操作数栈4字节,会分两次压)- 这里小的数字都是和字节码指令存在一起,超过
short
范围的数字存入了常量池
istore_1
- 将操作数栈顶数据弹出,存入局部变量表的
slot_1
- 实现给a变量赋值
ldc #32
- 从常量池中加载#3项数据到操作数栈
- 注意
short.MAX_VALUE
是32767
,所以32768=Short.MAX_VALUE + 1
实际是在编译期间计算好的
istore_2
iload_2
- 将局部变量表中2位置的变量读取到操作数栈
iadd
- 将栈顶两int类型整数相加,结果入栈
istore_3
- 将操作数栈中的数据取出来存到局部变量表3的位置
- 执行之后:
getstatic #4
- 从常量池中找到对应类型,然后从堆中找到该类型的对象(或者创建对象),然后将引用放入操作数栈
- 注意:放入操作数栈的并不是对象,而是对象的引用
iload_3
- 将局部变量表3号位置的数据放入操作数栈
invokevirtual #5
- 找到常量池中#5项
- 定位到方法区 【java/io/PrintStream.println:(I)V】 方法
- 生成新的栈帧,(分配
locals
,stack
等) - 传递参数,执行新栈帧中的字节码
- 执行完毕,弹出栈帧
- 清除main操作数栈内容
return
- 完成main方法调用,弹出main栈帧
- 程序结束
练习-分析 i++
目的:从字节码角度分析 a++
相关题目
源码:
1 | /** |
1 | Classfile /E:/project/java/demo2/src/main/java/com/example/demo1/test/com/example/demo1/test/com/example/demo1/test/com/example/demo1/test/Test2.class |
分析:
- 注意
iinc
指令是直接在局部变量slot
上进行运算 a++
和++a
的区别是先执行iload
还是先执行iinc
a++
先执行iload
,然后再iinc
自增++a
先执行iinc
自增,再执行iload
bipush 10
- 把10先放到操作数栈
istore_1
- 把操作数栈中栈顶的数据弹出,存到局部变量表一号桶位
iload_1
- 把局部变量表一号槽位中的数据读取到操作数栈
iinc 1,1
- 执行
a++
- 局部变量表一号槽位数据自增1
- 前面的1表示局部变量表的槽位,后面的1表示自增1
- 该操作直接在局部变量表中完成,并不会影响操作数栈中的数据
iinc 1,1
- 执行
++a
- 局部变量表一号槽位自增1(不会影响到操作数栈)
iload_1
- 把局部变量表一号槽位的数据加载到操作数栈(12加载到操作数栈)
iadd
- 将栈顶两int类型整数相加,结果入栈
iload_1
a--
先iload
再iinc
- 先将12放入操作数栈
iinc 1,-1
istore_2
- 把操作数栈顶的数据弹出到局部变了表2号槽位
条件判断指令
详情见《Java字节码指令大全》<1.11 控制流指令——条件跳转指令>
Java字节码指令大全 | YS (gitee.io)
说明:
- byte、short、char都会按 int 比较,因为操作数栈是4个字节
- goto用来跳转到指定行号的字节码
循环控制指令
- 其实循环也是依靠上面的判断指令完成的
源码:1
2
3
4
5
6
7
8public class Test {
public static void main(String[] args) {
int a = 0;
while (a < 10){
a++;
}
}
}编译后的字节码如下:
iconst_0 准备一个0值入栈
istore_1 将操作数栈顶的数据,也就是刚刚的0值,存放到局部变量表1号位
iload_1 将局部变量表1号位,也就是刚刚的0值存入操作数栈
bipush 准备一个数字10入栈
if_icmpge 若栈顶两int类型值前大于等于后则跳转,也就说0所代表的变量大于等于10的时候,可以跳转到第14行字节码
iinc 局部变量表1号位自增1,也就是0值的位置,现在为1
goto 跳转到第2行字节码执行
然后会继续循环判断
以此实现循环,当满足条件的时候就会跳转到14行return
字节码:
1 | 0: iconst_0 |
构造方法
<cinit>()V
1 | public class Test2 { |
静态变量的赋值操作和静态代码块的内容,最终会被合并成一个操作
编译器会按从上向下的顺序,收集所有static几台代码块和静态成员赋值的代码,合并为一个特殊的方法<cinit>()V
:
1 | 0: bipush 10 |
<cinit>()V
方法会在类加载的初始化阶段被调用
<init>()V
源码:
1 | public class Test3 { |
编译器会按从上到下的顺序,收集所有
{}
代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后面
先调用父类Object
类的<init>
方法
然后给this.a
赋值S1
然后给this.b
赋值为20
(并不是先赋值初始化的10
,而是处于上面的代码块的20
)
然后给this.b
赋值为10
给this.a
赋值为S2
然后下面的是原本构造方法里面的代码
将局部变量表1号位的数据赋值给this.a
将局部变量表2号位的数据赋值给this.b
1 | public com.example.demo1.test1.Test3(java.lang.String, int); |
方法调用
1 | public class Test4 { |
通过字节码可以看出,调用
构造方法
和私有普通方法
以及final方法
,都是使用的invokespecial
指令
调用普通公共方法
使用的是invokevirtual
指令
调用静态方法
使用的是invokestatic
指令
invokespecial
:编译时方法绑定调用方法invokestatic
:调用静态方法invokevirtual
:运行时方法绑定调用方法invokespecial
和invokestatic
属于静态绑定,在字节码生成的时候,就已经知道需要找到那个类的那个方法了,性能相比之下会更高一点invokevirtual
并不能在生成字节码的时候就知道调用的是那个类的,因为java多态的原因,方法可能会被重写,有可能调用的是父类的方法,也可能调用的是子类额方法
1 | 0: new #2 // class com/example/demo1/test1/Test4 |
多态的原理
示例代码如下,内容很简单
创建一个Animal
的抽象类,他有两个子类实现了他的抽象方法eat()
之后在test()
方法中以父类去调用eat()
,main()
方法中传入不同的实现
1 | /** |
分析
- 在方法启动的时候需要添加以下参数,禁止虚拟机自动压缩指针
-XX:-UseCompressedOops -XX:-UseCompressedClassPointers
- 使用
System.in.read();
暂停程序,然后使用jps
命令获取进程id - 运行HSDB工具
- 进入JDK安装目录,执行下面的命令:
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
- 进入图形界面输入程序的进程id
- 打开图形界面,点击file再点击
Attach to HotSpot...
,之后输入进程id,就会连上当前进程 - 然后点击
Tools
,再点击Find Object by Query
,之后输入查询语句,类似于sql语句select + 别名 + from + 需要查询的类的全限定名 + 别名
- 如:
select d from com.example.demo1.test1.Dog d
- 如下图所示,可以在
Inspector
框里面查看对象的相关信息,不过这些并不是对象的真正信息 - 点击标记1处,会弹出标记2处的框,然后使用mem指令查看对象头信息的地址信息
mem
命令后面跟随的是标记3处的地址信息,后面的2表示要查看几个mark word- 在标记1处敲击回车,就会显示出类型的指针
- 然后再在
Tools
处打开一个Inspector
框,使用刚刚查出来的类型指针查询 - 如标记2处,就是对象在虚拟机中完整的类型表示
- 类的多态方法都存在一个
vtable
表中,也就是最后一行箭头所指的位置 vtable
的和当前类型指针的偏移量是1b8
,所以只需要以16进制将当前指针地址加上1b8
就可以得到vtable
的地址0x000000001c7c3ca0 + 1b8 = 0x000000001c7c3e58
- 下面就是执行
mem
指令,从上面第二张图可以看到vtable_len=6
,所以查6个markeme 0x000000001c7c3e58 6
- 如图所示,查询出对应的work,一共有六个,
Dog
类对应的多态方法 - 可以通过
Class Browser
查询Dog
类,对应一下这六个方法 - 从图中可以看出,
0x000000001c453c48
对应的是cat()
方法 - 如图所示,
0x000000001c453768
对应的是Dog
类的父类Animal
类的toString()
方法 toString()
方法也是一个多态方法,只不过Dog
并没有重写这个方法,所以使用的是父类的方法- 剩下的四个方法均来自
Animal
的父类Object
,对应关系如下图所示 finalize()
:该方法并没有在Animal
和Dog
类中重写,所以使用的是所有类的超类Object
类的equals()
:该方法同样没有重写,使用的也是Object
中的hashCode()
:同样使用的是Object
中的clone()
:同样使用的是Object
中的- 虚方法表是在类加载链接阶段就会诞生的,所以就是说在类链接阶段,就已经配置好了每个方法需要调用的具体实现
小结
执行invokevirtual
指令时
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际
Class
Class
结构中有vtable
,它在类加载的链接阶段
就已经根据方法的重写规划生成好了- 查表的到方法的具体地址
- 执行方法中的字节码
异常梳理
try-catch
1 | public class Test6 { |
从下面的字节码中可以看出:
0值入栈,然后放入局部变量表中,然后数值10入栈,然后赋值给局部变量表1号位,然后
goto
到12行
如果没有异常,那么执行到5行后就会跳转到12行结束不过可以看出,多出来了一个
Exception table
的结构,[from,to)
是前闭后开的检测范围,一旦这个范围内的字节码执行出现了异常,则通过type
匹配异常类型,如果一致则进入target
所指示的行号
8行的字节码指令astore_2
是将异常对象引用存入局部变量表的2号位
1 | public static void main(java.lang.String[]); |
多个catch块的情况(single-catch)
1 | public class Test7 { |
从编译出的字节码中可以看出,多个
catch
一样都是监控的第2到5行字节码(不包含5)
区别在于后面的type
,会根据type
匹配到不同的异常之后,进行不同的跳转行(根据target判断跳转到哪一行)
1 | public static void main(java.lang.String[]); |
multi-catch的情况
1 | public class Test8 { |
编译后的字节码如下所示:
三个异常监控的范围都是0-22
,然后根据type
去进行匹配,如果匹配到了就会跳转到25
行
和多个catch
块的形式唯一的区别就是匹配到异常之后跳转的位置是相同的
1 | public static void main(java.lang.String[]); |
finally
1 | public class Test9 { |
从下面的字节码中可以看出,
finally
的代码块其实就是在编译的时候复制到每一个代码块的最后位置
如,try
,最后两行的字节码就是finally
里面的内容catch
里面的也是一样
不过从异常表中可以看出,多了两个any
的异常监控,any
表示所有异常any
监控2-5
行,则是为了防止有超出Exception
的异常发生,比如Exception
的父类或者兄弟类
监控11-15
行,这时为了防止执行Exception
的catch
块的时候再发生异常
通过字节码来控制,保证每一块代码块跳转之前都会执行finally
里面的异常,即便是发生了未知的异常,也能够跳转到finally
的代码块中执行
整个字节码一共有三个分支:1.try分支
2.catch分支
3.catch没有匹配到的异常的分支(最后会把异常再上抛)如果在
finally
中return
,将会吞掉代码块中的异常
1 | public static void main(java.lang.String[]); |
synchronized
1 | public class Test { |
字节码如下所示,其实和finally的字节码类似
会将一份普通代码分为两个分支,一个是正常结束的分支,一个是异常结束的分支,无论在哪个分支上,都会复制一份解锁的字节码,以保证加锁后肯定能够解锁
1 | public static void main(java.lang.String[]); |
编译期处理
所谓的语法糖,其实就是指
java
编译器把*.java
源代码编译为*.class
字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java
编译器给我们的一个额外福利
注意:以下代码的分析,借助了javap
工具,idea的反编译功能,idea插件jclasslib
等工具。另外,编译器转换的结果直接就是class字节码,只是为了便于阅读,给出了几乎等价的java源码方式,并不是编译器还会转换出中间的java源码,切记。
默认构造器
1 | public class Test1 { |
- 编译成class之后:
1
2
3
4
5
6
7public class Test1 {
//这个无参构造器就是编译器帮助我们加上的
public Test1() {
//即调用父类Object的无参构造,调用java/lang/Object.<init>:()V
super();
}
}
自动拆装箱
- 这个特性是JDk5开始加入的,代码片段1:
1
2
3
4
5
6public class Test2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
} - 在JDK5之前按照上面这样写是无法进行编译的,必须写成下面代码片段2的形式:
1
2
3
4
5
6public class Test2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}
泛型集合取值
- 泛型也是在JDK5开始加入的特性,但java在编译泛型代码后,会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都是当做了Object类型来处理的
1 | public class Test3 { |
所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换操作
1
2//需要将 Object 转换成 Integer
Integer x = (Integer)list.get(0);如果前面的x变量类型修改为int基本类型,那么最终生成的字节码是:
1
2//需要将 Object 转换成 Integer ,并执行拆箱操作
int x = (Integer)list.get(0).intValue();下面是编译之后的字节码
- 第11行对应的就是
list.add(10)
,因为list
要放的是包装类型,10
是基本类型,所以先调用了Integer.valueOf()
方法把10
转换成了Integer
类型 - 第14行可以看到,其实调用的就是
list.add(Object o)
方法,并没有泛型的类型 - 第22行也可以看到,获取数据调用的
List.get()
的返回值也是一个Object
类型 - 第27行,
checkcast
表示强转,将get
的Object
类型强转成Integer
类型
- 第11行对应的就是
泛型擦除掉的是字节码上的泛型信息,可以看到
LocalVariableTypeTable
仍然保留了方法参数泛型的信息
1 | public static void main(java.lang.String[]); |
- 方法上的泛型信息,可以通过反射
1
public Set<Integer> test(List<String> list, Map<Integer,Object> map){return null;}
1 | Method test = Test4.class.getMethod("test",List.class,Map.class); |
输出结果:
1 | 原始类型 - interface java.util.List |
可变参数
可变参数也是JDk5开始加入的新特性
1
2
3
4
5
6
7
8
9public class Test5 {
public static void foo(String... args){
String[] array = args;//直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo("hello","world");
}
}可变参数
String... args
其实就是一个String[] args
,从代码中的赋值语句就可以看出来同样java编译会在编译期间将上述代码转换为:
1
2
3
4
5
6
7
8
9public class Test5 {
public static void foo(String[] args){
String[] array = args;//直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo(new String[]{"hello","world"});
}
}
注意:
如果调用了foo()
无参,则等价代码为foo(new String[]{})
,创建了一个空的数组,而不会传递null
进去
foreach循环
在JDK5开始引入的语法糖,数组循环:
1
2
3
4
5
6
7
8public class Test6 {
public static void main(String[] args) {
int[] array = {1,2,3,4,5};//数组赋值的简化写法,也是语法糖
for (int i : array) {
System.out.println(i);
}
}
}会被编译转换为:
1
2
3
4
5
6
7
8
9public class Test6 {
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
8public class Test7 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5);
for (Integer i : list) {
System.out.println(i);
}
}
}
实际会被编译器转换为迭代器调用:
1 | public class Test7 { |
注意:foreach
循环写法,能够配合数组,以及所有实现了Iterable
接口的集合类一起使用,其中Iterable
用来获取集合的迭代器(Iterable
)
switch 字符串
- 从JDK7开始,switch可以用于字符串和枚举类,这个功能其实也是语法糖,例如:
1
2
3
4
5
6
7
8
9
10
11public class Test8 {
public static void choose(String str) {
switch (str) {
case "hello":
System.out.println("H");
break; case "world":
System.out.println("W");
break;
}
}
}
注意:
switch配合String和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码自然清楚
上面的代码会被编译器转换成:
1 | public class Test8 { |
从上面的代码可以看到,字符串使用
switch
其实是被拆成了两个整数的switch
来执行的,第一遍是根据字符串的hashCode
和equals
将字符串转换成对应byte
类型,第二遍才是利用byte
执行进行比较
为什么第一遍时必须比较hashCode
又利用equals
比较呢?
hashCode
是为了提高效率,减少可能的比较,而equals
是为了防止hashCode
冲突,例如BM
和C.
,这两个字符串的hashCode
都是2123
switch 枚举
switch
枚举的例子,原始代码:
1 | public class Test { |
转换后代码:
1 | public class Test1 { |
枚举类
JDK7新增了枚举类,以前面的性别枚举为例:
1 | public enum Sex { |
1 | public final class Sex extends Enum<Sex> { |
try-with-resources
JDK7开始新增了对需要关闭的资源处理的特殊语法 try-with-resources
1 | try(资源变量 = 创建资源对象){ |
其中资源对象需要实现AutoCloseable接口,例如 InputStream
、OutputStream
等接口都实现了AutoCloseable接口,使用try-with-resources
可以不写finally语句块,编译器会帮助生成关闭资源代码,例如:
1 | public class Test2 { |
编译转换后:
1 | public class Test2 { |
为什么要设计一个addSuppressed(Throwable e)
(添加被压制异常)的方法呢?
是为了防止异常信息丢失,如下所示:
1 | public class Test3 { |
输出:
除0异常和手动抛出来的close异常都未丢失
1 | java.lang.ArithmeticException: / by zero |
方法重写时的桥接方法
一般来说方法重写分两种情况:
- 父类和子类返回值完全一致
- 子类返回值可以是父类返回值的子类
1 | class A extends B{ |
对于子类,java编译器会做如下处理:
1 | class A extends B{ |
其中桥接方法比较特殊,仅对java虚拟机可见,并且与原来的public Number m()没有命名冲突,可以使用下面的反射代码来验证:
1 | public static void main(String[] args) { |
输出:
1 | public java.lang.Integer com.example.demo1.test3.A.m() |
匿名内部类
源代码:
1 | public class Test5 { |
转换后代码:
1 | //额外生成的一个类 |
1 | public class Test5 { |
引用局部变量的匿名内部类,源代码:
1 | public class Test5 { |
转换后代码:
1 | //额外生成的代码 |
1 | public class Test5 { |
注意:
这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是final的原因
因为在创建Test5$1
对象时,将a
的值赋值给了Test5$1
对象的val$a
属性,所以a
不应该再发生变化了,如果变化,那么val$a
属性就没有机会再跟着一起变化
类加载阶段
加载
- 将类的字节码载入方法区中,内部采用
C++
的instanceKlass
描述java
类,他的重要field
有:_java_mirror
即java
的类镜像,例如对String
来说,String.class
,作用就是把klass
暴露给java
使用_super
即父类_fields
即成员变量_methods
即方法_constants
即常量池_class_loader
即类加载器_vtable
虚方法表_itable
接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
注意:
instanceKlass
这样的【元数据】是存储在方法区的(jdk1.8之后的元空间),但_java_mirror
是存储在堆中的- 可以通过前面介绍的
HSDB
工具查看 - 类加载之后,会在堆中保留一份信息,也就是类对象,类对象中会有地址指向元空间中的元数据,元空间中
_java_mirror
也会有指针指向堆中的元数据,可以通过类对象创建实例对象,创建出来的实例对象中,对象头里面会保留类对象信息
链接
验证
- 验证类是否符合JVM规范,安全性检查
用可以编辑16进制文件的工具修改编译后的class内容,然后在控制台运行:
(报错内容为magic魔数部分值不正确)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Error: 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 com/example/demo1/test3/Test6
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:338)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
准备
- 为
static
变量分配空间,设置默认值static
变量在JDK7之前存储于instanceKlass
末尾,从JDK7开始,存储于_java_mirror
末尾static
变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成- 如果
static
变量是final
的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成 - 如果
static
变量是final
的,但属于引用类型,那么赋值也会在初始化阶段完成
解析
- 将常量池中的符号引用解析为直接引用
- 符号引用替换为直接引用的理解:
- 符号引用:比如一个类中引用了其他类,但是JVM不知道实际引用的其他类地址会在哪,所以就会用符号引用代表,等到解析的时候,再根据唯一符号引用去找到其他类的地址,不止是其他类的代表,符号引用也可以代表方法,字段等,主要注意的是符号引用与虚拟机布局无关,引用的目标不一定已经加载到内存
- 直接引用:直接引用与虚拟机布局有关,如果使用直接引用,那么引用的目标一定已经加载到内存中
- 符号引用要转换成直接引用才有效,这也说明直接引用的效率要比符号引用高,那么为什么要用符号引用呢?这时 因为类加载之前,javac会将源代码编译成class文件,这个时候javac是不知道被编译的类中所引用的类、方法或者变量他们的引用地址在哪,所以只能用符号引用来代表,当然,符号引用也是要准备JVM虚拟机规范的
初始化
<cinit>()V 方法
初始化即调用<cinit>()V
,虚拟机会保证这个类【构造方法】的线程安全
发生的时机
类的初始化是懒惰的,并不会在程序启动的时候全部初始化,而是等需要的时候初始化
会导致初始化的情况:
main
方法所在的类,总是被首先初始化- 首次访问这个类的静态变量或者静态方法时
- 子类初始化,如果父类还没有初始化,会引发
- 子类访问父类静态变量,只会触发父类的初始化
Class.forName()
new
会导致初始化
不会导致初始化的情况:
- 访问类的
static final
静态常量(基本类型和字符串类型)不会触初始化 类对象.class
不会触发初始化- 创建该类的数组不会触发初始化
- 类加载器的
loadClass
方法 Class.forName()
方法的参数2为false
时
1 | public class Test { |
类加载器
以JDK8为例:
名称 | 描述 | 加载目录 | 说明 |
---|---|---|---|
Bootstrap ClassLoader | 启动类加载器 | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | 扩展类加载器 | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader | 应用程序类加载器 | classpath | 上级为Extension |
自定义类加载器 | 自定义 | 自定义类加载器 | 上级为Application |
启动类加载器
使用Bootstrap类加载器加载类:
1 | public class F { |
执行:
1 | public class Test1 { |
输出:
1 | $ java -Xbootclasspath/a:. com.example.demo1.test5.Test1 |
-Xbootclasspath
表示设置bootclasspath
- 其中
/a:.
表示将当前目录追加至bootclasspath
之后 - 可以使用这个方法替换核心类
java -Xbootclasspath:<new bootclasspath>
完全替换路径java -Xbootclasspath/a:<向后追加路径>
java -Xbootclasspath/p:<向前追加路径>
扩展类加载器
源代码:
1 | public class G { |
执行
1 | public class Test2 { |
输出:
1 | classpath G init |
- 可以看到,默认情况下使用的是
AppClassLoader
类加载器 - 那么怎么才能使用扩展类加载器加载呢?
- 将对应的类,打成jar包,放到扩展目录下
源代码:
1 | public class G { |
更改输出内容,打包放入扩展目录,可以根据输出内容的不同,判断是加载的哪一个类
使用命令打包 jar -cvf my.jar com/example/demo1/test5/G.class
将jar包放入JAVA_HOME/jre/lib/ext目录下后,改回类中的输出内容,再次运行代码:
1 | ext G init |
从输出的内容可以看出,使用的是 ExtClassLoader
类加载器
双亲委派模式
所谓双亲委派,就是指调用类加载器的loadClass
方法时,查找类的规则
注意:
这里的双亲,翻译为上级似乎更为合适,因为他们并没有继承关系
1 | protected Class<?> loadClass(String name, boolean resolve) |
线程上下文类加载器(SPI)
1 | String url = "jdbc:mysql://localhost:3306/test"; |
上面是获取数据库连接,并没有写去加载哪一个数据库驱动
不需要Class.forName("com.mysql.jdbc.Driver")
来加载,也可以正确加载驱动
代码中并没有指定使用的是哪一个数据库驱动,是如何做到的呢?
源码:(代码中省略了部分代码,只保留了需要观察的部分)
1 | public class DriverManager { |
从代码中可以看出,静态代码块中主要逻辑在 loadInitialDrivers();
方法中,那我们来看一下 loadInitialDrivers();
的实现:
1 | private static void loadInitialDrivers() { |
- 上面代码标记1处的
ServiceLoader
就是大名鼎鼎的Service Provider Interface (SPI)
- 约定如下:
- 在jar包的
META-INF/service
包下,以接口全限定名为文件名,文件内容是实现类名称
- 在jar包的
如图,mysql的驱动:
这样就可以通过下面的方法使用:
1
2
3
4
5ServiceLoader<接口类型> loader = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iterator = loader.iterator();
while (iterator.hasNext()){
iterator.next();
}接着看
ServiceLoader.load()
方法1
2
3
4
5public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序加载器,它内部又是由
Class.forName()
调用了线程上下文类加载器完成类加载,具体代码在ServiceLoader
的内部类LazyIterator
中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
27private 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
}
自定义类加载器
什么时候需要自定义类加载器?
- 想要加载非
classpath
随意路径中的类文件 - 都是通过接口来实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
实现步骤
- 继承
ClassLoader
类 - 要准从双亲委派机制,重写
findClass
方法- 注意不是重写
loadClass
方法,否则不会走双亲委派机制
- 注意不是重写
- 读取类文件的字节码
- 调用父类的
defineClass
方法来加载类 - 使用者调用该类加载器的
loadClass
方法
如下所示,MyClassLoader 就是我们自定义的类加载器,可以到自定义的目录下进行类加载:
1 | public class Test { |
运行期优化
即时编译
分层编译
运行下面的例子,可以看到,每次创建的一千个对象的时间,会从最开始的每次31000
纳秒,到二百次之后降低到11900
左右
1 | public class Test { |
原因是什么?
JVM将执行状态分成了5个层次:
- 0层:解释执行(Interpreter)
- 1层:使用C1即时编译器编译执行(不带profiling)
- 2层:使用C1即时编译器编译执行(带基本的profiling)
- 3层:使用C1即时编译器编译执行(带完全的profiling)
- 4层:使用C2即时编译器编译执行
profiling
是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回放次数】等
即时编译器(JIT)和解释器的区别:
- 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- JIT是将一些字节码编译为机器码,并存入
Code Cache
,下次遇到相同的代码,直接执行,无需编译 - 解释器是将字节码解释为针对所有平台都通用的机器码
- JIT会根据平台类型,生成平台特定的机器码
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;
另一方面,对于占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度;
执行效率上简单比较一下Interpreter < C1 < c2
总的目标是发现热点代码(hotspot名称的由来),优化之
上面的一种优化手段称之为【逃逸分析】,检查新创建的对象是否逃逸。
可以使用-XX:-DoEscapeAnalysis
关闭逃逸分析,再运行刚才的示例观察结果
如果开启逃逸分析,编译器发现新创建的对象在循环外面没有调用,那么就会选择不创建对象
方法内联
(Inlining)
1 | public static int square(final int i) { |
1 | System.out.println(square(9)); |
如果发现
square()
方法是热点方法,并且长度不太长,会进行内联,所谓的内联就是把方法内代码拷贝、黏贴到调用者的位置
1 | System.out.println(9 * 9); |
还能够进行常量折叠(constant folding
)的优化
1 | System.out.println(81); |
可以使用参数打印内联信息-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
从输出的结果可以看到,自定义的 square()
方法已经内联优化
也可以使用参数禁用内联-XX:CompileCommand=dontinline,*Test.square
(*号表示所有包下的Test类中的square方法)
字段优化
JWH基准测试请参考 : OpenJDK: jmh
创建Maven工程,添加以下依赖:
1 | <dependency> |
编写基准测试代码:
1 | //预热,预热2伦,每次间隔1毫秒 |
上面代码运行之后,输出结果:
Score项为评分,可以看到,三种循环方式,评分并没有太大的差异
1 | Benchmark Mode Cnt Score Error Units |
可以把doSum()
方法上的注解参数修改为@CompilerControl(CompilerControl.Mode.DONT_INLINE)
,禁止方法内联
再次运行代码:
可以看出,得分比较与第一次,全都有了大幅度下降
1 | Benchmark Mode Cnt Score Error Units |
分析:
在刚才的示例中,doSum()
方法是否内联会影响elements
成员变量读取的优化
如果doSum()
方法内联了,刚才的test1
方法会被优化成下面的样子(伪代码):
1 |
|
可以省去 1999
次Field
读取操作
但如果doSum()
方法没有内联,则不会有上面的优化
反射优化
1 | public class Test { |
foo.invoke()
前面0-15次调用,使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现
之后会生成一个新的实现
下面的代码是 NativeMethodAccessorImpl
类删减之后的内容
可以看到,每次调用 this.numInvocations
都会加一,当大于ReflectionFactory.inflationThreshold()
的结果时就会进入判断中的内容,而判断中会通过字节码技术,重新生成一个 MethodAccessorImpl
的实现类
1 | class NativeMethodAccessorImpl extends MethodAccessorImpl { |
可以通过 阿里巴巴提供的工具包 arthas-boot 来查看通过字节码技术生成的类 下载路径: https://arthas.gitee.io/arthas-boot.jarjava -jar .\arthas-boot.jar
工具启动后会列出来对应的程序,主要选择自己想要查看的程序即可
然后使用 jad sun.reflect.GeneratedMethodAccessor1
命令,后面是需要查看的类的全限定名称
下面是自动生成的代码:
1 | /* |
注意:
通过查看 ReflectionFactory
源码可以知道:
sun.reflect.noInflation
可以用来禁止膨胀(直接生成sun.reflect.GeneratedMethodAccessor1
,但是首次生成比较耗时,如果反射只使用一次,不划算)sun.reflect.inflationThreshold
可以修改膨胀阀值