1. 类文件结构
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
ruanrenzhao@MacBook-Pro constantpool % 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 37 6f 72 67 2f 6d 61 73
0000320 74 65 72 79 6f 75 72 73 65 6c 66 2f 74 75 74 6f
0000340 72 69 61 6c 2f 6a 76 6d 2f 63 6f 6e 73 74 61 6e
0000360 74 70 6f 6f 6c 2f 48 65 6c 6c 6f 57 6f 72 6c 64
0000400 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a
0000420 65 63 74 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f
0000440 53 79 73 74 65 6d 01 00 03 6f 75 74 01 00 15 4c
0000460 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72
0000500 65 61 6d 3b 01 00 13 6a 61 76 61 2f 69 6f 2f 50
0000520 72 69 6e 74 53 74 72 65 61 6d 01 00 07 70 72 69
0000540 6e 74 6c 6e 01 00 15 28 4c 6a 61 76 61 2f 6c 61
0000560 6e 67 2f 53 74 72 69 6e 67 3b 29 56 00 21 00 05
0000600 00 06 00 00 00 00 00 02 00 01 00 07 00 08 00 01
0000620 00 09 00 00 00 1d 00 01 00 01 00 00 00 05 2a b7
0000640 00 01 b1 00 00 00 01 00 0a 00 00 00 06 00 01 00
0000660 00 00 0c 00 09 00 0b 00 0c 00 02 00 09 00 00 00
0000700 25 00 02 00 01 00 00 00 09 b2 00 02 12 03 b6 00
0000720 04 b1 00 00 00 01 00 0a 00 00 00 0a 00 02 00 00
0000740 00 0f 00 08 00 10 00 0d 00 00 00 05 01 00 0e 00
0000760 00 00 01 00 0f 00 00 00 02 00 10
0000773
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];
2. 字节码指令
2.1 javap 工具
javap -v HelloWorld.class
2.2 图解执行流程
public class ByteCodeAnalysis {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
ruanrenzhao@MacBook-Pro bytecode % javap -v ByteCodeAnalysis.class
// 类的基本信息
Classfile /Users/ruanrenzhao/IdeaProjects/masteryourself/tutorial/tutorial-java/tutorial-jvm/target/classes/org/masteryourself/tutorial/jvm/bytecode/ByteCodeAnalysis.class
Last modified 2022-5-4; size 692 bytes
MD5 checksum 248004ee67b49c826cda4281b2cba3b8
Compiled from "ByteCodeAnalysis.java"
public class org.masteryourself.tutorial.jvm.bytecode.ByteCodeAnalysis
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 // org/masteryourself/tutorial/jvm/bytecode/ByteCodeAnalysis
#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 Lorg/masteryourself/tutorial/jvm/bytecode/ByteCodeAnalysis;
#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 ByteCodeAnalysis.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 org/masteryourself/tutorial/jvm/bytecode/ByteCodeAnalysis
#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 org.masteryourself.tutorial.jvm.bytecode.ByteCodeAnalysis();
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 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/masteryourself/tutorial/jvm/bytecode/ByteCodeAnalysis;
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 15: 0
line 16: 3
line 17: 6
line 18: 10
line 19: 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: "ByteCodeAnalysis.java"
2.2.1 常量池载入运行时常量池(这里只列举部分)
2.2.2 方法字节码载入方法区
2.2.3 main 线程开始运行,分配栈帧内存
stack=2, locals=4, args_size=1
2.2.4 执行引擎开始执行字节码
运行时栈帧中存储了以下内容
- 局部变量
- 操作数栈
- 动态链接
- 返回地址
-
1. bipush 10
将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
ldc 将一个 int 压入操作数栈
ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
2. istore_1
3. ldc #3
从常量池加载 #3 数据到操作数栈
注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的
4. istore_2
5. iload_1
6. iload_2
7. iadd
8. istore_3
9. getstatic #4
10. iload_3
11. invokeevirtual #5
找到常量池 #5 项
定位到方法区 java/io/PrintStream.println:(I)V 方法
生成新的栈帧(分配 locals、stack 等)
传递参数,执行新栈帧中的字节码
执行完毕,弹出栈帧
清除 main 操作数栈内容
12. return
2.3 i++ 问题字节码分析
```java public class IncAnalysis {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
// 11
System.out.println(a);
// 34
System.out.println(b);
}
}
```java
{
public org.masteryourself.tutorial.jvm.bytecode.IncAnalysis();
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 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/masteryourself/tutorial/jvm/bytecode/IncAnalysis;
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
// 将 10 压入操作数栈
0: bipush 10
// 将操作数栈顶数据弹出,存入局部变量表的 slot 1(10)
2: istore_1
// 从局部变量表的 slot 1 加载数据到操作数栈中(10)
3: iload_1
// 注意 iinc 指令是直接在局部变量 slot 上进行计算,不需要经过操作数栈(此时 slot 1 数据为 11)
4: iinc 1, 1
// 注意 iinc 指令是直接在局部变量 slot 上进行计算,不需要经过操作数栈(此时 slot 1 数据为 12)
7: iinc 1, 1
// 从局部变量表的 slot 1 加载数据到操作数栈中(12)
10: iload_1
// 在操作数栈上进行加法运算(结果是 22)
11: iadd
// 从局部变量表的 slot 1 加载数据到操作数栈中(12)
12: iload_1
// 注意 iinc 指令是直接在局部变量 slot 上进行计算,需要经过操作数栈(此时 slot 1 数据为 11)
13: iinc 1, -1
// 在操作数栈上进行加法运算(结果是 34)
16: iadd
// 将操作数栈顶数据弹出,存入局部变量表的 slot 2(34)
17: istore_2
18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
21: iload_1
// 打印 slot 1(a) 的结果为 11
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
// 打印 slot 2(b) 的结果为 34
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 15: 0
line 16: 3
line 17: 18
line 18: 25
line 19: 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.4 条件判断指令
public class ConditionAnalysis {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
}
public static void main(java.lang.String[]);
Code:
// 将常量 0 压入操作数栈中
0: iconst_0
// 将操作数栈顶数据弹出,存入局部变量表的 slot 1(赋值给 a)
1: istore_1
// 从局部变量表的 slot 1 加载数据到操作数栈中
2: iload_1
// 判断是否 !=0,如果是跳转到 12 行
3: ifne 12
// 将 10 压入操作数栈中
6: bipush 10
// 将操作数栈顶数据弹出,存入局部变量表的 slot 1(赋值给 a)
8: istore_1
// 跳转到 15 行结束程序
9: goto 15
// 将 20 压入操作数栈中
12: bipush 20
// 将操作数栈顶数据弹出,存入局部变量表的 slot 1(赋值给 a)
14: istore_1
15: return
2.5 循环控制指令
public class WhileAnalysis {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
// 判断两个 int 是否 >=,如果是跳转到 14 行结束程序
5: if_icmpge 14
// 如果不是,在局部变量表上进行 iinc 操作
8: iinc 1, 1
// 注意这里是跳转到第 2 行,重新从局部变量上读取数据然后与 10 进行比较
11: goto 2
14: return
2.6 构造方法
2.6.1 <cinit>()V
public class StaticConstructAnalysis {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
}
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 <cinit>()V
<cinit>()V
方法会在类加载的初始化阶段被调用
static {};
Code:
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.6.2 <init>()V
public class ConstructAnalysis {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public ConstructAnalysis(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
ConstructAnalysis d = new ConstructAnalysis("s3", 30);
// s3
System.out.println(d.a);
// 30
System.out.println(d.b);
}
}
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后
public org.masteryourself.tutorial.jvm.bytecode.ConstructAnalysis(java.lang.String, int);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String s1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String 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
2.7 方法调用
public class MethodInvokeAnalysis {
public MethodInvokeAnalysis() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
MethodInvokeAnalysis d = new MethodInvokeAnalysis();
d.test1();
d.test2();
d.test3();
d.test4();
MethodInvokeAnalysis.test4();
}
}
new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 <init>:()V
(会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量
最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态
成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
比较有意思的是 d.test4()
是通过【对象引用】调用一个静态方法,可以看到在调用 invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了
还有一个执行 invokespecial 的情况是通过 super 调用父类方法
public static void main(java.lang.String[]);
Code:
0: new #2 // class org/masteryourself/tutorial/jvm/bytecode/MethodInvokeAnalysis
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.8 多态原理
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际 Class
- Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查表得到方法的具体地址
-
2.9 异常原理
2.9.1 try catch
```java public class TryCatchAnalysis {
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 位置
```java
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:
line 15: 0
line 17: 2
line 20: 5
line 18: 8
line 19: 9
line 21: 12
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
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 8
locals = [ class "[Ljava/lang/String;", int ]
stack = [ class java/lang/Exception ]
frame_type = 3 /* same */
2.9.2 多个 single catch
public class MultiSingleCatchAnalysis {
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 位置被共用
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 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:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/NullPointerException
2 5 22 Class java/lang/Exception
LineNumberTable:
line 15: 0
line 17: 2
line 24: 5
line 18: 8
line 19: 9
line 24: 12
line 20: 15
line 21: 16
line 24: 19
line 22: 22
line 23: 23
line 25: 26
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/ArithmeticException;
16 3 2 e Ljava/lang/NullPointerException;
23 3 2 e Ljava/lang/Exception;
0 27 0 args [Ljava/lang/String;
2 25 1 i I
StackMapTable: number_of_entries = 4
frame_type = 255 /* full_frame */
offset_delta = 8
locals = [ class "[Ljava/lang/String;", int ]
stack = [ class java/lang/ArithmeticException ]
frame_type = 70 /* same_locals_1_stack_item */
stack = [ class java/lang/NullPointerException ]
frame_type = 70 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 3 /* same */
2.9.3 multi catch
public class MultiCatchAnalysis {
public static void main(String[] args) {
try {
Method test = MultiCatchAnalysis.class.getMethod("test");
test.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
public static void test() {
System.out.println("ok");
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: ldc #2 // class org/masteryourself/tutorial/jvm/bytecode/ex/MultiCatchAnalysis
2: ldc #3 // String test
4: iconst_0
5: anewarray #4 // class java/lang/Class
8: invokevirtual #5 // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
11: astore_1
12: aload_1
13: aconst_null
14: iconst_0
15: anewarray #6 // class java/lang/Object
18: invokevirtual #7 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
21: pop
22: goto 30
25: astore_1
26: aload_1
27: invokevirtual #11 // Method java/lang/ReflectiveOperationException.printStackTrace:()V
30: return
Exception table:
from to target type
0 22 25 Class java/lang/NoSuchMethodException
0 22 25 Class java/lang/IllegalAccessException
0 22 25 Class java/lang/reflect/InvocationTargetException
LineNumberTable:
line 19: 0
line 20: 12
line 23: 22
line 21: 25
line 22: 26
line 24: 30
LocalVariableTable:
Start Length Slot Name Signature
12 10 1 test Ljava/lang/reflect/Method;
26 4 1 e Ljava/lang/ReflectiveOperationException;
0 31 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 89 /* same_locals_1_stack_item */
stack = [ class java/lang/ReflectiveOperationException ]
frame_type = 4 /* same */
public static void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String ok
5: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 27: 0
line 28: 8
2.9.4 finally
public class FinallyAnalysis {
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 剩余的异常类型流程
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
2: bipush 10
4: istore_1
5: bipush 30
7: istore_1
8: goto 27
11: astore_2
12: bipush 20
14: istore_1
15: bipush 30
17: istore_1
18: goto 27
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
11 15 21 any
LineNumberTable:
line 15: 0
line 17: 2
line 21: 5
line 22: 8
line 18: 11
line 19: 12
line 21: 15
line 22: 18
line 21: 21
line 22: 25
line 23: 27
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
StackMapTable: number_of_entries = 3
frame_type = 255 /* full_frame */
offset_delta = 11
locals = [ class "[Ljava/lang/String;", int ]
stack = [ class java/lang/Exception ]
frame_type = 73 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 5 /* same */
2.10 synchronized
方法级别的 synchronized 不会在字节码指令中有所体现
public class SyncAnalysis {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: aload_1
9: dup
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
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:
// 12~22 或者 25~28 行出现任何异常,都会跳转到 25 行
from to target type
12 22 25 any
25 28 25 any
LineNumberTable:
line 15: 0
line 16: 8
line 17: 12
line 18: 20
line 19: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 25
locals = [ class "[Ljava/lang/String;", class java/lang/Object, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
3. 编译期处理
所谓的 语法糖 ,其实就是指 java 编译器把 .java
源码编译为 .class
字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码
3.1 默认构造器
public class Candy1 {
}
// 编译成 class 后的代码
public class Candy1 {
// 这个无参构造器是java编译器帮我们加上的
public Candy1() {
// 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
super();
}
}
3.2 自动拆装箱
这个特性是 JDK 5 开始加入的
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
// 等价于
public class Candy2 {
public static void main(String[] args) {
//基本类型赋值给包装类型,称为装箱
Integer x = Integer.valueOf(1);
//包装类型赋值给基本类型,称谓拆箱
int y = x.intValue();
}
}
3.3 泛型集合取值
泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理
public class Candy3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
// 实际调用的是 List.add(Object e)
list.add(10);
// 实际调用的是 Object obj = List.get(int index);
Integer x = list.get(0);
}
}
泛型擦除的是字节码上的泛型信息, 但方法入参和返回值的泛型依旧可以获取
public class GenericInfo {
public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
return null;
}
/**
* 输出结果:
* 原始类型 - interface java.util.List
* 泛型参数[0] - class java.lang.String
* 原始类型 - interface java.util.Map
* 泛型参数[0] - class java.lang.Integer
* 泛型参数[1] - class java.lang.Object
*/
public static void main(String[] args) throws Exception {
Method test = GenericInfo.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
System.out.println("原始类型 - " + parameterizedType.getRawType());
Type[] arguments = parameterizedType.getActualTypeArguments();
for (int i = 0; i < arguments.length; i++) {
System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
}
}
}
}
}
3.4 可变参数
可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来
如果调用了 foo()
则等价代码为 foo(new String[]{})
,创建了一个空的数组,而不会传递 null 进去
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");
}
}
// 等价于
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"});
}
}
3.5 foreach 循环
foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器( Iterator )
public class Candy5_1 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦 f
for (int e : array) {
System.out.println(e);
}
}
}
// 等价于
public class Candy5_1 {
public Candy5_1 {}
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);
}
}
}
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);
}
}
}
// 等价于
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);
}
}
}
3.6 switch 字符串
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖
switch 配合 String 和枚举使用时,变量不能为 null,因为会调用对象的某些方法进行转换
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;
}
}
}
}
// 转换后
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");
}
}
}
可以看到,执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较。
为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突,例如 BM
和 C.
这两个字符串的 hashCode 值都是 2123 ,如果有如下代码:
public class Candy6_2 {
public static void choose(String str) {
switch (str) {
case "BM": {
System.out.println("h");
break;
}
case "C.": {
System.out.println("w");
break;
}
}
}
}
// 转换后
public class Candy6_2 {
public Candy6_2() {
}
public static void choose(String str) {
byte x = -1;
switch (str.hashCode()) {
case 2123: // hashCode 值可能相同,需要进一步用 equals 比较
if (str.equals("C.")) {
x = 1;
} else if (str.equals("BM")) {
x = 0;
}
default:
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
}
3.7 switch 枚举
enum Sex {
MALE,
FEMALE
}
public class Candy7 {
public static void foo(Sex sex) {
switch (sex) {
case MALE:
System.out.println("男");
break;
case FEMALE:
System.out.println("女");
break;
}
}
}
// 转换后
public class Candy7 {
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
// 数组大小即为枚举元素个数,里面存储case用来对比的数字
static int[] map = new int[2];
static {
map[Sex.MALE.ordinal()] = 1;
map[Sex.FEMALE.ordinal()] = 2;
}
}
public static void foo(Sex sex) {
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
}
}
3.8 枚举类
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);
}
}
3.9 try-with-resources
JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources
try(资源变量 = 创建资源对象){
} catch() {
}
其中资源对象需要实现 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("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 t = null;
try {
System.out.println(is);
} catch (Throwable e1) {
// t 是我们代码出现的异常
t = e1;
throw e1;
} finally {
// 判断了资源不为空
if (is != null) {
// 如果我们代码有异常
if (t != null) {
try {
is.close();
} catch (Throwable e2) {
// 如果 close 出现异常,作为被压制异常添加
t.addSuppressed(e2);
}
} else {
// 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
is.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
为什么要设计一个 addSuppressed(Throwable e)
(添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常)
public class Test6 {
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 test.Test6.main(Test6.java:7)
Suppressed: java.lang.Exception: close 异常
at test.MyResource.close(Test6.java:18)
at test.Test6.main(Test6.java:6)
3.10 方法重写的桥接方法
方法重写时对返回值分两种情况
- 父子类的返回值完全一致
子类返回值可以是父类返回值的子类 ```java class A {
public Number m() {
return 1;
}
}
class B extends A {
@Override
// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
public Integer m() {
return 2;
}
}
// 等价于
class B extends A {
public Integer m() {
return 2;
}
// 此方法才是真正重写了父类 public Number m() 方法
public synthetic bridge Number m() {
// 调用 public Integer m()
return m();
}
}
其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,可以用下面反射代码来验证:
```java
for (Method m : B.class.getDeclaredMethods()) {
System.out.println(m);
}
// 会输出两个
// public java.lang.Integer test.candy.B.m()
// public java.lang.Number test.candy.B.m()
3.11 匿名内部类
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
// 转换后
// 会额外生成一个类
final class Candy11$1 implements Runnable {
Candy11$1() {
}
public void run() {
System.out.println("ok");
}
}
public class Candy11 {
public static void main(String[] args) {
// 调用转换后的类
Runnable runnable = new Candy11$1();
}
}
这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建 Candy11$1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val$x 属性,所以 x 不应该再发生变化了,如果变化,那么 val$x 属性没有机会再跟着一起变化
public class Candy11 {
public static void main(String[] args) {
int x = 1;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(x);
}
};
}
}
// 转换后
public class Candy11 {
public static void main(String[] args) {
int x = 1;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(x);
}
};
}
}
final class Candy11$1 implements Runnable {
//多创建了一个变量
int val$x;
//变为了有参构造器
public Demo8$1(int x) {
this.val$x = x;
}
@Override
public void run() {
System.out.println(val$x);
}
}