字节码指令集与解析举例(二)
操作数栈管理指令
如同操作一个普通数据结构中的堆栈那样,JVM提供的操作数栈管理指令,可以用于直接操作操作数栈的指令。
这类指令包括如下内容:
-
将一个或两个元素从栈顶弹出,并且直接废弃:
pop、pop2; -
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:
dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2; -
将栈最顶端的两个Slot数值位置交换:
swap。 Java虚拟机没有提供交换两个64位数据类型(long double)数值的指令。 -
指令
nop,是一个非常特殊的指令,它的字节码为0x00。和汇编语言中的nop一样,它表示什么都不做。这条指令一般可用于调试、占位等。
这些指令属于通用型,对栈的压入或者弹出无需指明数据类型。
这些指令属于通用型,对栈的压入或者弹出无需指明数据类型。
说明:
- 不带
_x的指令是复制栈顶数据并压入栈顶。包括两个指令,dup和dup2。dup的系数代表要复制的Slot个数。dup开头的指令用于复制1个Slot的数据。例如1个int或1个reference类型数据dup2开头的指令用于复制2个Slot的数据。例如1个long,或2个int,或1个int+1个
- 带
_x的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令,dup_x1、dup2_x1、dup_x2、dup2_x2。对于带_x的复制插入指令,只要将指令的dup和x的系数相加,结果即为需要插入的位置。因此dup_x1插入位置:1+1=2,即栈顶2个Slot下面dup_x2插入位置:1+2=3,即栈顶3个Slot下面dup2_x1插入位置:2+1=3,即栈顶3个Slot下面dup2_x2插入位置:2+2=4,即栈顶4个Slot下面
pop:将栈顶的1个Slot数值出栈。例如1个short类型数值pop2:将栈顶的2个Slot数值出栈。例如1个double类型数值,或者2个int类型数值
/**
* 指令6:操作数栈管理指令
*/
public class StackOperateTest {
public void print() {
Object obj = new Object();
String info = obj.toString();
}
}
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 aload_1
9 invokevirtual #3 <java/lang/Object.toString>
12 astore_2
13 return
/**
* 指令6:操作数栈管理指令
*/
public class StackOperateTest {
public void print() {
Object obj = new Object();
// String info = obj.toString();
obj.toString();
}
}
变成pop指令了
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 aload_1
9 invokevirtual #3 <java/lang/Object.toString>
12 pop
13 return
/**
* 指令6:操作数栈管理指令
*/
public class StackOperateTest {
public long nextIndex() {
return index++;
}
private long index = 0;
}

控制转移指令
程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为
- 比较指令
- 条件跳转指令
- 比较条件跳转指令
- 多条件分支跳转指令
- 无条件跳转指令
比较指令(这个是算术指令)
比较指令的说明:
- 比较指令的作用是比较栈顶两个元素的大小,并将比较结果入栈。
- 比较指令有:
dcmpg、dcmpl、fcmpg、fcmpl、lcmp
与前面讲解的指令类似,首字符d表示double类型,f表示float,l表示long
- 对于 double和float类型的数字,由于NaN的存在,各有两个版本的比较指令。以 float为例,有fcmpg和fcmpl两个指令,它们的区别在于在数字比较时,若遇到NaN值,处理结果不同。
- 指令
dcmpl和dcmpg也是类似的,根据其命名可以推测其含义,在此不再赘述。 - 指令
lcmp针对long型整数,由于long型整数没有NaN值,故无需准备两套指令。
举例:
指令 fcmpg和fcmpl都从栈中弹出两个操作数,并将它们做比较,设桟顶的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入;若v1>v2则压入1;若v1 < v2则压入-1。
两个指令的不同之处在于,如果遇到NaN值,fcmpg会压入1,而fcmpl会压入-1.
条件跳转指令
条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令选行栈顶元素的准备,然后进行条件跳转。
条件跳转指令有:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull。这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)。
它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置。
具体说明
| 指令 | 说明 |
|---|---|
| ifeq | equals 当栈顶int类型数值等于0时跳转 |
| ifne | not equals 当栈顶in类型数值不等于0时跳转 |
| iflt | lower than 当栈顶in类型数值小于0时跳转 |
| ifle | lower or equals 当栈顶in类型数值小于等于0时跳转 |
| ifgt | greater than 当栈顶int类型数组大于0时跳转 |
| ifge | greater or equals 当栈顶in类型数值大于等于0时跳转 |
| ifnull | 为null时跳转 |
| ifnonnull | 不为null时跳转 |
注意:
-
与前面运算规则一致
- 对于boolean、 byte、 char、short类型的条件分支比较操作,都是使用int类型的比较指令完成
- 对于long、 float、double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转
-
由于各类型的比较最终都会转为int类型的比较操作,所以Java虚拟机提供的int类型的条件分支指令是最为丰富和强大的。
ifeq举例
/**
* 指令7:控制转移指令
*/
public class IfSwitchGotoTest {
public void compare1() {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
}
0 iconst_0
1 istore_1
2 iload_1
3 ifne 12 (+9)
6 bipush 10
8 istore_1
9 goto 15 (+6)
12 bipush 20
14 istore_1
15 return
ifnonnull举例
/**
* 指令7:控制转移指令
*/
public class IfSwitchGotoTest {
public boolean compareNull(String str) {
if (str == null) {
return true;
} else {
return false;
}
}
}
0 aload_1
1 ifnonnull 6 (+5)
4 iconst_1
5 ireturn
6 iconst_0
7 ireturn
结合比较指令
/**
* 指令7:控制转移指令
*/
public class IfSwitchGotoTest {
/**
* 结合比较指令
*/
public void compare2() {
float f1 = 9;
float f2 = 10;
System.out.println(f1 < f2);
}
public void compare3() {
int i1 = 10;
long l1 = 20;
System.out.println(i1 < l1);
}
public int compare4(double d) {
if (d > 50.0) {
return 1;
} else {
return -1;
}
}
}

println有参数为boolean的方法,所以这里打印的不是数字而是布尔值

compare3:出现了宽化类型转换
0 bipush 10
2 istore_1
3 ldc2_w #6 <20>
6 lstore_2
7 getstatic #4 <java/lang/System.out>
10 iload_1
11 i2l
12 lload_2
13 lcmp
14 ifge 21 (+7)
17 iconst_1
18 goto 22 (+4)
21 iconst_0
22 invokevirtual #5 <java/io/PrintStream.println>
25 return
compare4:
0 dload_1
1 ldc2_w #8 <50.0>
4 dcmpl
5 ifle 10 (+5)
8 iconst_1
9 ireturn
10 iconst_m1
11 ireturn
比较条件跳转指令
比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。
这类指令有:if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、 ificmpge、if_ acmpeq和if_acmpne其中指令助记符加上“if_”后,以字符“i”开头的指令针对int型整数操作(也包括 short和byte类型),以字符“a”开头的指令表示对象引用的比较。
具体说明
| 指令 | 说明 |
|---|---|
| if_icmpeq | 比较栈顶两int类型数值大小,当前者等于后者时跳转 |
| if_icmpne | 比较栈顶两int类型数值大小,当前者不等于后者时跳转 |
| if_icmplt | 比较栈顶两int类型数值大小,当前者小于后者时跳转 |
| if_icmple | 比较栈顶两int类型数值大小,当前者小于等于后者时跳转 |
| if_icmpgt | 比较栈顶两int类型数值大小,当前者大于后者时跳转 |
| if_icmpge | 比较栈顶两int类型数值大小,当前者大于等于后者时跳转 |
| if_acmpeq | 比较栈顶两引用类型数值,当结果相等时跳转 |
| if_acmpne | 比较栈顶两引用类型数值,当结果不相等时跳转 |
这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下一条语句。
/**
* 指令7:控制转移指令
*/
public class IfSwitchGotoTest {
/**
* 比较条件跳转指令
*/
public void ifCompare1() {
int i = 10;
int j = 20;
System.out.println(i < j);
}
public void ifCompare2() {
short s1 = 9;
byte b1 = 10;
System.out.println(s1 > b1);
}
public void ifCompare3() {
Object obj1 = new Object();
Object obj2 = new Object();
System.out.println(obj1 == obj2);
System.out.println(obj1 != obj2);
}
}
ifComapre1
0 bipush 10
2 istore_1
3 bipush 20
5 istore_2
6 getstatic #4 <java/lang/System.out>
9 iload_1
10 iload_2
11 if_icmpge 18 (+7)
14 iconst_1
15 goto 19 (+4)
18 iconst_0
19 invokevirtual #5 <java/io/PrintStream.println>
22 return
ifCompare2:short和byte也使用
0 bipush 9
2 istore_1
3 bipush 10
5 istore_2
6 getstatic #4 <java/lang/System.out>
9 iload_1
10 iload_2
11 if_icmple 18 (+7)
14 iconst_1
15 goto 19 (+4)
18 iconst_0
19 invokevirtual #5 <java/io/PrintStream.println>
22 return
ifCompare3
0 new #10 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 new #10 <java/lang/Object>
11 dup
12 invokespecial #1 <java/lang/Object.<init>>
15 astore_2
16 getstatic #4 <java/lang/System.out>
19 aload_1
20 aload_2
21 if_acmpne 28 (+7)
24 iconst_1
25 goto 29 (+4)
28 iconst_0
29 invokevirtual #5 <java/io/PrintStream.println>
32 getstatic #4 <java/lang/System.out>
35 aload_1
36 aload_2
37 if_acmpeq 44 (+7)
40 iconst_1
41 goto 45 (+4)
44 iconst_0
45 invokevirtual #5 <java/io/PrintStream.println>
48 return
多条件分支跳转
多条件分支跳转指令是专为switch-case语句设计的,主要有tableswitch和lookupswitch。
| 指令名称 | 描述 |
|---|---|
| tableswitch | 用于switch条件跳转,case值连续 |
| lookupswitch | 用于switch条件跳转,case值不连续 |
从助记符上看,两者都是switch语句的实现,它们的区别:
tableswitch要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高。- 指令
lookupswitch内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低。
指令tableswitch的示意图如下图所示。由于tableswitch的case值是连续的,因此只需要记录最低值和最高值,以及每一项对应的offset偏移量,根据给定的index值通过简单的计算即可直接定位到offset。

指令lookupswitch处理的是离散的case值,但是出于效率考虑,将case-offset对按照case值大小排序,给定index时,需要査找与index相等的case,获得其offset,如果找不到则跳转到default。指令lookupswitch如下图所示。

示例
/**
* 指令7:控制转移指令
*/
public class IfSwitchGotoTest {
/**
* 3. 多条件分支跳转
*
* @param select
*/
public void switch1(int select) {
int num;
switch (select) {
case 1:
num = 10;
break;
case 2:
num = 20;
break;
case 3:
num = 30;
break;
default:
num = 40;
}
}
public void switch2(int select) {
int num;
switch (select) {
case 100:
num = 10;
break;
case 500:
num = 20;
break;
case 200:
num = 30;
break;
default:
num = 40;
}
}
/**
* JDK7新特性:引入String类型
*
* @param season
*/
public void switch3(String season) {
switch (season) {
case "SPRING":
break;
case "SUMMER":
break;
case "AUTUMN":
break;
case "WINTER":
break;
}
}
}
switch1是连续的,所以从字节码中可以看出是使用的tableswitch指令
0 iload_1
1 tableswitch 1 to 3 1: 28 (+27)
2: 34 (+33)
3: 40 (+39)
default: 46 (+45)
28 bipush 10
30 istore_2
31 goto 49 (+18)
34 bipush 20
36 istore_2
37 goto 49 (+12)
40 bipush 30
42 istore_2
43 goto 49 (+6)
46 bipush 40
48 istore_2
49 return
而switch2不是连续的,所以使用的是lookupswitch
0 iload_1
1 lookupswitch 3
100: 36 (+35)
200: 48 (+47)
500: 42 (+41)
default: 54 (+53)
36 bipush 10
38 istore_2
39 goto 57 (+18)
42 bipush 20
44 istore_2
45 goto 57 (+12)
48 bipush 30
50 istore_2
51 goto 57 (+6)
54 bipush 40
56 istore_2
57 return
注意String类型的字节码:先通过哈希值,然后通过equals判断
0 aload_1
1 astore_2
2 iconst_m1
3 istore_3
4 aload_2
5 invokevirtual #11 <java/lang/String.hashCode>
8 lookupswitch 4
-1842350579: 52 (+44)
-1837878353: 66 (+58)
-1734407483: 94 (+86)
1941980694: 80 (+72)
default: 105 (+97)
52 aload_2
53 ldc #12 <SPRING>
55 invokevirtual #13 <java/lang/String.equals>
58 ifeq 105 (+47)
61 iconst_0
62 istore_3
63 goto 105 (+42)
66 aload_2
67 ldc #14 <SUMMER>
69 invokevirtual #13 <java/lang/String.equals>
72 ifeq 105 (+33)
75 iconst_1
76 istore_3
77 goto 105 (+28)
80 aload_2
81 ldc #15 <AUTUMN>
83 invokevirtual #13 <java/lang/String.equals>
86 ifeq 105 (+19)
89 iconst_2
90 istore_3
91 goto 105 (+14)
94 aload_2
95 ldc #16 <WINTER>
97 invokevirtual #13 <java/lang/String.equals>
100 ifeq 105 (+5)
103 iconst_3
104 istore_3
105 iload_3
106 tableswitch 0 to 3 0: 136 (+30)
1: 139 (+33)
2: 142 (+36)
3: 145 (+39)
default: 145 (+39)
136 goto 145 (+9)
139 goto 145 (+6)
142 goto 145 (+3)
145 return
无条件跳转
目前主要的无条件跳转指令为goto。指令goto接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。
如果指令偏移量太大,超过双字节的带符号整数的范围,则可以使用指令goto_w,它和goto有相同的作用,但是它接收4个宇节的操作数,可以表示更大的地址范围。
指令jsr、jsr_w、ret虽然也是无条件跳转的,但主要用于try-finally语句,且已经被虚拟机逐渐废弃,故不在这里介绍这两个指令。
| 指令名称 | 描述 |
|---|---|
| goto | 无条件跳转 |
| goto_w | 无条件跳转(宽索引) |
| jsr | 跳转至指定16位offset位置,并将jsr下条指令地址压入栈顶 |
| jsr_w | 跳转至指定32位offer位置,并将jsr_w下条指令地址压入栈顶 |
| ret | 返回至由指定的局部变量所给出的指令位置(一般与jsr、jsr_w联合使用) |
/**
* 指令7:控制转移指令
*/
public class IfSwitchGotoTest {
/**
* 4. 无条件跳转指令
*/
public void whileInt() {
int i = 0;
while (i < 100) {
String s = "atguigu";
i++;
}
}
public void whileDouble() {
double d = 0.0;
while (d < 100.1) {
String s = "atguigu";
d++;
}
}
public void printFor() {
short i;
for (i = 0; i < 100; i++) {
String s = "atguigu";
}
}
}
whileInt
0 iconst_0
1 istore_1
2 iload_1
3 bipush 100
5 if_icmpge 17 (+12)
8 ldc #17 <atguigu>
10 astore_2
11 iinc 1 by 1
14 goto 2 (-12)
17 return
思考:
- whileTest和forTest的字节码是一样的,区别是i的作用域不同
- doWhileTest中i++至少会执行一次
/**
* 思考:如下两个方法的操作有何不同?
*/
public void whileTest() {
int i = 1;
while (i <= 100) {
i++;
}
// 可以继续使用i
}
public void forTest() {
for (int i = 1; i <= 100; i++) {
}
// 不可以继续使用i
}
/**
* 更进一步
*/
public void doWhileTest() {
int i = 1;
do {
i++;
} while (i <= 100);
}
异常处理指令
抛出异常指令
athrow指令
在Java程序中显式抛出异常的操作 (throw语句)都是由athrow指令来实现。
除了使用throw语句显式抛出异常情况之外,JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在idiv或ldiv指令中抛出 ArithmeticException异常。
注意
正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时,Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。
异常及异常的处理:
- 过程一:异常对象的生成过程→throw(手动/自动)→指令:
athrow - 过程二:异常的处理:抓抛模型。try-catch-finally→使用异常表
import java.io.IOException;
/**
* 指令8:异常处理
*/
public class ExceptionTest {
public void throwZero(int i) {
if (i == 0) {
throw new RuntimeException("参数值为0");
}
}
public void throwOne(int i) throws RuntimeException, IOException {
if (i == 0) {
throw new RuntimeException("参数值为1");
}
}
}
throwZero方法执行过程

throwOne方法执行字节码指令,和上面几乎相同
0 iload_1
1 ifne 14 (+13)
4 new #2 <java/lang/RuntimeException>
7 dup
8 ldc #5 <参数值为1>
10 invokespecial #4 <java/lang/RuntimeException.<init>>
13 athrow
14 return
但是多了一个异常表

public void throwsArithmetic() {
int i = 10;
int j = i / 0;
System.out.println(j);
}
可以看到,字节码指令中没有athrow,JVM会自动抛
0 bipush 10
2 istore_1
3 iload_1
4 iconst_0
5 idiv
6 istore_2
7 getstatic #6 <java/lang/System.out>
10 iload_2
11 invokevirtual #7 <java/io/PrintStream.println>
14 return
异常处理与异常表
处理异常
在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(早期使用jsr、ret指令),而是采用异常表来完成的。
异常表
如果一个方法定义了一个try-catch或者try-finally的异常处理,就会创建一个异常表。它包含了每个异常处理或者finally块的信息。异常表保存了每个异常处理信息。比如:
- 起始位置
- 结束位置
- 程序计数器记录的代码处理的偏移地址
- 被捕获的异常类在常量池中的索引
当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。
不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标。
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
/**
* 指令8:异常处理
*/
public class ExceptionTest {
public void tryCatch() {
try {
File file = new File("/hello.txt");
FileInputStream fileInputStream = new FileInputStream(file);
String info = "hello";
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (RuntimeException e) {
e.printStackTrace();
}
}
}
如果出现异常,根据异常表执行字节码指令

try-finally代码执行过程举例
/**
* 指令8:异常处理
*/
public class ExceptionTest {
/**
* 思考:如下方法返回结果为多少?
*
* @return
*/
public static String func() {
String str = "hello";
try {
return str;
} finally {
str = "atguigu";
}
}
public static void main(String[] args) {
System.out.println(func());
}
}
hello
Process finished with exit code 0

同步控制指令
Java虚拟机支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的。
方法级的同步
方法级的同步:是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法
当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置。
- 如果设置了,执行线程将先持有同步锁,然后执行方法。最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁。
- 在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获得同一个锁。
- 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。
/**
* 指令9:同步控制指令
*/
public class SynchronizedTest {
private int i = 0;
public synchronized void add() {
i++;
}
private Object obj = new Object();
public void subtract() {
synchronized (obj) {
i--;
}
}
}
对于add方法,可以从方法访问标识中看到这是一个同步方法,但是在字节码指令方面和不加synchronized相同,是一个隐式的同步机制。

0 aload_0
1 dup
2 getfield #2 <SynchronizedTest.i>
5 iconst_1
6 iadd
7 putfield #2 <SynchronizedTest.i>
10 return
这段代码和普通的无同步操作的代码没有什么不同,没有使用monitorenter和monitorexit进行同步区控制。这是因为,对于同步方法而言,当虚拟机通过方法的访问标示符判断是一个同步方法时,会自动在方法调用前进行加锁,当同步方法执行完毕后,不管方法是正常结束还是有异常抛出,均会由虚拟机释放这个锁。因此,对于同步方法而言,monitorenter和monitorexit指令是隐式存在的,并未直接出现在字节码中。
方法内指定指令序列的同步
同步一段指令集序列:通常是由Java中的synchronized语句块来表示的。JVM的指令集有monitorenter和monitorexit两条指令来支持synchronized关键字的语义。
当一个线程进入同步代码块时,它使用monitorenter指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块。
当线程退出同步块时,需要使用monitorexit声明退出。在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。
指令monitorenter和monitorexit在执行时,都需要在操作数栈顶压入对象,之后monitorenter和monitorexit的锁定和释放都是针对这个对象的监视器进行的。
下图展示了监视器如何保护临界区代码不同时被多个线程访问,只有当线程4离开临界区后,线程1、2、3才有可能进入。

substrct解析

JVM官方对于两个指令的解释



浙公网安备 33010602011771号