目录
一、概述
程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为:
- 1)比较指令
- 2)条件跳转指令
- 3)比较条件跳转指令
- 4)多条件分支跳转指令
- 5)无条件跳转指令等
二、比较指令
比较指令属于算术指令,比较指令的说明:
- 比较指令的作用是比较栈顶两个元素的大小,并将比较结果入栈。
- 比较指令有: dcmpg、dcmpl、 fcmpg、fcmpl、lcmp
与前面讲解的指令类似,首字符d表示 double类型,f表示float,l表示long。
- 对于 double、float和类型的数字,由于aN的存在,各有两个版本的比较指令。以 float为例,有fcmpg和fcmpl两个指令,它们的区别在于在数字比较时,若遇到NaN值,处理结果不同。
- 指令lcmp针对long型整数,由于long型整数没有NaN值,故无需准备两套指令。
指令 fcmpg和fcmp1都从栈中弹出两个操作数,并将它们做比较,设桟顶的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入0;若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类型的条件分支比较操作来完成整个分支跳转;
【a】ifeq举例
public void compare01() {
int a = 0;
if (a != 0) {
a = 10;
} else {
a = 20;
}
}
查看其字节码信息如下:
0 iconst_0 //将0压入操作数栈中,即a=0
1 istore_1 //将a=0存入局部变量表中索引为1的位置
2 iload_1 //将局部变量表中索引为1的值加载到操作数栈中
3 ifeq 12 (+9) //判断a的值是不是等于0,显然0 = 0, 为true,跳转到12行执行
6 bipush 10
8 istore_1
9 goto 15 (+6)
12 bipush 20 //将20压入操作数栈中
14 istore_1 //20存入局部变量表中索引为1的位置
15 return //方法返回20
通过图解方式理解其执行过程:
【b】ifnonnull举例
public boolean compare02(String str) {
if (str == null) {
return true;
} else {
return false;
}
}
查看其字节码信息如下:
0 aload_1 //将局部变量表中索引为1的值加载到操作数栈中
1 ifnonnull 6 (+5) //判断栈顶元素是否非null,如果非null,跳转到第6行执行
4 iconst_1 //压入1,返回true
5 ireturn
6 iconst_0 //压入0,返回false
7 ireturn
通过图解方式理解其执行过程:
【c】综合比较指令举例一
public void compare03() {
float f1 = 9;
float f2 = 10;
System.out.println(f1 < f2);
}
查看其字节码信息如下:
0 ldc #2 <9.0> //将9.0压入操作数栈中
2 fstore_1 //将9.0存入局部变量表中索引为1的位置
3 ldc #3 <10.0> //将10.0压入操作数栈中
5 fstore_2 //将10.0存入局部变量表中索引为2的位置
6 getstatic #4 <java/lang/System.out> //获取System.out静态对象
9 fload_1 //将局部变量表中索引为1的位置上的值,即9.0压入操作数栈中
10 fload_2 //将局部变量表中索引为2的位置上的值,即10.0压入操作数栈中
11 fcmpg //判断9.0 < 10.0,显然小于,则压入-1到操作数栈中
12 ifge 19 (+7) //判断-1是否大于0,显然不满足,不发生跳转
15 iconst_1 //操作数栈中压入1
16 goto 20 (+4) //跳转到20行执行
19 iconst_0
20 invokevirtual #5 <java/io/PrintStream.println> //调用PrintStream.println方法
23 return //方法返回true
通过图解方式理解其执行过程:
【d】综合比较指令举例二
public void compare04() {
int i1 = 10;
long l1 = 20;
System.out.println(i1 > l1);
}
查看其字节码信息如下:
0 bipush 10 //将10压入操作数栈中
2 istore_1 //将10存入局部变量表中角标为1的位置
3 ldc2_w #6 <20> //将20压入操作数栈中
6 lstore_2 //将20存入局部变量表中角标为2的位置
7 getstatic #4 <java/lang/System.out> //获取静态变量System.out
10 iload_1 //将局部变量表中角标为1的值加载到操作数栈中,即10
11 i2l //发生窄化类型转换
12 lload_2 //将局部变量表中角标为2的值加载到操作数栈中,即20
13 lcmp //比较10和20,显然10<20,所以压入-1到操作数栈中
14 ifle 21 (+7) //判断-1是否小于等于0,显然返回true,发生跳转到21行执行
17 iconst_1
18 goto 22 (+4)
21 iconst_0 //将0压入操作数栈中
22 invokevirtual #5 <java/io/PrintStream.println> //调用PrintStream.println方法
25 return //方法返回false
通过图解方式理解其执行过程:
【e】综合比较指令举例三
public int compare05(double d) {
if (d > 50.0) {
return 1;
} else {
return -1;
}
}
假设d = 10.0,查看其字节码信息如下:
0 dload_1 //将10.0压入操作数栈中
1 ldc2_w #8 <50.0> //将50.0压入操作数栈中
4 dcmpl //比较,返回-1,压入操作数栈中
5 ifle 10 (+5) //判断-1是否小于等于0,满足条件,跳转到第10行执行
8 iconst_1
9 ireturn
10 iconst_m1 //操作数栈中压入-1
11 ireturn //方法返回false
通过图解方式理解其执行过程:
四、比较条件跳转指令
比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。
这类指令有:if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、 ificmpge、if_ acmpeq和if_acmpne。其中指令助记符加上“if_”后,以字符“i”开头的指令针对int型整数操作(也包括 ishort和byte类型),以字符“a”开头的指令表示对象引用的比较。
具体说明如下表:
指令 | 说明 |
if_icmpeq | 比较栈顶两int类型数值大小,当前者等于后者时跳转 |
if_icmpne | 比较栈顶两int类型数值大小,当前者不等于后者时跳转 |
if_icmplt | 比较栈顶两int类型数值大小,当前者小于后者时跳转 |
if_icmple | 比较栈顶两int类型数值大小,当前者小于等于后者时跳转 |
if_icmpgt | 比较栈顶两int类型数值大小,当前者大于后者时跳转 |
if_icmpge | 比较栈顶两int类型数值大小,当前者大于等于后者时跳转 |
if_acmpeq | 比较栈顶两引用类型数值,当结果相等时跳转 |
if_acmpne | 比较栈顶两引用类型数值,当结果不相等时跳转 |
这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下一条语句。
【a】示例一
public void ifCompare01() {
int i = 10;
int j = 20;
System.out.println(i > j);
}
查看其字节码信息如下:
0 bipush 10 //将10压入操作数栈中
2 istore_1 //将10存入局部变量表中索引为1的位置
3 bipush 20 //将20压入操作数栈中
5 istore_2 //将20存入局部变量表中索引为2的位置
6 getstatic #4 <java/lang/System.out> //获取静态变量System.out
9 iload_1 //将局部变量表中索引为1的值,10压入操作数栈中
10 iload_2 //将局部变量表中索引为2的值,20压入操作数栈中
11 if_icmple 18 (+7) //判断栈顶两个元素,下面的元素10显然小于栈顶元素20,返回true,跳转18行执行
14 iconst_1
15 goto 19 (+4)
18 iconst_0 //操作数栈中压入0
19 invokevirtual #5 <java/io/PrintStream.println> //调用PrintStream.println
22 return //方法返回false
通过图解方式理解其执行过程:
【b】示例二
public void ifCompare02() {
short s1 = 9;
byte b1 = 10;
System.out.println(s1 > b1);
}
查看其字节码信息如下:
0 bipush 9 //将9压入操作数栈中
2 istore_1 //将9存入局部变量表中角标为1的位置
3 bipush 10 //将10压入操作数栈中
5 istore_2 //将10存入局部变量表中角标为2的位置
6 getstatic #4 <java/lang/System.out> //获取静态变量System.out
9 iload_1 //将9加载到操作数栈中
10 iload_2 //将10加载到操作数栈中
11 if_icmple 18 (+7) //比较9/10,小于,满足条件,跳转到18行执行
14 iconst_1
15 goto 19 (+4)
18 iconst_0 //将0压入操作数栈中
19 invokevirtual #5 <java/io/PrintStream.println> //调用PrintStream.println方法
22 return //方法返回false
通过图解方式理解其执行过程:
【c】示例三
public void ifCompare03() {
Object obj1 = new Object();
Object obj2 = new Object();
System.out.println(obj1 == obj2);
System.out.println(obj1 != obj2);
}
查看其字节码信息如下:
0 new #10 <java/lang/Object> //创建一个Object对象
3 dup //复制一份上一步创建一个Object对象
4 invokespecial #1 <java/lang/Object.<init>> //执行Object对象的<init>方法进行初始化
7 astore_1 //将Object对象存入局部变量表中角标为1的位置
8 new #10 <java/lang/Object> // //创建一个Object对象
11 dup //复制一份上一步创建一个Object对象
12 invokespecial #1 <java/lang/Object.<init>> 执行Object对象的<init>方法进行初始化
15 astore_2 将Object对象存入局部变量表中角标为2的位置
16 getstatic #4 <java/lang/System.out> //获取System.out静态变量
19 aload_1 //将obj1加载到操作数栈中
20 aload_2 //将obj2加载到操作数栈中
21 if_acmpne 28 (+7) //比较obj1、obj2是否不相等,显然返回true,跳转到28行执行
24 iconst_1
25 goto 29 (+4)
28 iconst_0 //将0压入操作数栈中
29 invokevirtual #5 <java/io/PrintStream.println> //调用PrintStream.println方法
32 getstatic #4 <java/lang/System.out> //输出false
35 aload_1 //将obj1加载到操作数栈中
36 aload_2 //将obj2加载到操作数栈中
37 if_acmpeq 44 (+7) //比较obj1、obj2是否相等,显然返回false,不发生跳转
40 iconst_1 //将1压入操作数栈中
41 goto 45 (+4) //跳转到45行执行
44 iconst_0
45 invokevirtual #5 <java/io/PrintStream.println> //调用PrintStream.println方法
48 return //输出true
通过图解方式理解其执行过程:
五、多条件分支跳转指令
多条件分支跳转指令是专为switch一case语句设计的,主要有tableswitch和lookupswitch。
指令名称 | 描述 |
tableswitch | 用于switch条件跳转,case值连续 |
lookupswitch | 用于switch条件跳转,case值不连续 |
从助记符上看,两者都是switch语句的实现,它们的区别:
- tableswitch要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高。
- 指令lookupswitch内部存放着各个离散的case一offse对,每次执行都要搜索全部的case一offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低。
指令tableswitch的示意图如下图所示。由于tableswitch的case值是连续的,因此只需要记录最低值和最高值,以及每一项对应的offset偏移量,根据给定的index值通过简单的计算即可直接定位到offset。
指令lookupswitch处理的是离散的case值,但是出于效率考虑,将case一offset对按照case值大小排序,给定index时,需要査找与index相等的case,获得其offset,如果找不到则跳转到default。指令lookupswitch 如下图所示。
【a】示例一
0 iload_1 //将局部变量表中角标为1的位置上的值,即select变量的值压入操作数栈中
1 tableswitch 1 to 3 1: 28 (+27) //如果case的值连续,使用tableSwitch指令,后面指明了匹配到哪个值之后就跳转到哪一行进行执行
2: 34 (+33)
3: 37 (+36)
default: 43 (+42)
###################select = 1##################
28 bipush 10 //将10压入操作数栈中
30 istore_2 //将10存入局部变量表中角标为2的位置
31 goto 46 (+15) //方法返回10
###################select = 2、select = 3##################
34 bipush 20 //将20压入操作数栈中
36 istore_2 //将20存入局部变量表中角标为2的位置
37 bipush 30 //将30压入操作数栈中
39 istore_2 //将30存入局部变量表中角标为2的位置
40 goto 46 (+6) //方法返回30
###################default##################
43 bipush 40 //将40压入操作数栈中
45 istore_2 //将40存入局部变量表中角标为2的位置
46 return //方法返回40
查看其字节码信息如下:
public void switch01(int select) {
int num;
switch (select) {
case 1:
num = 10;
break;
case 2:
num = 20;
// break;
case 3:
num = 30;
break;
default:
num = 40;
}
}
【b】示例二
public void switch02(int select) {
int num;
switch (select) {
case 100:
num = 10;
break;
case 500:
num = 20;
break;
case 200:
num = 30;
break;
default:
num = 40;
}
}
查看其字节码信息如下:
0 iload_1 //将局部变量表中角标为1的位置上的值,即select变量的值压入操作数栈中
1 lookupswitch 3 //switch不连续的话,使用lookupswitch指令,并且字节码会做一次优化,从小到大排序
100: 36 (+35)
200: 48 (+47)
500: 42 (+41)
default: 54 (+53)
##########select = 100##############
36 bipush 10
38 istore_2
39 goto 57 (+18)
##########select = 500##############
42 bipush 20
44 istore_2
45 goto 57 (+12)
##########select = 200##############
48 bipush 30
50 istore_2
51 goto 57 (+6)
###########default#############
54 bipush 40
56 istore_2
57 return
【c】示例三
public void switch03(String season) {
switch (season) {
case "SPRING":
break;
case "SUMMER":
break;
case "AUTUMN":
break;
case "WINTER":
break;
}
}
查看其字节码信息如下:
0 aload_1
1 astore_2
2 iconst_m1
3 istore_3
4 aload_2
5 invokevirtual #11 <java/lang/String.hashCode> //调用String.hashCode方法
8 lookupswitch 4 //多分支字符串判断 通过hashcode和equals来判断
-1842350579: 52 (+44)
-1837878353: 66 (+58)
-1734407483: 94 (+86)
1941980694: 80 (+72)
default: 105 (+97)
############SPRING#############
52 aload_2
53 ldc #12 <SPRING>
55 invokevirtual #13 <java/lang/String.equals> //调用String.equals方法
58 ifeq 105 (+47)
61 iconst_0
62 istore_3
63 goto 105 (+42)
############SUMMER#############
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)
############AUTUMN#############
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)
############WINTER#############
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联合使用) |
【a】示例一:循环结构与goto的搭配
public void whileInt() {
int i = 0;
while (i < 100) {
String s = "hello world";
i++;
}
}
查看其字节码信息如下:
0 iconst_0 //将0压入操作数栈中
1 istore_1 //将0保存到局部变量表中角标为1的位置
2 iload_1 //将局部变量表中角标为1的位置的值,压入操作数栈中
3 bipush 100 //将100压入操作数栈中
5 if_icmpge 17 (+12) //比较栈顶两个元素,0<100,返回false,不进行跳转
8 ldc #17 <hello world> //将hello world 压入操作数栈中
10 astore_2 //将hello world保存到局部变量表中角标为2的位置
11 iinc 1 by 1 //将局部变量表中角标为1的位置的值加1
14 goto 2 (-12) //跳转到第2行,重复执行
17 return //方法返回
【b】示例二:while循环结构
public void whileTest() {
int i = 1;
while (i <= 100) {
i++;
}
//可以继续使用i
}
查看其字节码信息如下:
0 iconst_1 //将1压入操作数栈中
1 istore_1 //将1保存到局部变量表中角标为1的位置
2 iload_1 //将局部变量表中角标为1的位置的值,加载到操作数栈中
3 bipush 100 //将100压入操作数栈中
5 if_icmpgt 14 (+9) //1<100,返回false,不进行指令跳转
8 iinc 1 by 1 //将局部变量表角标为1的值加1
11 goto 2 (-9) //重新跳转到第2行执行,直到if_icmpgt返回true,发生指令跳转,方法结束
14 return //方法返回
【c】示例三:for循环结构
public void printFor() {
for (int i = 1; i <= 100; i++) {
}
//不可以继续使用i
}
查看其字节码信息如下:
0 iconst_1 //将1压入操作数栈中
1 istore_1 //将1存入局部变量表中角标为1的位置
2 iload_1 //将局部变量表中角标为1的位置的值,即1压入操作数栈中
3 bipush 100 //将100压入操作数栈中
5 if_icmpgt 14 (+9) //1<100,if_icmpgt返回false,不进行指令跳转
8 iinc 1 by 1 //将局部变量表中角标为1的位置的值加1
11 goto 2 (-9) //跳转到第2行继续重复如上判断过程
14 return //方法返回
【d】示例四:do..while循环结构
public void doWhileTest() {
int i = 1;
do {
i++;
} while (i <= 100);
}
查看其字节码信息如下:
0 iconst_1 //将1压入操作数栈中
1 istore_1 //将1保存到局部变量表中角标为1的位置
2 iinc 1 by 1 //将局部变量表中角标为1的位置的值加1
5 iload_1 //将局部变量表中的值2,压入操作数栈中
6 bipush 100 //将100压入操作数栈中
8 if_icmple 2 (-6) //2<100,返回true,满足条件发生指令跳转,跳转到第2行继续重复上面的步骤
11 return //方法返回