JVM3️⃣字节码技术②不同代码的字节码指令
1、整数自增
Java 整数可以自增(
++i
和i++
)和自减(--i
和i--
)
- iinc:int 类型的自增指令(int increament)
- 注意:直接在局部变量表的 slot 上运算,无需加载到操作数栈。
- 格式:
iinc slot 增量
。
- ++i 或 ++i:区别在于 iload 和 iinc 的执行顺序。
- 自增或自减:区别在于增量的正负号。
1.1、示例
Java 文件
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
字节码:如下,仅展示方法部分
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) 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 8: 0
line 9: 3
line 10: 18
line 11: 25
line 12: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 a I
18 15 2 b I
1.2、分析
主线程开始运行,分配栈帧内存。
操作数栈:stack = 2,容量为 2。
局部变量表:locals = 3,长度为 3。
-
bipush 10
:将整数 10 入栈 -
istore_1
:将栈顶数据(int,10),保存到局部变量表下标为 1 的 slot -
iload_1
:将局部变量表中 slot 1 的数据(int,10),加载到操作数栈的栈顶 -
iinc 1, 1
:将 slot 1 的整数自增 1(10 → 11) -
iinc 1, 1
:将 slot 1 的整数自增 1(11 → 12) -
iload_1
:将局部变量表中 slot 1 的数据(int,12),加载到操作数栈的栈顶 -
iadd
:将操作数栈的两个整数相加并出栈,求和结果(22)入栈 -
iload_1
:将局部变量表中 slot 1 的数据(int,12),加载到操作数栈的栈顶 -
iinc 1, -1
:将 slot 1 的整数自增 -1(12 → 11) -
iadd
:将操作数栈的两个整数相加并出栈,求和结果(34)入栈 -
istore_2
:将栈顶数据(int,34),保存到局部变量表下标为 2 的 slot
到此,a 和 b 的赋值情况已分析结束。(省略打印语句的分析,可以往前一节看)
最终答案:a == 11,b == 34
2、条件、循环
2.1、条件判断
public static void main(String[] args) {
int a = 0;
if(a == 0) {
a = 10;
} else {
a = 20;
}
}
字节码文件:仅展示方法部分
-
ifne:判断栈顶数据是否 != 0
-
goto:跳转到指定行指令
0: iconst_0 // 将整数 0 入栈 1: istore_1 // 将栈顶数据(0)保存到局部变量表 slot 1 2: iload_1 // 加载局部变量表 slot 1 数据(0) 3: ifne 12 // 判断 “!= 0”。成立则跳转到第 12 行指令,不成立则执行下一行指令 6: bipush 10 // 将整数 10 入栈 8: istore_1 // 将栈顶数据(10)保存到局部变量表 slot 1 9: goto 15 // 跳转到第 15 行指令 12: bipush 20 // 将整数 20 入栈 14: istore_1 // 将栈顶数据(20)保存到局部变量表 slot 1 15: return
2.2、循环控制
在循环控制中,使用的也是 if 语句的字节码指令。
do-while
public static void main(String[] args) {
int a = 0;
do {
a++;
} while (a < 10);
}
if_icmplt:判断栈顶数据两个元素,是否 < 0(top.next - top)
0: iconst_0 // 将整数 0 入栈
1: istore_1 // 将栈顶数据(0)保存到局部变量表 slot 1
2: iinc 1, 1 // 将 slot 1 的整数自增 1
5: iload_1 // 加载局部变量表 slot 1 数据(1)
6: bipush 10 // 将整数 10 入栈
8: if_icmplt 2 // 比较栈顶两个数据,判断“< 0”。成立则跳转到第 2 行指令
11:return
while 和 for
以下两种方式的代码,编译后的字节码完全相同。
// while
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
// for
public static void main(String[] args){
for (int i = 0; i < 10; i++){
}
}
if_icmpge:判断栈顶数据两个元素,是否 >= 0(top.next - top)
0: iconst_0 // 将整数 0 入栈
1: istore_1 // 将栈顶数据(0)保存到局部变量表 slot 1
2: iload_1 // 加载局部变量表 slot 1 数据(0)
3: bipush 10 // 将整数 10 入栈
5: if_icmpge 14 // 比较栈顶两个数据,判断“>= 0”。成立则跳转到第 14 行指令
8: iinc 1, 1 // 将 slot 1 的整数自增 1
11: goto 2 // 跳转到第 2 行指令
14: return
2.3、思考题
-
问:循环结束时 x 的值。
public static void main(String[] args) { int i = 0; int x = 0; while (i < 10) { x = x++; i++; } }
-
分析:对
x = x++
进行分析(x 初值为 0,位于 slot2)iload_2 // 加载局部变量表 slot 2 数据(x,此时为 0) iinc 2,1 // 将 slot 2 整数自增 1(x:0 → 1) istore_2 // 将栈顶数据(0)存储到 slot 2(将 x 覆盖为0)
结论:每次循环中,x 先自增 1,再被覆盖为 0。
最终结果:0
3、构造方法
<cinit>()V
类构造方法:针对静态结构,类加载的初始化阶段被调用。
public class Demo {
static int i = 10;
static { i = 20; }
static { i = 30; }
}
编译器按从上往下的顺序,收集所有静态结构(静态代码块,静态成员)的赋值语句,合并为<cinit>()V
方法
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
<init>()V
构造方法:针对非静态结构,类加载的初始化阶段被调用。
public class Demo {
private String a = "s1";
{ b = 20; }
private int b = 10;
{ a = "s2"; }
public Demo3_8_2(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo3_8_2 d = new Demo3_8_2("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
-
编译器按从上往下的顺序,收集所有非静态结构(代码块,成员变量)的赋值语句,合并为
<cinit>()V
方法 -
原始构造方法(与类同名的构造函数),也会被收集进去,但构造函数位于最后。
// 父类构造方法 0: aload_0 1: invokespecial #1 // super.<init>()V // 非静态结构 4: aload_0 5: ldc #2 // "s1" 7: putfield #3 // this.a 10: aload_0 11: bipush 20 13: putfield #4 // this.b 16: aload_0 17: bipush 10 19: putfield #4 22: aload_0 23: ldc #5 // "s2" 25: putfield #3 // 构造函数 28: aload_0 29: aload_1 30: putfield #3 33: aload_0 34: iload_2 35: putfield #4 38: return
4、方法调用
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 d = new Demo();
d.test1();
d.test2();
d.test3();
d.test4();
Demo.test4();
}
}
-
new:创建对象。分配堆内存,将对象引用压入操作数栈
-
dup:复制操作数栈的栈顶数据,此时栈中有 2 个相同的对象引用(一个用于初始化,一个用于存储)
-
有关方法调用的说明,看本文 2.2 的字节码指令。
0: new #2 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
5、异常
5.1、try-catch
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
字节码
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
// try
2: bipush 10
4: istore_1
5: goto 12
// catch
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
StackMapTable: ...
MethodParameters: ...
}
- 异常表(Exception table),异常中特有的结构。
- from/to:可能出现异常的代码,即 try 块。【左闭右开 [from, to)】
- target:发生匹配到的异常时的跳转行号。
- type:异常类型
- 异常监测:监测 [from, to) 的代码(try 块)
- 发生与 catch 块匹配的异常时,跳转到 target(第 8 行)
a_store2
:将异常对象的引用放入 slot2。- 执行该 catch 块的内容,结束。
- 没有异常,执行完 try 块代码后,跳出 try-catch 块。
- 发生与 catch 块匹配的异常时,跳转到 target(第 8 行)
5.2、多个 catch
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;
}
}
字节码
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
// try
2: bipush 10
4: istore_1
5: goto 26
// catch
8: astore_2
9: bipush 30
11: istore_1
12: goto 26
// catch
15: astore_2
16: bipush 40
18: istore_1
19: goto 26
// catch
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: ...
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: ...
MethodParameters: ...
异常表(Exception table)
- 相比一个 single-catch 块,增加了相应个数的异常记录。
- 每个 catch 块都有一组字节码指令。
异常监测:检测 [from, to) 的代码(try 块)
- 发生与 catch 块匹配的异常时,跳转到 target(按异常类型的定义顺序匹配)
- ArithmeticException:跳转到第 8 行
- NullPointerException:跳转到第 15 行
- Exception:跳转到第 22 行
astore_2
:将异常对象的引用放入 slot2。- 由于只会匹配并进入一个异常分支,所以 slot2 公用。
- 执行该 catch 块的内容,结束。
若没有异常,执行完 try 块代码后,跳出 try-catch 块。
5.3、multi-catch
public static void main(String[] args) {
try {
Method test = Demo3_11_3.class.getMethod("test");
test.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
public static void test() {
System.out.println("ok");
}
字节码
stack=3, locals=2, args_size=1
0: ldc #2
2: ldc #3
4: iconst_0
5: anewarray #4
8: invokevirtual #5
11: astore_1
12: aload_1
13: aconst_null
14: iconst_0
15: anewarray #6
18: invokevirtual #7
21: pop
22: goto 30
// catch
25: astore_1
26: aload_1
27: invokevirtual #11 // e.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: ...
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: ...
MethodParameters: ...
异常表(Exception table)
- 相比一个 single-catch 块,增加了相应个数的异常记录。
- 相比多个 single-catch 块,只有一组 catch 块的字节码指令。
异常监测:检测 [from, to) 的代码(try 块)
- 发生与 multi-catch 块匹配的异常时,跳转到 target(对异常类型的定义顺序没有要求,但不能是父子类)
astore_1
:将异常对象的引用放入 slot1。- 执行该 catch 块的内容,结束。
若没有异常,执行完 try 块代码后,跳出 try-catch 块。
5.4、finally
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
字节码
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
// try
2: bipush 10
4: istore_1
// finally
5: bipush 30
7: istore_1
8: goto 27
// catch Exception
11: astore_2
12: bipush 20
14: istore_1
// finally
15: bipush 30
17: istore_1
18: goto 27
// catch any
21: astore_3
// finally
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
StackMapTable: ...
MethodParameters: ...
- finally 块的字节码指令,被复制并分别放入 try 块、catch 块。
- 增加 any 异常,表示可能出现的其它异常类型(如 Error)
- catch any 异常块中,也有 finally 代码的字节码指令;
- 该异常会被存储在一个“无名” slot,并被 athrow 指令抛出。
5.5、finally 对返回值影响
finally 块中出现 return 的情况。
问:方法的返回值是什么?10 还是 20 ?
public class Demo {
public static void main(String[] args) {
int result = test();
}
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
}
在 Java SE 中,我们知道在方法返回之前会执行 finally 块,所以此时肯定返回的是 20。
从字节码的角度分析。
stack=1, locals=2, args_size=0
// try
0: bipush 10
2: istore_0
// finally
3: bipush 20
5: ireturn
// catch any
6: astore_1
// finally
7: bipush 20
9: ireturn
Exception table:
from to target type
0 3 6 any
LineNumberTable: ...
StackMapTable: ...
- finally 块代码对应的字节码指令,被复制并分别放入 try 块、catch any 块。
- ireturn:将栈顶的整数作为返回值返回
- 与上一个案例中的 finally 相比,any 异常不会被 athrow 抛出。
- 结论:当 finally 块中出现 return,异常会被吞掉!
再来看 finally 块中没有 return,但修改作为返回值的变量的情况。
问:方法的返回值是什么?10 还是 20 ?
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;
}
}
}
字节码
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0
// try
3: iload_0
4: istore_1 // 将 i 放入 slot1 作为返回值
// finally
5: bipush 20
7: istore_0 // 修改的是 slot0,不影响返回值
// return
8: iload_1 // 加载的是 slot1
9: ireturn // 返回栈顶数据(10)
// catch any
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
StackMapTable: ...
- finally 块代码对应的字节码指令,被复制并分别放入 try 块、catch any 块。
- 注意
- slot0 和 slot1 都存储了变量 i,分别用于值的修改和返回值。
- slot0:当 finally 块中修改 i 时,修改的是 slot0 的 i;
- slot1:当方法返回时,返回的是 slot1 的i。
- 说明:i 值不会被修改,相当于锁定了返回值。
- 结论:当 finally 块中没有 return,try 块中固定了返回值,不会被 finally 影响!
6、同步(Monitor)
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
字节码
stack=2, locals=4, args_size=1
// 实例化对象并初始化
0: new #2
3: dup
4: invokespecial #1
7: astore_1
// 开始同步代码块
8: aload_1
9: dup
10: astore_2 // 复制一份lock引用,用于解锁
// 加锁
11: monitorenter
12: getstatic #3 // <- System.out
15: ldc #4 // <- "ok"
17: invokevirtual #5 // invokevirtual println:(Ljava/lang/String;)V
// 解锁
20: aload_2
21: monitorexit
22: goto 30
// catch any
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;
StackMapTable: ...
- 进入同步代码块后,会复制一份被加锁的对象并存储在局部变量表,用于解锁。
- monitorenter 加锁,monitorexit 解锁。加锁和解锁指令成对出现。
- 不论是否会出现异常,monitor 都会将对象引用解锁。
- 注:方法级别的 synchronized 不会在字节码指令中有所体现。