JVM之字节码技术
# 1 类文件结构
一个简单的 HelloWorld.java 文件
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
2
3
4
5
将其执行 javac -parameters -d . HelloWorld.java
编译后的 HelloWorld.class 文件如下(Notepad++使用十六进制进行查看)
根据 JVM 规范,类文件结构如下,这是在 JVM 运行中的所有字节码文件都需要遵循的
class-file {
u4 magic; //魔数
u2 minor_version; //小版本号
u2 major_version; //大版本号
u2 constant_pool_count; //常量池中常量个数+1
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags; //类的访问控制符标识(public,static,final,abstract等)
u2 this_class; //该类的描述(值为对常量池的引用,引用的值为CONSTANT_Class_info)
u2 super_class; //父类的描述(值为对常量池的引用,引用的值为CONSTANT_Class_info)
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; //属性数,包括InnerClasses,EnclosingMethod,SourceFile等
attribute_info attributes[attributes_count]; //属性表集合
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1.1 魔数
0-3 字节,表示它是否是【class】类型的文件
# 1.2 版本
4-7 字节,表示类的版本 00 34(52) 表示是 Java 8
# 1.3 常量池
8-9 字节,表示常量池长度,00 1f (31) 表示常量池有 #1- #30 项,注意 #0 项不计入,也没有值,可以发现一个 Class 文件中最多具备 65535(2^16) 个常量,它包括了以下所有类型。
常量池类型映射表,目前为止 JVM 一共定义了 14 种类型的常量。
Constant Type | Value | Length(Byte) | Tip |
---|---|---|---|
CONSTANT_Class | 7 | 2 | Class信息 |
CONSTANT_Fieldref | 9 | 4 | 成员变量信息,前2为 CONSTANT_Class,后2为 CONSTANT_NameAndType |
CONSTANT_Methodref | 10 | 4 | 方法信息,前2为 CONSTANT_Class,后2为 CONSTANT_NameAndType |
CONSTANT_InterfaceMethodref | 11 | 4 | 接口方法信息,前2为 CONSTANT_Class,后2为 CONSTANT_NameAndType |
CONSTANT_String | 8 | 2 | 字符串常量名称 |
CONSTANT_Integer | 3 | 4 | int值 |
CONSTANT_Float | 4 | 4 | float值 |
CONSTANT_Long | 5 | 8 | long值 |
CONSTANT_Double | 6 | 8 | double值 |
CONSTANT_NameAndType | 12 | 4 | 名称和类型信息,前2为 命名信息,后2为 类型信息 |
CONSTANT_Utf8 | 1 | 2 | 长度记录了该字符串的长度,通过该记录的长度来读取后续相应字节长度信息 |
CONSTANT_MethodHandle | 15 | - | 表示方法句柄(后续遇到再补充) |
CONSTANT_MethodType | 16 | - | 表示方法类型(后续遇到再补充) |
CONSTANT_InvokeDynamic | 18 | - | 表示一个动态方法调用点(后续遇到再补充) |
根据以上映射表来对其余字节码的分析
常量池中 #1 项 0a(10),由映射表得知是一个 Method 信息,其通过后续的 00 06(6) 和 00 11(17) 分别表示该方法的【所属类】引用常量池中 #6 项、【方法名】引用常量池中 #17 项。
常量池中 #2 项 09(9),由映射表得知是一个 Field 信息,其通过后续的 00 12(18) 和 00 13(19) 分别表示该成员变量的【所属类】引用常量池中 #18 项、【成员变量名】引用常量池中 #19 项。
常量池中 #3 项 08(8),由映射表得知是一个字符串常量名称,其通过后续的 00 14(20)表示其引用了常量池中 #20 项。
依此类推进行其他常量池的解析,这里就不演示了,了解一个解析形式即可。
以上分析结果的常量池大概就是使用反编译后的常量池结果,而且可以看到所有的字符串常量池中的信息一一对应
#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 // 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 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
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
# 1.4 访问标识与继承信息
解析完常量池后的8的字节,就是该部分的信息
- 2:该class类型和访问修饰符Flag
- 2:通过常量池找到本类全限定名
- 2:通过常量池找到父类全限定名
- 2:表示接口数量
Flag Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC | 0x0001 | public 访问权限 |
ACC_FINAL | 0x0010 | final 修饰符 |
ACC_SUPER | 0x0020 | Treat superclass methods specially when invoked by the invokespecial instruction. 父类调用方式 |
ACC_INTERFACE | 0x0200 | interface 类型 |
ACC_ABSTRACT | 0x0400 | abstract 类型 |
ACC_SYNTHETIC | 0x1000 | 通过合成/生成的代码,没有源代码 |
ACC_ANNOTATION | 0x2000 | annotation 类型 |
ACC_ENUM | 0x4000 | enum 类型 |
# 1.5 Field 信息
解析完以上的信息的后续2字节,表示成员变量的数量,此案例文件并没有成员变量
FieldType | Type | Interpretation |
---|---|---|
B | byte | |
C | char | |
D | double | |
F | float | |
I | int | |
J | long | |
LClassName ; | reference | 引用类型 |
S | short | |
Z | boolean | |
[ | reference | 一维数组 |
# 1.6 Method 信息
解析完以上的信息的后续2字节,表示方法的数量
一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成
就是解析为以下反编译文件(使用十六进制分析过程过于庞大)
{
public 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 1: 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 3: 0
line 4: 8
MethodParameters:
Name Flags
args
}
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
# 1.7 附加属性
解析为以下反编译文件的内容
SourceFile: "HelloWorld.java"
参考文献 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
# 2 字节码指令
# 2.1 入门
分析方法中的字节码指令信息
构造方法对应的字节码信息如下图,对应的反编译信息如下文本块所示
public 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 1: 0
2
3
4
5
6
7
8
9
10
有以下对应关系
- 2a => aload_0:加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
- b7 => invokespecial:预备调用构造方法,哪个方法呢?
- 00 01:引用常量池中 #1 项,即【 Method java/lang/Object.""😦)V 】
- b1:表示返回
main方法对应的字节码信息如下图,对应的反编译信息如下文本块所示
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 3: 0
line 4: 8
MethodParameters:
Name Flags
args
2
3
4
5
6
7
8
9
10
11
12
13
14
15
有以下对应关系
- 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 (opens new window)
# 2.2 javap 工具
自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件
使用命令 javap -v HelloWorld.class
,查看完整的反编译信息
Classfile /G:/Idea_workspace/JVM/src/main/java/HelloWorld.class
Last modified 2022-12-3; size 462 bytes
MD5 checksum f882f9f216fff39f1b95dd6b16b209a7
Compiled from "HelloWorld.java"
public class 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 // 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 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 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 1: 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 3: 0
line 4: 8
MethodParameters:
Name Flags
args
}
SourceFile: "HelloWorld.java"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 2.3 图解方法执行流程
# 1) 原始 Java 代码
package com.zhuhjay.demo3;
public class Demo3_1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
2
3
4
5
6
7
8
9
# 2) 编译后的字节码文件
使用命令 javap -v Demo3_1.class
进行反编译,查看字节码
Classfile /G:/Idea_workspace/JVM/target/classes/com/zhuhjay/demo3/Demo3_1.class
Last modified 2022-12-3; size 619 bytes
MD5 checksum 2f036be3ec6ee9204c53f83b17dacc1f
Compiled from "Demo3_1.java"
public class com.zhuhjay.demo3.Demo3_1
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/zhuhjay/demo3/Demo3_1
#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/zhuhjay/demo3/Demo3_1;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 Demo3_1.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/zhuhjay/demo3/Demo3_1
#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.zhuhjay.demo3.Demo3_1();
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
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zhuhjay/demo3/Demo3_1;
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: 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 9: 0
line 10: 3
line 11: 6
line 12: 10
line 13: 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: "Demo3_1.java"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# 3) 常量池载入运行时常量池
JVM 通过类加载器将以上字节码数据读入,将常量池数据存储到运行时常量池(属于方法区的一部分)中
- #3 这个数据的来源是
Short.MAX_VALUE + 1
,java源码中比较小的数值并不会存储在常量池中,而是与方法的字节码指令存储在一起,一旦超出了Short.MAX_VALUE(32767)
就会存储到常量池中。

# 4) 方法字节码载入方法区

# 5) main 线程开始运行,分配栈帧内存
会根据以下信息来进行栈帧分配,操作数栈深度为2,局部变量表长度为4
stack=2, locals=4, args_size=1

# 6) 执行引擎开始执行字节码
bipush 10
- 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
- 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

istore_1
- 将操作数栈顶数据弹出,存入局部变量表的 slot 1


以上操作代表源代码的 int a = 10;
这个赋值操作
ldc #3
- 从常量池加载 #3 数据到操作数栈
- 注意
Short.MAX_VALUE
是 32767,所以32768 = Short.MAX_VALUE + 1
实际是在编译期间计算好的(常量折叠优化)

istore_2
- 将操作数栈顶数据弹出,存入局部变量表的 slot 2

以上操作代表源代码的 int b = Short.MAX_VALUE + 1;
这个赋值操作
iload_1
- 将局部变量表中的 slot 1 数据读取复制到操作数栈中

iload_2
- 将局部变量表中的 slot 2 数据读取复制到操作数栈中

iadd
- 将操作数栈中的数据进行相加的操作


istore_3
- 将操作数栈顶数据弹出,存入局部变量表的 slot 3

以上操作代表源代码的 int c = a + b;
这个操作
getstatic #4
- 从常量池中获取成员变量 #4 找到堆中的对象,将该对象的引用放入到操作数栈中

iload_3
- 将局部变量表中的 slot 3 数据读取复制到操作数栈中

invokevirtual #5
- 找到常量池 #5 项
- 定位到方法区
java/io/PrintStream.println:(I)V
方法 - 生成新的栈帧(分配 locals、stack等,每执行一个方法都会有新的栈帧)
- 传递参数,执行新栈帧中的字节码

- 执行完毕,弹出栈帧
- 清除 main 操作数栈内容

return
完成 main 方法调用,弹出 main 栈帧
程序结束
# 2.4 练习 - 分析i++
从字节码角度分析 i++ 相关题目
源码
package com.zhuhjay.demo3;
public class Demo3_2 {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
}
2
3
4
5
6
7
8
9
字节码
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: 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 9: 0
line 10: 3
line 11: 18
line 12: 25
line 13: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 a I
18 15 2 b I
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
分析:
注意 iinc 指令是直接在局部变量 slot 上进行运算
a++ 和 ++a 的区别是 先执行 iload 还是 先执行 iinc
将一个 byte 数值类型 10 压入到操作数栈中

- 将操作数栈顶数据弹出,存入局部变量表的 slot 1,此时完成了代码
int a = 10
的操作

- 将局部变量表中的 slot 1 的数据读取复制到操作数栈中,
a++
执行顺序是先读取复制,再进行自增的操作

- 对局部变量表中的 slot 1 进行长度为 1 的自增,是直接在局部变量表中直接进行操作,而不是在操作数栈中进行
- 此时局部变量表中的 a 和 操作数栈中的数值不是一个相等的值

- 现将进行
++a
的运算,会先对局部变量表中的 slot 1 中的数值进行自增,而后才将该结果读取复制到操作数栈中

- 读取局部变量表中 slot 1 的数值到操作数栈中

- 计算完
a++
和++a
过后,操作数栈中已经存在两个数据了,所以会先对操作数栈中的数据进行相加的操作

- 现将进行
a--
的运算,先将局部变量表中 slot 1 读取复制到操作数栈中

- 将局部变量表中的 slot 1 进行自减操作

- 将操作数栈中的数据进行相加,到此为止
a++ + ++a + a--
的运算结束

- 最后将操作数栈中的数据存放到局部变量表中对应的位置上去

# 2.5 条件判断指令
指令 | 助记符 | 含义 |
---|---|---|
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 |
几点说明:
- byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
- goto 用来进行跳转到指定行号的字节码
源码
package com.zhuhjay.demo3; public class Demo3_3 { 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
11字节码
- 比较小的数是通过
iconst
指令 ifne
指令判断操作数栈中的两个数值是否不相等,如果不相等,则跳转到序号为12
的指令,否则继续往下执行- 该程序 if 判断有两个执行路径
3 -> 6 -> 8 -> 9 -> 15
:执行到goto
指令后会跳转到对应的序号指令继续执行3 -> 12 -> 14 0> 15
:通过了 if 判断跳转到对应的序号指令继续执行
0: iconst_0 1: istore_1 2: iload_1 3: ifne 12 6: bipush 10 8: istore_1 9: goto 15 12: bipush 20 14: istore_1 15: return
1
2
3
4
5
6
7
8
9
10- 比较小的数是通过
Tip:
以上比较指令中没有 long,float,double 的比较,那么它们要比较怎么办?
参考 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp (opens new window)
# 2.6 循环控制指令
其实循环控制还是前面介绍的那些指令,例如 while 循环:
package com.zhuhjay.demo3;
public class Demo3_4 {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}
2
3
4
5
6
7
8
9
字节码,通过指令 if_icmpge
判断是否大于等于,是则跳出当前 while 循环,否则继续自增
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
2
3
4
5
6
7
8
再比如 do while 循环:
package com.zhuhjay.demo3;
public class Demo3_5 {
public static void main(String[] args) {
int a = 0;
do {
a++;
} while (a < 10);
}
}
2
3
4
5
6
7
8
9
字节码,与 while 循环不同的地方就是,do while 会先进行自增操作,之后才进行判断
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iload_1
6: bipush 10
8: if_icmplt 2
11: return
2
3
4
5
6
7
最后再看看 for 循环:
package com.zhuhjay.demo3;
public class Demo3_6 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) { }
}
}
2
3
4
5
6
字节码,会发现 for 循环编译后的字节码和 while 循环是一模一样的
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
2
3
4
5
6
7
8
# 2.7 练习 - 判断结果
请从字节码角度分析,下列代码运行的结果:
- 问题出现在
x = x++
,这部分代码的字节码运行流程如下- 从局部变量表中先获取 x 的数值并复制到操作数栈中,此时:
slot x:0; stack x:0
- 对局部变量表中的 x 所在的槽位数值自增1,此时:
slot x:1; stack x:0
- 将操作数栈中的值赋值给局部变量表中 x ,此时:
slot x:0; stack null
- 一直重复以上操作,x 的结果永远都是 0
- 从局部变量表中先获取 x 的数值并复制到操作数栈中,此时:
- 若将以上问题代码改为
x = ++x
,将不会出现以上的问题
package com.zhuhjay.demo3;
public class Demo3_7 {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
System.out.println(x); // 0
}
}
2
3
4
5
6
7
8
9
10
11
12
# 2.8 构造方法
# 1) <clinit>()V
public class Demo3_8 {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
}
2
3
4
5
6
7
8
9
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法<clinit>()V
,该方法会在类加载的初始化阶段被调用
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
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: return
2
3
4
5
6
7
8
9
10
11
12
# 2) <init>()V
public class Demo3_9 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo3_9(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo3_9 d = new Demo3_9("s3", 30);
System.out.println(d.a); // s3
System.out.println(d.b); // 30
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后
public com.zhuhjay.demo3.Demo3_9(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // 调用父类的构造方法(Object)
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 // <- "s3"
30: putfield #3 // -> this.a
33: aload_0
34: iload_2 // <- 30
35: putfield #4 // -> this.b
38: return
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
# 2.9 方法调用
看一下几种不同的方法调用对应的字节码指令
public class Demo3_10 {
public Demo3_10() { }
private void test1() { }
private final void test2() { }
public void test3() { }
public static void test4() { }
public static void main(String[] args) {
Demo3_10 d = new Demo3_10();
d.test1();
d.test2();
d.test3();
d.test4();
Demo3_10.test4();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
字节码
0: new #2 // class com/zhuhjay/demo3/Demo3_10
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new
是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈dup
是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合invokespecial
调用该对象的构造方法"<init>":()V
(会消耗掉栈顶一个引用),另一个要配合astore_1
赋值给局部变量最终方法(final),私有方法(private),构造方法都是由
invokespecial
指令来调用,属于 静态绑定普通成员方法是由
invokevirtual
调用,属于动态绑定,即支持多态成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】比较有意思的是
d.test4();
是通过【对象引用】调用一个静态方法,可以看到在调用invokestatic
之前执行了pop
指令,把【对象引用】从操作数栈弹掉了还有一个执行
invokespecial
的情况是通过super
调用父类方法
# 2.10 多态的原理
package com.zhuhjay.demo3;
public class Demo3_11 {
public static void test(Animal animal) {
animal.eat();
System.out.println(animal.toString());
}
public static void main(String[] args) throws IOException {
test(new Dog());
test(new Cat());
System.in.read();
}
}
abstract class Animal {
public abstract void eat();
@Override
public String toString() {
return "我是" + this.getClass().getSimpleName();
}
}
class Dog extends Animal {
@Override
public void eat() { System.out.println("啃骨头"); }
}
class Cat extends Animal {
@Override
public void eat() { System.out.println("吃鱼"); }
}
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
# 1) 运行代码
注意:运行代码前需要添加虚拟机参数 -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
,禁止指针压缩,省去了地址换算
停在 System.in.read()
方法上,这时运行 jps
获取进程 id
# 2) 运行 HSDB 工具
进入 JDK 目录,执行命令 java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
进入图形界面后 Attach 到对应的 进程id,效果如下

# 3) 查找内存中的对象
打开 Tools -> Find Object By Query
工具
输入 select d from com.zhuhjay.demo3.Dog d
并执行,语法同 SQL 一般,查询结果就是在内存中存在的 Dog 对象内存地址。
# 4) 查看对象内存结构
点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是 MarkWord,后 8 字节就是对象的 Class 指针
但目前看不到它的实际地址,在底层是 C++ 的数据结构,可以看到类中的一些数据,子类父类等等,虚引用表长度 _vtable_len

# 5) 查看对象的内存地址
通过 Windows -> Console
进入命令行模式,执行 mem 0x0000023338896cd0 2
mem
有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)
在结果中的第二行即为 Class 的内存地址

# 6) 查看类的 vtable
方法一:
Alt+R
进入Inspector
工具,输入刚刚查询到的 Class 内存地址方法二:打开
Tools -> Class Browser
输入 Dog 查找,也一样可以找到该 Class 对应的内存地址,然后使用方法一进行查询
无论通过哪种方法,都可以找到 Dog Class
的 vtable
长度为 6,意思就是 Dog
类有 6 个虚方法(多态相关的,final,static 不会列入)
那么这 6 个方法都是谁呢?从 Class 的起始地址开始算,偏移 0x1b8
就是 vtable
的起始地址,进行计算得到:
0x00000233675d3c90
0x1b8 +
---------------------
0x00000233675d3e48
2
3
4
然后使用 Windows -> Console
工具执行命令,就能够得到这 6 个虚方法的入口地址了
mem 0x00000233675d3e48 6
0x00000233675d3e48: 0x00000233671d1b10
0x00000233675d3e50: 0x00000233671d15e8
0x00000233675d3e58: 0x00000233675d3758
0x00000233675d3e60: 0x00000233671d1540
0x00000233675d3e68: 0x00000233671d1678
0x00000233675d3e70: 0x00000233675d3c38
2
3
4
5
6
7
# 7) 验证方法地址
通过 Tools -> Class Browser
查看每个类的方法定义,比较可知每一个地址都对上了查询到的所有虚方法地址
Dog - public void eat() @0x00000233675d3c38;
Animal - public java.lang.String toString() @0x00000233675d3758;
Object - protected void finalize() @0x00000233671d1b10;
Object - public boolean equals(java.lang.Object) @0x00000233671d15e8;
Object - public native int hashCode() @0x00000233671d1540;
Object - protected native java.lang.Object clone() @0x00000233671d1678;
2
3
4
5
6
# 8) 小结
当执行 invokevirtual
指令时
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际 Class
- Class 结构中有
vtable
,它在类加载的链接阶段就已经根据方法的重写规则生成好了 - 查表得到方法的具体地址
- 执行方法的字节码
# 2.11 异常处理
# 1) try-catch
public class Demo3_12_1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
}
2
3
4
5
6
7
8
9
10
部分字节码如下
- 可以看到多出来一个
Exception table
的结构,[from, to)
是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过type
匹配异常类型,如果一致,进入target
所指示行号 - 8 行的字节码指令
astore_2
是将异常对象引用存入局部变量表的slot 2
位置,记录异常对象信息
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
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/Exception;
0 13 0 args [Ljava/lang/String;
2 11 1 i I
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2) 多个 single-catch 块的情况
public class Demo3_12_2 {
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;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
部分字节码信息如下
- 因为异常出现时,只能进入
Exception table
中一个分支,所以局部变量表Slot 2
位置被共用
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: ...
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 [Ljava/lang/String;
2 25 1 i I
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
# 3) multi-catch 的情况
public class Demo3_12_3 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (ArithmeticException | NullPointerException | NumberFormatException e) {
i = 20;
}
}
}
2
3
4
5
6
7
8
9
10
部分字节码如下
- 将异常入口进行一次收集,局部变量表
Slot 2
只收集一个,并且是这些异常的父类型
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/ArithmeticException
2 5 8 Class java/lang/NullPointerException
2 5 8 Class java/lang/NumberFormatException
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/RuntimeException;
0 13 0 args [Ljava/lang/String;
2 11 1 i I
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 4) finally
public class Demo3_12_4 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
部分字节码如下
- 可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
- JVM 会自动捕捉其没有被 catch 块捕获的异常,且如果真的捕获到了这部分异常,同样也会执行 finally 中的代码,并且使用
athrow
指令将异常向外抛出
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 // finally |
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 // finally |
17: istore_1 // 30 -> i |
18: goto 27 // return -------------------
21: astore_3 // catch any -> slot 3 ------
22: bipush 30 // finally |
24: istore_1 // 30 -> i |
25: aload_3 // <- slot 3 |
26: athrow // throw --------------------
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any
11 15 21 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I
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
# 2.12 练习 - finally 面试题
# 1) finally 出现了 return
public class Demo3_13_1 {
public static void main(String[] args) {
System.out.println(test());
}
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
部分字节码如下
- 由于 finally 中的
ireturn
被插入了所有可能的流程,因此返回结果肯定以 finally 的为准 - 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
- 跟上例中的 finally 相比,发现没有
athrow
了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=0
0: bipush 10 // <- 10 放入栈顶
2: istore_0 // 10 -> slot 0 (从栈顶移除)
3: bipush 20 // <- 20 放入栈顶 (finally代码)
5: ireturn // 返回栈顶 int(20)
6: astore_1 // catch any -> slot 1
7: bipush 20 // <- 20 放入栈顶 (finally代码)
9: ireturn // 返回栈顶 int(20)
Exception table:
from to target type
0 3 6 any
2
3
4
5
6
7
8
9
10
11
12
13
14
15
若将案例代码改为如下
- 制造一个异常
int i = 1/0
,运行后发现不会有异常出现,且结果与上个例子相同
public class Demo3_13_1 {
public static void main(String[] args) {
System.out.println(test());
}
public static int test() {
try {
int i = 1/0;
return 10;
} finally {
return 20;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 2) finally 对返回值影响
public class Demo3_13_2 {
public static void main(String[] args) {
System.out.println(test());
}
public static int test() {
int i = 0;
try {
return i;
} finally {
i = 100;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
部分字节码如下
- finally 中不带 return,就可以正常的捕获异常了(从字节码中
athrow
看出)
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: iconst_0 // <- 0 放入栈顶
1: istore_0 // 0 -> i
2: iload_0 // <- i(0)
3: istore_1 // 0 -> slot 1,暂存至 slot 1,目的是为了固定返回值
4: bipush 100 // <- 100 放入栈顶
6: istore_0 // 100 -> i
7: iload_1 // <- i(0),读取返回值放置栈顶
8: ireturn // 返回栈顶 i(0)
9: astore_2 // catch 部分
10: bipush 100
12: istore_0
13: aload_2
14: athrow
Exception table:
from to target type
2 4 9 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
2 13 0 i I
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 2.13 synchronized
public class Demo3_14 {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
2
3
4
5
6
7
8
部分字节码如下
- JVM 会自动给
synchronized
代码块中的代码进行异常捕获,当捕获到异常或者synchronized
代码块运行结束时,都会进行锁的释放,并且锁的释放过程也会进行异常的捕获,确保锁的释放
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 // new Object
3: dup // 复制一份,用来初始化
4: invokespecial #1 // invokespecial -> <init>:()V
7: astore_1 // 将 Object 引用 -> lock
8: aload_1 // <- lock (synchronized开始)
9: dup // 复制一份,一个用于加锁指令一个用于解锁指令
10: astore_2 // 将 Object 引用 -> slot 2
11: monitorenter // 加锁指令
12: getstatic #3 // <- System.out
15: ldc #4 // <- "ok"
17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: aload_2 // <- slot 2(解锁指令使用的 lock引用)
21: monitorexit // 解锁指令
22: goto 30
25: astore_3 // catch any -> slot 3
26: aload_2 // <- slot 2(解锁指令使用的 lock引用)
27: monitorexit // 解锁指令
28: aload_3
29: athrow // 异常抛出
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
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
注意:方法级别的
synchronized
不会在字节码指令中有所体现
# 3 编译器处理
所谓的 语法糖
,其实就是指 java 编译器把 *.java
源码编译为 *.class
字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利
注意,以下代码的分析,借助了 javap
工具,idea 的反编译功能,idea 插件 jclasslib
等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
# 3.1 默认构造器
public class Candy1 { }
编译成class后的代码:
public class Candy1 {
// 这个无参构造是编译器帮助我们加上的
public Candy1() {
super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
}
}
2
3
4
5
6
# 3.2 自动拆装箱
这个特性是 JDK 5
开始加入的:代码一
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
2
3
4
5
6
这段代码在 JDK 5
之前是无法编译通过的,必须改写为:代码二
public class Candy2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}
2
3
4
5
6
显然之前版本的代码太麻烦了,需要在基本类型和包装类型直接来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 JDK 5
以后都由编译器在编译阶段完成,即 代码一
都会在编译阶段被转为 代码二
# 3.3 泛型集合取值
泛型也是在 JDK 5
开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除
的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object
类型来处理:
public class Candy3 {
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);
}
}
2
3
4
5
6
7
所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
2
如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:
// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
2
擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable
仍然保留了方法参数泛型的信息
- JVM 将代码
Integer x = list.get(0);
通过Object obj = List.get(int index);
方式获取出来后,会使用checkcast
指令对照泛型信息表来进行类型的转换
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: ...
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;>;
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
使用反射,仍然能够获得这些信息(只能获取到方法参数上和返回值的泛型信息):
public Set<Integer> test(List<String> list, Map<Integer, Object> map) {return null}
Method test = Candy3.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("\t泛型参数[%d] - %s\n", i, arguments[i]);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
输出
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object
2
3
4
5
# 3.4 可变参数
可变参数也是 JDK 5
开始加入的新特性: 例如:
public class Candy4 {
public static void foo(String... args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
2
3
4
5
6
7
8
9
可变参数 String... args
其实是一个 String[] args
,从代码中的赋值语句中就可以看出来。 同样 java 编译器会在编译期间将上述代码变换为:
public class Candy4 {
public static void foo(String[] args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
}
2
3
4
5
6
7
8
9
注意 如果调用了
foo()
则等价代码为foo(new String[]{})
,创建了一个空的数组,而不会传递null
进去
# 3.5 foreach 循环
仍是 JDK 5
开始引入的语法糖,数组的循环:
public class Candy5_1 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};
for (int e : array) {
System.out.println(e);
}
}
}
2
3
4
5
6
7
8
字节码信息如下
- 数组赋初值的简化写法也是语法糖
- 数组的
foreach
会被编译成fori
public class Candy5_1 {
public Candy5_1() { }
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);
}
}
}
2
3
4
5
6
7
8
9
10
而集合的循环:
public class Candy5_2 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer i : list) {
System.out.println(i);
}
}
}
2
3
4
5
6
7
8
实际被编译器转换为对迭代器的调用:
- 使用迭代器进行遍历(只有实现了
Iterable
接口的集合才能使用foreach
语法糖 )
public class Candy5_2 {
public Candy5_2() { }
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator iter = list.iterator();
while(iter.hasNext()) {
Integer e = (Integer)iter.next();
System.out.println(e);
}
}
}
2
3
4
5
6
7
8
9
10
11
注意 foreach 循环写法,能够配合数组,以及所有实现了
Iterable
接口的集合类一起使用,其 中Iterable
用来获取集合的迭代器(Iterator
)
# 3.6 switch 字符串
从 JDK 7
开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
public class Candy6_1 {
public static void choose(String str) {
switch (str) {
case "hello":
System.out.println("h");
break;
case "world":
System.out.println("w");
break;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
注意 switch 配合 String 和枚举使用时,变量不能为
null
,原因分析完语法糖转换后的代码应当自然清楚
会被编译器转换为:
public class Candy6_1 {
public Candy6_1() { }
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");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
可以看到,执行了两遍 switch,第一遍是根据字符串的 hashCode
和 equals
将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较。
为什么第一遍时必须既比较 hashCode
,又利用 equals
比较呢?hashCode
是为了提高效率,减少可能的比较;而 equals
是为了防止 hashCode
冲突,例如 BM
和 C.
这两个字符串的 hashCode
值都是 2123 ,如果有如下代码:
public class Candy6_1 {
public static void choose(String str) {
switch (str) {
case "BM":
System.out.println("h");
break;
case "C.":
System.out.println("w");
break;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
编译结果为
public class Candy6_1 {
public Candy6_1() { }
public static void choose(String str) {
byte var2 = -1;
switch(str.hashCode()) {
case 2123:
if (str.equals("C.")) {
var2 = 1;
} else if (str.equals("BM")) {
var2 = 0;
}
default:
switch(var2) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3.7 switch 枚举
switch 枚举的例子,原始代码
public enum Sex {
MALE, FEMALE;
}
2
3
public class Candy7 {
public static void foo(Sex sex) {
switch (sex) {
case MALE:
System.out.println("男");break;
case FEMALE:
System.out.println("女");break;
}
}
}
2
3
4
5
6
7
8
9
10
编译后会生成以下文件
- 其中
Candy7$1.class
文件是用来映射枚举类 Sex 的一个静态内部类
字节码如下
static final int[] $SwitchMap$com$zhuhjay$jit$Sex;
descriptor: [I
flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=1, args_size=0
0: invokestatic #1 // Method com/zhuhjay/jit/Sex.values:()[Lcom/zhuhjay/jit/Sex;
3: arraylength
4: newarray int
6: putstatic #2 // Field $SwitchMap$com$zhuhjay$jit$Sex:[I
9: getstatic #2 // Field $SwitchMap$com$zhuhjay$jit$Sex:[I
12: getstatic #3 // Field com/zhuhjay/jit/Sex.MALE:Lcom/zhuhjay/jit/Sex;
15: invokevirtual #4 // Method com/zhuhjay/jit/Sex.ordinal:()I
18: iconst_1
19: iastore
20: goto 24
23: astore_0
24: getstatic #2 // Field $SwitchMap$com$zhuhjay$jit$Sex:[I
27: getstatic #6 // Field com/zhuhjay/jit/Sex.FEMALE:Lcom/zhuhjay/jit/Sex;
30: invokevirtual #4 // Method com/zhuhjay/jit/Sex.ordinal:()I
33: iconst_2
34: iastore
35: goto 39
38: astore_0
39: return
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
也就是会生成以下代码
public class Candy7 {
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class Candy7$1 {
// 数组大小即为枚举元素个数,里面存储case用来对比的数字
static final int[] $SwitchMap$com$zhuhjay$jit$Sex = new int[2];
static {
$SwitchMap$com$zhuhjay$jit$Sex[Sex.MALE.ordinal()] = 1;
$SwitchMap$com$zhuhjay$jit$Sex[Sex.FEMALE.ordinal()] = 2;
}
}
public static void foo(Sex sex) {
int x = Candy7$1.$SwitchMap$com$zhuhjay$jit$Sex[sex.ordinal()];
switch (x) {
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
}
}
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
# 3.8 枚举类
JDK 7
新增了枚举类,以前面的性别枚举为例
public enum Sex {
MALE, FEMALE;
}
2
3
转换后的代码如下
- 继承
Enum
枚举类 final
修饰类- 实例个数都在本类中创建,并且不可修改,属于常量
- 私有构造方法,保护枚举类
Enum.valueOf(Sex.class, name)
底层原理就是Map
,通过键查找对应的枚举
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};
}
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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3.9 try-with-resources
JDK 7
开始新增了对需要关闭的资源处理的特殊语法 try-with-resources
:
其中资源对象需要实现 AutoCloseable
接口,例如 InputStream
、 OutputStream
、 Connection
、 Statement
、 ResultSet
等接口都实现了 AutoCloseable
,使用 try-with-resources
可以不用写 finally
语句块,编译器会帮助生成关闭资源代码,例如:
public class Candy9 {
public static void main(String[] args) {
try (InputStream is = new FileInputStream("E:\\10067\\Documents\\dist\\index.html")) {
System.out.println(is);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
编译后代码如下
- 更完整的关流操作,甚至可以保留压制异常
var2.addSuppressed(var11);
public class Candy9 {
public Candy9() { }
public static void main(String[] args) {
try {
InputStream is = new FileInputStream("E:\\10067\\Documents\\dist\\index.html");
Throwable var2 = null;
try {
System.out.println(is);
} catch (Throwable var12) {
// var2 是我们代码出现的异常
var2 = var12;
throw var12;
} finally {
if (is != null) {
// 如果我们代码有异常
if (var2 != null) {
try {
is.close();
} catch (Throwable var11) {
// 当关流的时候也出现了异常,那么作为压制异常添加
var2.addSuppressed(var11);
}
} else {
// 我们代码没有异常,关流出现了异常,那么 var14 关流的异常
is.close();
}
}
}
} catch (Exception var14) {
var14.printStackTrace();
}
}
}
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
为什么要设计一个 addSuppressed(Throwable e)
(添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources
生成的 fianlly
中如果抛出了异常):
public class Test1 {
public static void main(String[] args) {
try (MyResource resource = new MyResource()) {
int i = 1/0;
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyResource implements AutoCloseable {
@Override
public void close() throws Exception {
throw new Exception("关流异常");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
执行以上代码有以下结果,可以发现异常信息被完整的保留了下来
java.lang.ArithmeticException: / by zero
at com.zhuhjay.jit.Test1.main(Test1.java:10)
Suppressed: java.lang.Exception: 关流异常
at com.zhuhjay.jit.MyResource.close(Test1.java:20)
at com.zhuhjay.jit.Test1.main(Test1.java:11)
2
3
4
5
# 3.10 方法重写时的桥接方法
我们都知道,方法重写时对返回值分两种情况:
- 父子类的返回值完全一致
- 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
class A {
public Number m() {
return 1;
}
}
class B extends A {
@Override
public Integer m() {
return 2;
}
}
2
3
4
5
6
7
8
9
10
11
对于子类,java 编译器会做如下处理:
class B extends A {
public Integer m() {
return 2;
}
// 此方法才是真正重写了父类 public Number m() 方法
public synthetic bridge Number m() {
// 调用 public Integer m()
return m();
}
}
2
3
4
5
6
7
8
9
10
其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m()
没有命名冲突,可以用下面反射代码来验证:
for (Method declaredMethod : B.class.getDeclaredMethods()) {
System.out.println(declaredMethod);
}
2
3
发现有两个 m()
方法,证明了字节码中桥接方法的存在
public java.lang.Integer com.zhuhjay.jit.B.m()
public java.lang.Number com.zhuhjay.jit.B.m()
2
# 3.11 匿名内部类
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
2
3
4
5
6
7
8
9
10
源代码编译后会生成一个额外的类
// 额外生成的类
final class Candy11$1 implements Runnable {
Candy11$1() { }
public void run() {
System.out.println("ok");
}
}
2
3
4
5
6
7
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Candy11$1();
}
}
2
3
4
5
引用局部变量的匿名内部类,源代码:
public class Candy11 {
public static void main(final String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok" + args.length);
}
};
}
}
2
3
4
5
6
7
8
9
10
会在额外生成的类中添加一个有参构造方法,通过参数的方式传递给该类的成员变量的存储,后续让方法获取成员变量的信息来达到目的
// 额外生成的类
final class Candy11$1 implements Runnable {
final String[] val$args;
Candy11$1(String[] args) {
this.val$args = args;
}
public void run() {
System.out.println("ok" + val$args.length);
}
}
2
3
4
5
6
7
8
9
10
public class Candy11 {
public static void main(final String[] args) {
Runnable runnable = new Candy11$1(args);
}
}
2
3
4
5
注意 这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是
final
的:因为在创建Candy11$1
对象时,将args
的值赋值给了Candy11$1
对象的val$args
属性,所以args
不应该再发生变化,如果变化了,那么val$args
属性没有机会跟着一起变化。(当不对变量进行修改时,编译器会将局部变量用final
修饰)