📚《深入浅出JVM字节码》
《Java虚拟机字节码:从入门到实战》的开源版本。作者通过自己的实战经验,整合出一套适合新手的高效学习教程。归纳并提炼知识点,制定合理路线,帮助读者更快掌握核心技术。
Java语言提供的条件分支语句包含if语句、switch语句、三目运算符,这些条件语句是如何通过字节码实现的呢?
使用Java语言编写的if语句使用案例代码如下。
public int ifFunc(int type) {
if (type == 1) {
return 100;
} else if (type == 2) {
return 1000;
} else {
return 0;
}
}
使用javap命令输出ifFunc方法的字节码如下。
public int ifFunc(int);
Code:
0: iload_1
1: iconst_1
2: if_icmpne 8
5: bipush 100
7: ireturn
8: iload_1
9: iconst_2
10: if_icmpne 17
13: sipush 1000
16: ireturn
17: iconst_0
18: ireturn
偏移量为0、1、2 三条字节码指令完成第一个if语句的判断。iload_1将参数type的值放入操作数栈顶,由于是非静态方法,所示局部变量表索引为0的Slot存储的是this引用,因此局部变量表索引为1的Slot存储的才是方法的第一个参数。iconst_1指令将立即数1放入操作数栈顶。if_icmpne指令完成操作数栈顶两个整数的比较,该指令的操作码为0xA0,指令执行需要一个操作数,操作数是当前方法某条字节码指令的偏移量。当栈顶的两个int类型的元素不相等时,跳转到操作数指向的字节码指令。
if_icmpne字节码指令是判断两个值不相等才跳转,这与java代码刚好相反。在java代码中,if左右两个元素相等才执行if体内的代码,而编译后字节码指令按if与else if、else的编写顺序生成,当if 左右两个元素相等时继续往下执行便是对应java语言中的if语句的代码块,因此字节码层面会看到相反的条件比较跳转。
偏移量为8、9、10的三条字节码指令也是完成比较跳转的操作,最后一个else从偏移量为17的字节码指令开始,如果else代码块中没有返回指令,那么会继续往下执行。
如果第一个if中没有返回指令呢?如:
public int ifFunc2(int type) {
if (type == 1) {
type = 2;
}else {
type = 3;
}
return type;
}
使用javap命令输出ifFunc2方法的字节码如下。
public int ifFunc2(int);
Code:
0: iload_1
1: iconst_1
2: if_icmpne 10
5: iconst_2
6: istore_1
7: goto 12
10: iconst_3
11: istore_1
12: iload_1
13: ireturn
如字节码所示,编译器在if_icmpne指令后面为局部变量type赋值后,使用一条goto指令跳转到else结束的后面的第一条字节码指令。
所以,当if或者else if的代码块中没有return指令时,编译器会为其添加一条goto指令用于跳出if条件分支语句。goto指令是无条件跳转指令,操作码为0xA7,操作数是当前方法的某条字节码指令的偏移量,本例中,goto指令的操作码是12,表示跳转到偏移量为12的字节码指令,偏移量为12的字节码指令是iload_1,所以goto指令之后将会指向该指令。
if_icmpne指令用于两个int类型值比较,不相等才跳转。更多比较跳转指令如下表所示。
指令的操作码 | 指令的助记符 | 操作数 | 描述 |
---|---|---|---|
0x9F | if_icmpeq | index,字节码指令的下标 | 如果栈顶两个int类型值相等则跳转 |
0xA0 | if_icmpne | index,字节码指令的下标 | 如果栈顶两个int类型值不相等则跳转 |
0xA1 | if_icmplt | index,字节码指令的下标 | 如果栈顶两int类型值前小于后则跳转 |
0xA4 | if_icmple | index,字节码指令的下标 | 如果栈顶两int类型值前小于等于后则跳转 |
0xA3 | if_icmpgt | index,字节码指令的下标 | 如果栈顶两int类型值前大于后则跳转 |
0xA2 | if_icmpge | index,字节码指令的下标 | 如果栈顶两int类型值前大于等于后则跳转 |
0xC6 | ifnull | index,字节码指令的下标 | 如果栈顶引用值为null则跳转 |
0xC7 | ifnonnull | index,字节码指令的下标 | 如果栈顶引用值不为null则跳转 |
0xA5 | if_acmpeq | index,字节码指令的下标 | 如果栈顶两引用类型值相等则跳转 |
0xA6 | if_acmpne | index,字节码指令的下标 | 如果栈顶两引用类型不相等则跳转 |
与0比较的跳转指令如下表所示。
指令的操作码 | 指令的助记符 | 操作数 | 描述 |
---|---|---|---|
0x99 | ifeq | index,字节码指令的下标 | 如果栈顶int类型值等于0则跳转 |
0x9A | ifne | index,字节码指令的下标 | 如果栈顶int类型值不为0则跳转 |
0x9B | iflt | index,字节码指令的下标 | 如果栈顶int类型值小于0则跳转 |
0x9E | ifle | index,字节码指令的下标 | 如果栈顶int类型值小于等于0则跳转 |
0x9D | ifgt | index,字节码指令的下标 | 如果栈顶int类型值大于0则跳转 |
0x9C | ifge | index,字节码指令的下标 | 如果栈顶int类型值大于等于0则跳转 |
使用Java语言编写的switch语句使用案例代码如下。
public int switchFunc(int stat) {
int a = 0;
switch (stat) {
case 5:
a = 0;
break;
case 6:
case 8:
a = 1;
break;
}
return a;
}
使用javap命令输出switchFunc方法的字节码如下。
public int switchFunc(int);
Code:
0: iconst_0
1: istore_2
2: iload_1
3: tableswitch { // 5 to 8
5: 32
6: 37
7: 39
8: 37
default: 39
}
32: iconst_0
33: istore_2
34: goto 39
37: iconst_1
38: istore_2
39: iload_2
40: ireturn
与if语句一样的是,switch代码块中的每个case代码块都是按顺序编译生成字节码的,switch代码块中的所有字节码都在tableswitch这条指令的后面。
tableswitch指令的操作码为0xAA,该指令的操作数是不定长的,每个操作数的长度为四个字节,编译器会为case区间(本例中,case最小值为5,最大值为8,区间为[5,8])的每一个数字都生成一个case语句,就是添加一个操作数,操作数存放下一条字节码指令的相对偏移量,注意,是相对偏移量。以上面例子说明,tableswitch指令对应的字节码为:
AA | 00 00 00 24 | 00 00 00 05 | 00 00 00 08 |
00 00 00 1D | 00 00 00 22 | 00 00 00 24 | 00 00 00 22
第一个字节0xAA是tableswitch指令的操作码,后面每四个字节为一个操作数。前面四个字节0x00000024转为10进制是36,由于tableswitch指令的偏移量为3,因此该操作数表示匹配default时跳转到偏移量为39的字节码指令。紧随其后的是0x00000005与0x00000008,这两个数代表表格的区间,从5到8,也就是case 5到case 8,虽然我们代码中没有case 7,编译器还是为我们生成了。后面的0x0000001d、0x00000022、0x00000024、0x00000022分别+3得到的结果就是case 5到8分别跳转到的目标字节码指令的绝对偏移量。
从前面的例子我们可以看出,tableswitch指令生成的字节码占用的空间很大,而且当case的值不连续时,还会生成一些无用的映射。如果case的每个值都不连续呢?如:
public int switch2Func(int stat) {
int a = 0;
switch (stat) {
case 1:
a = 0;
break;
case 100:
a = 1;
break;
}
return a;
}
假设,编译器将代码清单3-43的switch语句生成tableswitch指令,那么这条指令将浪费掉4乘以98的字节空间,如果再加个case 1000,那么浪费的空间更大。显然,这种情况下再使用tableswitch指令是不可取的。
使用javap输出switch2Func方法的字节码如下。
public int switch2Func(int);
Code:
0: iconst_0
1: istore_2
2: iload_1
3: lookupswitch { // 2
1: 28
100: 33
default: 35
}
28: iconst_0
29: istore_2
30: goto 35
33: iconst_1
34: istore_2
35: iload_2
36: ireturn
正如你所看到的,编译器使用lookupswitch指令替代了tableswitch指令。lookupswitch指令的操作码为0xAB,与tableswitch指令一样,该指令的操作数也是不定长的,每个操作数的长度为四个字节,操作数存放的也是下一条字节码指令的相对偏移量,注意,还是相对偏移量。以上面例子说明,lookupswitch指令对应的字节码为。
AB | 00 00 00 20 | 00 00 00 02 | 00 00 00 01
00 00 00 19 | 00 00 00 64 | 00 00 00 1E
第一个字节0xAB是lookupswitch指令的操作码,接着后面四个字节也是匹配default时跳转的目标指令相对当前指令的偏移量,紧随其后四个字节0x00000002代表后面跟随多少个条件映射,每八个字节为一个条件映射,前四个字节为匹配条件,后四个字节为条件匹配时跳转的目标字节码指令的相对偏移量。0x00000001表示当当前操作数栈栈顶的值为1时,跳转到相对偏移量为0x00000019的字节码指令,0x00000019转为10进制是25,加上当前lookupswitch指令的偏移量3等于28;0x00000064转为十进制为100,0x0000001E转为十进制加上3等于33。
三目运算符也叫三元运算符,这是由三个操作数组成的运算符。
使用三目运算符的案例代码如下。
public int syFunc(boolean sex) {
return sex ? 1 : 0;
}
使用javap命令输出syFunc方法的字节码如下。
public int syFunc(boolean);
Code:
0: iload_1
1: ifeq 8
4: iconst_1
5: goto 9
8: iconst_0
9: ireturn
由于方法参数sex是boolean类型,因此使用sex作为条件表达式编译后会使用ifeq指令实现跳转,即与0比较。当前操作数栈顶元素的值等于0则跳转,不等于0继续往下执行。
三目运算符的表达式为:<表达式1> ? <表达式2> :<表达式3>。因此三目运算符也支持多层嵌套,但实际开发中不建议这么做,因为会导致代码能以理解。
发布于:2021 年 08 月 21 日
作者: 吴就业
链接: https://github.com/wujiuye/JVMByteCodeGitBook
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
📚目录
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。