一、类文件结构
对 HelloWorld.java 使用 javac 命令进行编译生成 class 文件。-parameters:编译后保留方法参数的名称信息。(参考文献)
javac -parameters -g HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("HelloWorld");
}
}
在 Linux 下使用 od 命令读取 class 文件。第一列是标号八进制,后面的列都是十六进制。
[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 0a 48 65 6c 6c 6f 57 6f 72 6c 64 07 00 1c
0000300 0c 00 1d 00 1e 01 00 1a e9 8d 94 e6 b6 99 e5 a2
0000320 b8 2f 41 2f 6a 76 6d 2f 48 65 6c 6c 6f 57 6f 72
0000340 6c 64 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f
0000360 62 6a 65 63 74 01 00 10 6a 61 76 61 2f 6c 61 6e
0000400 67 2f 53 79 73 74 65 6d 01 00 03 6f 75 74 01 00
0000420 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53
0000440 74 72 65 61 6d 3b 01 00 13 6a 61 76 61 2f 69 6f
0000460 2f 50 72 69 6e 74 53 74 72 65 61 6d 01 00 07 70
0000500 72 69 6e 74 6c 6e 01 00 15 28 4c 6a 61 76 61 2f
0000520 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 00 21
0000540 00 05 00 06 00 00 00 00 00 02 00 01 00 07 00 08
0000560 00 01 00 09 00 00 00 1d 00 01 00 01 00 00 00 05
0000600 2a b7 00 01 b1 00 00 00 01 00 0a 00 00 00 06 00
0000620 01 00 00 00 06 00 09 00 0b 00 0c 00 02 00 09 00
0000640 00 00 25 00 02 00 01 00 00 00 09 b2 00 02 12 03
0000660 b6 00 04 b1 00 00 00 01 00 0a 00 00 00 0a 00 02
0000700 00 00 00 09 00 08 00 0a 00 0d 00 00 00 05 01 00
0000720 0e 00 00 00 01 00 0f 00 00 00 02 00 10
0000735
根据 JVM 规范,类文件结构如下:
ClassFile {
// 字节数
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];
}
1、魔数
魔数,即文件类型,占 0~3 字节,ca fe ba be
表示它是 class 类型的文件。
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09
2、版本
4~7 字节,表示类的版本 00 34
(52),表示是 Java 8。
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09
3、常量池
| Constant Type | Value |
| - | - |
| CONSTANT_Class
| 7 |
| CONSTANT_Fieldref
| 9 |
| CONSTANT_Methodref
| 10 |
| CONSTANT_InterfaceMethodref
| 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、反编译
使用 javap -v xxx.class 对 class 文件进行反编译,-v 表示显示详细信息。
C:\software\IntelliJ IDEA Workspace\algorithm\out\production\algorithm\力扣\A\jvm>javap -v HelloWorld.class
Classfile /C:/software/IntelliJ IDEA Workspace/algorithm/out/production/algorithm/力扣/A/jvm/HelloWorld.class
// 文件最后修改时间,大小(字节)
Last modified 2021-1-31; size 558 bytes
// 文件的 MD5 签名
MD5 checksum ba76d7cbd14e48a0d827b449e911bc56
// java 源文件
Compiled from "HelloWorld.java"
// 类的包名+类名
public class 力扣.A.jvm.HelloWorld
minor version: 0
// 52 代码 JDK8
major version: 52
// 类的访问修饰符
flags: ACC_PUBLIC, ACC_SUPER
// 常量池
Constant pool:
// 方法引用,init 表示构造方法,() 表示无参,V表示无返回值
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
// 引用成员变量
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
// 字符串常量
#3 = String #23 // HelloWorld
// 方法引用
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // 力扣/A/jvm/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 L力扣/A/jvm/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#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 HelloWorld
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 力扣/A/jvm/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 力扣.A.jvm.HelloWorld();
// 方法参数和返回值
descriptor: ()V
flags: ACC_PUBLIC
// 代码
Code:
// 栈的深度,局部变量表的长度,参数的长度
stack=1, locals=1, args_size=1
// 把局部变量的第0项加载到操作数栈
0: aload_0
// 调用常量池的第一项方法
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
// 行号表
LineNumberTable:
// 前面代表 java 源代码的行号,后面是字节码中的行号
line 6: 0
// 本地变量表
LocalVariableTable:
// 从字节码开始算的起始范围,作用范围为5,槽位号,变量名,类型
Start Length Slot Name Signature
0 5 0 this L力扣/A/jvm/HelloWorld;
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 HelloWorld
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 10: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
// 方法的参数信息
MethodParameters:
// Flags 表示正常参数
Name Flags
args
}
SourceFile: "HelloWorld.java"
2、图解方法执行流程
代码如下:
public class Demo {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
进行反编译:
C:\software\IntelliJ IDEA Workspace\algorithm\src\力扣\A\jvm\A>javap -v Demo.class
Classfile /C:/software/IntelliJ IDEA Workspace/algorithm/src/力扣/A/jvm/A/Demo.class
Last modified 2021-1-31; size 481 bytes
MD5 checksum 840eba3033f3040d568e9c77ab057097
Compiled from "Demo.java"
public class 鍔涙墸.A.jvm.A.Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #20.#21 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #22.#23 // java/io/PrintStream.println:(I)V
#6 = Class #24 // 鍔涙墸/A/jvm/A/Demo
#7 = Class #25 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 MethodParameters
#15 = Utf8 args
#16 = Utf8 SourceFile
#17 = Utf8 Demo.java
#18 = NameAndType #8:#9 // "<init>":()V
#19 = Utf8 java/lang/Short
#20 = Class #26 // java/lang/System
#21 = NameAndType #27:#28 // out:Ljava/io/PrintStream;
#22 = Class #29 // java/io/PrintStream
#23 = NameAndType #30:#31 // println:(I)V
#24 = Utf8 鍔涙墸/A/jvm/A/Demo
#25 = Utf8 java/lang/Object
#26 = Utf8 java/lang/System
#27 = Utf8 out
#28 = Utf8 Ljava/io/PrintStream;
#29 = Utf8 java/io/PrintStream
#30 = Utf8 println
#31 = Utf8 (I)V
{
public 鍔涙墸.A.jvm.A.Demo();
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
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
MethodParameters:
Name Flags
args
}
SourceFile: "Demo.java"
(1)常量池载入运行时常量池
当 Java 代码被执行时,由 JVM 的类加载器,对 main 方法所在的类,进行类加载。类加载就是把 class 字节数据读取到内存中。常量池中的数据会放在运行时常量池中,运行时常量池实际在方法区中。在 java 中,比较小的数字存储在方法的字节码指令中,当数值大于 short 的最大值时,其存储在常量池中。
(2)方法字节码载入方法区
(3)main 线程开始运行,分配栈帧内存
绿色的为局部变量表,蓝色的为操作数栈。(stack=2,locals=4) 对应操作数栈有2个空间(每个空间 4 个字节),局部变量表中有 4 个槽位。
(4)执行引擎开始执行字节码
bipush 10:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有:
- sipush:将一个 short 压入操作数栈(其长度会补齐 4 个字节);
- ldc:将一个 int 压入操作数栈;
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节);
- 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池。
istore_1:将操作数栈顶数据弹出,存入局部变量表的 slot 1(槽位)。
idc #3:从常量池加载 #3 数据到操作数栈。注意:Short.MAX_VALUE
是 32767,所以 32768 = Short.MAX_VALUE + 1
实际是在编译期间计算好的。
istore_2:将操作数栈顶数据弹出,存入局部变量表的 slot 2(槽位)。
iload_1:接着执行 a+b 的操作,运算操作需要在操作数栈里操作,所以先对局部变量表进行读取。
iload_2:读取局部变量表的 slot 2,压入操作数栈中。
iadd:弹出操作数栈的两个变量,对其进行相加后再入栈。
istore_3:将操作数栈顶数据弹出,存入局部变量表的 slot 3(槽位)。
getstatic #4:去常量池中寻找成员变量引用,对象实际存储在堆中,将对象的引用放入操作数栈中。
iload_3:调用方法需要参数,所以先将参数入栈。
invokevirtual #5:
- 找到常量池 #5 项;
- 定位到方法去
java/io/PrintStream.println:(I)V
方法; - 生成新的栈帧(分配 locals、stack 等);
- 传递参数。执行新栈帧的字节码。
- 执行完毕,弹出栈帧;
- 清除 main 操作数栈内容。
return:
- 完成 main 方法调用,弹出 main 栈帧;
- 程序结束。
3、通过字节码指令来分析代码
(1)i++
代码如下:
public class Demo {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
}
反编译后的代码部分:
Code:
stack=2, locals=3, args_size=1
// 将 10 压入栈
0: bipush 10
// 将 10 弹出栈,放入局部变量表 1
2: istore_1
// 将局部变量 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/PrintSt
ream;
21: iload_1
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintSt
ream;
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
分析:a++ 先 iload 后 iinc(自增),++a 相反。注意 iinc 指令是直接在局部变量 slot 上进行运算。
(2)条件判断指令
| 指令 | 助记符 | 含义 |
| - | - | - |
| 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 |
以上比较指令中没有 long,float,double 的比较,详见(官方文档)
- byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节;
- goto 用来进行跳转到指定行号的字节码。
代码如下:
public class Demo {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
}
字节码如下:
// -1 ~ 5 之间的数用 iconst 表示
0: iconst_0
// 将其放入局部变量表
1: istore_1
// 将其放入栈
2: iload_1
// 判断该数是否 == 0,不等于 0 则跳到 12 行
3: ifne 12
// 将 10 压入栈
6: bipush 10
8: istore_1
9: goto 15
// 将 20 压入栈
12: bipush 20
14: istore_1
15: return
(3)x = x++
代码如下:
public class Demo {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
// x 为 0
System.out.println(x);
}
}
字节码如下:
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_1
5: bipush 10
7: if_icmpge 21
10: iload_2
11: iinc 2, 1
14: istore_2
15: iinc 1, 1
18: goto 4
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintSt
ream;
24: iload_2
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
分析:每次循环都是先将 x 的值压入栈,然后对局部变量表中的 x 自增,接着将栈中的值弹出,放入局部变量表中,等于没有做自增操作。
4、构造方法
(1)cinit
<cinit>()V
方法是类的构造方法,该方法会在类加载的初始化阶段被调用。
public class Demo {
static int i = 0;
static {
i = 20;
}
static {
i = 30;
}
}
编译器会按从上至下的顺序,收集所有 static 静态代码和静态成员赋值的代码,合并为一个特殊的方法 <cinit>()V
。如以下字节码:
// 将数字 10 入栈
0: bipush 10
// 将操作数栈的 10 赋值给常量池的第 2 项
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)init
<init>()V
方法是实例对象的构造方法。
public class Demo {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo demo = new Demo("s3", 30);
System.out.println(demo.a);
System.out.println(demo.b);
}
}
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。
public 鍔涙墸.A.jvm.A.Demo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
// 获取 this
0: aload_0
// 调用父类的构造方法
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
// 将 s1 压入栈
5: ldc #2 // String s1
// 给 this.a 赋值 s1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
// 给 this.b 赋值 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
// 给 this.b 赋值 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String s2
// 给 this.a 赋值 s2
25: putfield #3 // Field a:Ljava/lang/String;
// 开始执行原构造方法
28: aload_0
29: aload_1
30: putfield #3 // Field a:Ljava/lang/String;
33: aload_0
34: iload_2
35: putfield #4 // Field b:I
38: return
5、方法调用
代码如下:
public class Demo {
public Demo() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Demo demo = new Demo();
demo.test1();
demo.test2();
demo.test3();
demo.test4();
Demo.test4();
}
}
jvm 调用不同类型的方法,使用不同的字节码指令。invokespecial 和 invokestatic 属于静态绑定,它们在字节码指令生成时,就知道如果找到该方法。invokevirtual 是动态绑定,不能确定运行时调用的是哪个对象的方法,也许是子类的,也行是父类的。
- 私有、构造、被 final 修饰的方法,在调用时都使用 invokespecial 指令;
- 普通成员方法在调用时,使用 invokevirtual 指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定;
- 静态方法在调用时使用 invokestatic 指令。
字节码如下:
// 为对象在堆空间分配内存,分配成功会把对象的引用返回操作数栈
0: new #2 // class 鍔涙墸/A/jvm/A/Demo
// dup 复制栈顶的内容,现在栈里有两个对象的引用
3: dup
// invokespecial 将栈顶的引用弹出,调用方法(执行构造方法,消耗的是复制的引用)
4: invokespecial #3 // Method "<init>":()V
// 将栈顶的引用弹出(对象的引用,this)存入局部变量表
7: astore_1
8: aload_1
// 私有方法
9: invokespecial #4 // Method test1:()V
12: aload_1
// final
13: invokespecial #5 // Method test2:()V
16: aload_1
// 普通的 public 方法
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
6、多态原理
Java 虚方法:所有被 overriding 的方法都是 virtual 的,所有重写的方法都是 override 的。因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令。在执行 invokevirtual 指令时,经历了以下几个步骤:
- 先通过栈帧中对象的引用找到对象;
- 分析对象头,找到对象实际的 Class;
- Class 结构中有 vtable(虚方法表),它在类加载的链接阶段就已经根据方法的重写规则生成好了;
- 查询 vtable 找到方法的具体地址;
- 执行方法的字节码。
可以使用 HSDB 查看 JVM 运行时期的数据。
java -cp C:\software\Java\jdk1.8.0_211\lib\sa-jdi.jar sun.jvm.hotspot.HSDB
7、异常处理
(1)try-catch
public class Demo {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
}
字节码:
- 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号。
- 第 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 的位置。
0: iconst_0
// 将 0 放入局部变量表
1: istore_1
2: bipush 10
4: istore_1
// 没有发生异常就跳到 12 行
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)多个 single-catch
public class Demo {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (ArithmeticException e) {
i = 30;
} catch (NullPointerException e) {
i = 40;
} catch (Exception e) {
i = 50;
}
}
}
多个异常共用一个槽位,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用。
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 30
11: istore_1
12: goto 26
15: astore_2
16: bipush 40
18: istore_1
19: goto 26
22: astore_2
23: bipush 50
25: istore_1
26: return
Exception table:
// 都是检测 [2,5) 行代码
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
multi-catch 和 多个 single-catch 字节码原理一样。
public class Demo {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (ArithmeticException | NullPointerException e) {
i = 20;
}
}
}
(3)finally
public class Demo {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程。注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次。
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
// try 块
2: bipush 10
4: istore_1
// try 块执行完后,执行 finally 块
5: bipush 30
7: istore_1
8: goto 27
// catch 块
// 将异常信息放入局部变量表的 2 号槽位
11: astore_2
12: bipush 20
// catch 块执行完后,执行 finally 块
14: istore_1
15: bipush 30
17: istore_1
18: goto 27
// 出现异常,但未被捕获,会抛出其他异常,也需要执行 finally 块
21: astore_3
22: bipush 30
24: istore_1
25: aload_3
// 抛异常
26: athrow
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any // 剩余的异常类型,比如 Error
11 15 21 any // 剩余的异常类型,比如 Error
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
finally 出现了 return
public class Demo {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
}
代码运行结果为 20。由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准,所以无论 try 流程中是否发生异常都会返回 20。
Code:
stack=1, locals=2, args_size=0
0: bipush 10
// 将 10 放入局部变量表中
2: istore_0
// 执行 try 中的 finally
3: bipush 20
// 返回栈顶值 20
5: ireturn
// 执行 catch 剩余的异常类型的 finally
// 发生异常后,将异常对象放入局部变量表
6: astore_1
7: bipush 20
// 所以不管 try 中有没有异常都会返回 20
9: ireturn
Exception table:
from to target type
0 3 6 any
finally 中的 return 吞异常
将上例和上上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常。所以不要在 finally 中进行 return 操作。
public class Demo {
public static void main(String[] args) {
int result = test();
// 打印结果仍然是 20,并未抛出异常
System.out.println(result);
}
public static int test() {
try {
int i = 1 / 0;
return 10;
} finally {
return 20;
}
}
}
finally 对返回值的影响
上上例的字节码中第 2 行,似乎没啥用,看以下代码:
public class Demo {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
运行结果是 10,return 结果是另存的,并不受变量值的改变。
Code:
stack=1, locals=3, args_size=0
// 将 10 压入栈
0: bipush 10
// 把 10 赋值给 i
2: istore_0
// 把 i 加载到栈中
3: iload_0
// 将栈顶元素放入局部变量表 1 的位置,用来固定返回值
4: istore_1
// 执行 finally
// 把 20 入栈
5: bipush 20
// 给 i 赋值
7: istore_0
// 把局部变量表 1 的值入栈
8: iload_1
// 返回栈顶值 10
9: ireturn
// 如果发生异常执行
10: astore_2
// 执行 finally
11: bipush 20
13: istore_0
// 加载异常
14: aload_2
// 抛出异常
15: athrow
Exception table:
from to target type
3 5 10 any
LineNumberTable:...
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I
8、Synchronized
public class Demo {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
字节码如下:(方法级别的 Synchronized 不会在字节码指令中体现)
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
// 剩下的一份对象引用存放到局部变量表的 1 号槽位
7: astore_1
// 将局部变量表 1 号槽位的值,加载到栈中
8: aload_1
// 复制一份,放到栈顶,用于加锁时消耗
9: dup
// 将栈顶元素弹出,存到局部变量表 2 号槽位。栈中剩一份对象的引用
10: astore_2
// 加锁
11: monitorenter
// 锁住后面代码块
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
// 加载局部变量表 2 号槽位的对象引用,用于解锁
20: aload_2
// 解锁
21: monitorexit
22: goto 30
// 如果发生异常,则也需要执行解锁
25: astore_3
26: aload_2
// 解锁
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;
三、编译器处理
所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利。
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了几乎等价的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
1、默认构造器
public class Candy1 {
}
编译后 class 后的代码:
public class Candy1 {
// 无参构造器是 java 编译器帮我们加上的
public Candy1() {
// 调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
super();
}
}
2、自动拆装箱
基本类型和其包装类型的相互转换过程,称为拆装箱。在 JDK 5 以后,它们的转换可以在编译期自动完成。
public class Demo2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
在 JDK 5 之前必须改为一下代码,才能编译通过:
public class Demo2 {
public static void main(String[] args) {
// 基本类型赋值给包装类型,称为装箱
Integer x = Integer.valueOf(1);
// 包装类型赋值给基本类型,称谓拆箱
int y = x.intValue();
}
}
3、泛型集合取值
泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:
public class Candy {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
// 实际调用的是 List.add(Object e)
list.add(10);
Integer x = list.get(0);
}
}
对应字节码:
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/lan
g/Integer;
// 这里进行了泛型擦除,实际调用的是add(Objcet o)
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lan
g/Object;)Z
19: pop
20: aload_1
21: iconst_0
// 这里也进行了泛型擦除,实际调用的是get(Object o)
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/l
ang/Object;
// 这里进行了类型转换,将Object转换成了Integer
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return
LineNumberTable:
line 15: 0
line 17: 8
line 18: 20
line 19: 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 仍然保留了方法参数泛型的信息。
// 只有方法参数和返回值上的信息可以通过反射获取
LocalVariableTypeTable:
Start Length Slot Name Signature
8 24 1 list Ljava/util/List<Ljava/lang/Integer;>;
所以在取值时,编译器真正生成的字节码中,有一个类型转换的操作:
Integer x = (Integer) list.get(0);
如果要将返回结果赋值给一个 int 类型的变量,则还有自动拆箱的操作:
int x = ((Integer) list.get(0)).intValue();
4、可变参数
JDK 5 开始加入的新特性:
public class Demo4 {
public static void foo(String... args) {
// 将 args 赋值给 arr,可以看出 String...实际就是 String[]
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。同样 java 编译器会在编译期间将上述代码变换为:
public class Demo4 {
public Demo4 {}
public static void foo(String[] args) {
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
}
注意,如果调用的是foo(),则等价代码为 foo(new String[]{}),创建了一个空数组,而不是直接传递的 null。
5、foreach
public class Demo5 {
public static void main(String[] args) {
// 数组赋初值的简化写法也是一种语法糖。
int[] arr = {1, 2, 3, 4, 5};
for (int x : arr) {
System.out.println(x);
}
}
}
编译器会转换为:
public class Demo5 {
public Demo5() {
}
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
for (int i = 0; i < arr.length; ++i) {
int x = arr[i];
System.out.println(x);
}
}
}
如果是集合使用 foreach
public class Demo5 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer x : list) {
System.out.println(x);
}
}
}
集合要使用 foreach,需要该集合类实现了 Iterable接口,因为集合的遍历需要用到迭代器 Iterator。
public class Demo5 {
public Demo5 {}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// 获得该集合的迭代器
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
Integer x = (Integer)iterator.next();
System.out.println(x);
}
}
}
6、switch 字符串
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实就是语法糖。
public class Demo6 {
public static void main(String[] args) {
String str = "hello";
switch (str) {
case "hello" :
System.out.println("h");
break;
case "world" :
System.out.println("w");
break;
default:
break;
}
}
}
过程说明:
- 在编译期间,单个的 switch 被分为了两个:
- 第一个用来匹配字符串,并给 x 赋值;
- 字符串的匹配用到了字符串的 hashCode,还用到了 equals 方法;
- 使用 hashCode 是为了提高比较效率,使用 equals 是防止有 hashCode 冲突(如:BM 和 C.)
- 第二个用来根据 x 的值来决定输出语句。
- 第一个用来匹配字符串,并给 x 赋值;
- switch 配合 String 和枚举使用时,变量不能为 null,否则在调用 hashCode() 时会报空指针异常。
public class Demo6 {
public Demo6() {
}
public static void main(String[] args) {
String str = "hello";
int x = -1;
// 通过字符串的 hashCode+value 来判断是否匹配
switch (str.hashCode()) {
// hello 的 hashCode
case 99162322 :
// 再次比较,因为字符串的 hashCode 有可能出现 hash 冲突
if(str.equals("hello")) {
x = 0;
}
break;
// world 的 hashCode
case 11331880 :
if(str.equals("world")) {
x = 1;
}
break;
default:
break;
}
// 用第二个 switch 在进行输出判断
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
break;
default:
break;
}
}
}
7、switch枚举
public class Demo7 {
public static void main(String[] args) {
SEX sex = SEX.MALE;
switch (sex) {
case MALE:
System.out.println("man");
break;
case FEMALE:
System.out.println("woman");
break;
default:
break;
}
}
}
enum SEX {
MALE, FEMALE;
}
编译器中执行的代码如下
public class Demo7 {
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
// 数组大小即为枚举元素个数,里面存放了case 用于比较的数字
static int[] map = new int[2];
static {
// ordinal 即枚举元素对应所在的位置,MALE为0,FEMALE为1
map[SEX.MALE.ordinal()] = 1;
map[SEX.FEMALE.ordinal()] = 2;
}
}
public static void main(String[] args) {
SEX sex = SEX.MALE;
// 将对应位置枚举元素的值赋给 x,用于 case 操作
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("man");
break;
case 2:
System.out.println("woman");
break;
default:
break;
}
}
}
enum SEX {
MALE, FEMALE;
}
8、枚举类
JDK 7 新增了枚举类。
enum Sex {
MALE, FEMALE;
}
转换后的代码
public final class Sex extends Enum<Sex> {
// 对应枚举类中的元素
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
// 调用构造函数,传入枚举元素的值及 ordinal
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);
}
}
9、try-with-resources
JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources:
try(资源变量 = 创建资源对象){
} catch() {
}
其中资源对象需要实现 AutoCloseable 接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:
public class Candy9 {
public static void main(String[] args) {
try (InputStream is = new FileInputStream("d:\\1.txt")) {
System.out.println(is);
} catch (IOException e) {
e.printStackTrace();
}
}
}
会被转换为:
public class Candy9 {
public Candy9() {
}
public static void main(String[] args) {
try {
InputStream is = new FileInputStream("d:\\1.txt");
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) {
// 如果 close 出现异常,作为被压制异常添加,这样两个异常都不会丢失
var2.addSuppressed(var11);
}
} else {
// 如果代码中没有异常,close 出现的异常就是最好 catch 块中的 var14
is.close();
}
}
}
} catch (IOException var14) {
var14.printStackTrace();
}
}
}
为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常):
public class Candy9 {
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 异常");
}
}
输出(两个异常信息都不会丢失):
java.lang.ArithmeticException: / by zero
at B.A.jvm.Candy9.main(Candy9.java:20)
Suppressed: java.lang.Exception: close 异常
at B.A.jvm.MyResource.close(Candy9.java:29)
at B.A.jvm.Candy9.main(Candy9.java:21)
10、方法重写时的桥接方法
方法重写时对返回值分两种情况:
- 父子类的返回值完全一致;
- 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子);
class A {
public Number m() {
return 1;
}
}
class B extends A {
@Override
// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
public Integer m() {
return 2;
}
}
对于子类,java 编译器会做如下处理:
class B extends A {
@Override
public Integer m() {
return 2;
}
// 此方法才是真正重写了父类 public Number m() 方法
public synthetic bridge Number m() {
// 调用 public Integer m()
return m();
}
}
Number m()
是 jvm 生成的合成方法,对程序员不可见,该方法作为桥接方法。所以才能实现子类返回值可以是父类返回值的子类。该方法可以通过反射代码来查看。
11、匿名内部类
public class Demo8 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("running...");
}
};
}
}
转换后的代码:
public class Demo8 {
public static void main(String[] args) {
// 用额外创建的类来创建匿名内部类对象
Runnable runnable = new Demo8$1();
}
}
// 创建了一个额外的类,实现了Runnable接口
final class Demo8$1 implements Runnable {
public Demo8$1() {}
@Override
public void run() {
System.out.println("running...");
}
}
如果匿名内部类中引用了局部变量:
public class Demo8 {
public static void main(String[] args) {
final int x = 1;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(x);
}
};
}
}
转换后的代码:
public class Demo8 {
public static void main(String[] args) {
final int x = 1;
// 用额外创建的类来创建匿名内部类对象
Runnable runnable = new Demo8$1(x);
}
}
final class Demo8$1 implements Runnable {
// 多创建了一个变量
int val$x;
// 变为了有参构造器
public Demo8$1(int x) {
this.val$x = x;
}
@Override
public void run() {
System.out.println(val$x);
}
}
注意:这解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建 Demo8$1 对象时,将 x 的值赋值给了 Demo8$1 对象的 val$x 属性,不应该再发生变了,如果变,那么 x 属性没有机会再跟着一起变化。
以下代码无法编译通过:
public class Demo8 {
public static void main(String[] args) {
int x = 1;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(x);
}
};
x = 2;
}
}
标题:JVM 字节码技术
作者:Yi-Xing
地址:http://47.94.239.232/articles/2021/01/30/1612018889279.html
博客中若有不恰当的地方,请您一定要告诉我。前路崎岖,望我们可以互相帮助,并肩前行!