字节码指令之控制转移指令

目录

一、概述

二、比较指令

三、条件跳转指令

四、比较条件跳转指令

五、多条件分支跳转指令

 六、无条件跳转指令


一、概述

程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为:

  • 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时跳转

注意:

  1. 与前面运算规则一致
  • 对于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。

49

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

50

【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    	 //方法返回
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值