📚《深入浅出JVM字节码》

《Java虚拟机字节码:从入门到实战》的开源版本。作者通过自己的实战经验,整合出一套适合新手的高效学习教程。归纳并提炼知识点,制定合理路线,帮助读者更快掌握核心技术。


条件分支语句的实现

Java语言提供的条件分支语句包含if语句、switch语句、三目运算符,这些条件语句是如何通过字节码实现的呢?

if语句

使用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则跳转

switch语句

使用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虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!


📚目录