字节码指令(下篇)

上一篇

控制转移指令

程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为
1)比较指令、2)条件跳转指令、3)比较条件跳转指令、4)多条件分支跳转指令、5)无条件跳转指令等。

比较指令

,比较指令的作用是比较栈顶两个元素的大小,并将比较结果入栈。

●比较指令有: 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,则压入0;若v1>v2则压入1: 若v1<v2则压入-1。

两个指令的不同之处在于,如果遇到NaN值,fcmpg会 压入1,而fcmp1会压入-1。

数值类型的数据,才可以谈大小!
(byte\shortlchar\int; long\float \double)
boolean、引用数据类型不能比较大小。

条件跳转指令

条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后进行条件跳转。
条件跳转指令有: ifeq, iflt, ifle,ifne,ifgt,ifge,ifnull, ifnonnull。这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)。
它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置。

在这里插入图片描述
注意:
1.与前面运算规则一致:
●对于boolean、byte、 char. short 类型的条件分支比较操作,都是使用int类型的比较指令完成
●对于long、float、 double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转
2.由于各类型的比较最终都会转为int类型的比较操作,所以Java虚拟机提供的int类型的条件分支指令是最为丰富和强大的。
代码实例

 //1.条件跳转指令
    public void compare1(){
        int a = 0;
        if(a != 0){
            a = 10;
        }else{
            a = 20;
        }
    }
 /字节码
  0 iconst_0
 1 istore_1
 2 iload_1
 3 ifeq 12 (+9)
 6 bipush 10
 8 istore_1
 9 goto 15 (+6)
12 bipush 20
14 istore_1
15 return
/
//2.
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
//3
  public void compare3() {
        int i1 = 10;
        long l1 = 20;
        System.out.println(i1 > l1);
    }
 
//字节码
 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 ifle 21 (+7)
17 iconst_1
18 goto 22 (+4)
21 iconst_0
22 invokevirtual #5 <java/io/PrintStream.println>
25 return
///4.
 public int compare4(double d) {
        if (d > 50.0) {
            return 1;
        } else {
            return -1;
        }
    }
   字节码
    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、if_icmpge、if_acmpeq和if_acmpne.其中指令助记符加上“if_”后,以字符“i”开头的指令针对int型整数操作(也包括short和byte类型),以字符“a”开头的指令表示对象引用的比较。
在这里插入图片描述这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下条语句。

代码实例:

//2.比较条件跳转指令
    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);
    }
/字节码
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_icmple 18 (+7)
14 iconst_1
15 goto 19 (+4)
18 iconst_0
19 invokevirtual #5 <java/io/PrintStream.println>
22 return



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
 public void ifCompare3() {
        Object obj1 = new Object();
        Object obj2 = new Object();
        System.out.println(obj1 == obj2);//false
        System.out.println(obj1 != obj2);//true
    }
 /字节码
  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.
在这里插入图片描述

从助记符上看,两者都是switch语句的实现,它们的区别:
●tableswitch要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若千个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高。
●指令lookupswitch内 部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到四配的case值,并根据对应的offset计算跳转地址,因此效率较低。指令lookupswitch处理的是离散的case值,但是出于效率考虑,将case-offset对按照case值大小排序,给定index时,需要查找与index相等的case,获得其offset,如果找不到则跳转到default。
指令tableswitch的示意图如下图所示。由于tableswitch的case值 是连续的,因此只需要记录最低值和最高值,以及每一项对应的offset偏移量,根据给定的index值通过简单的计算即可直接定位到offset。
代码实例

  //3.多条件分支跳转
    public void swtich1(int select){
        int num;
        switch(select){
            case 1:
                num = 10;
                break;
            case 2:
                num = 20;
                //break;
            case 3:
                num = 30;
                break;
            default:
                num = 40;
        }
 //字节码
  0 iload_1
 1 tableswitch 1 to 3	1:  28 (+27)
	2:  34 (+33)
	3:  37 (+36)
	default:  43 (+42)
28 bipush 10
30 istore_2
31 goto 46 (+15)
34 bipush 20
36 istore_2
37 bipush 30
39 istore_2
40 goto 46 (+6)
43 bipush 40
45 istore_2
46 return
//2.
 public void swtich2(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 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
 //jdk7新特性:引入String类型
    public void swtich3(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>
  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-finblly语句,且已经被虚拟机逐渐废弃,故不在这里介绍这两个指令。
在这里插入图片描述
代码实例.

//4.无条件跳转指令
    public void whileInt() {
        int i = 0;
        while (i < 100) {
            String s = "atguigu.com";
            i++;
        }
    }
 ///字节码
  0 iconst_0
 1 istore_1
 2 iload_1
 3 bipush 100
 5 if_icmpge 17 (+12)
 8 ldc #17 <atguigu.com>
10 astore_2
11 iinc 1 by 1
14 goto 2 (-12)
17 return
 public void whileDouble() {
        double d = 0.0;
        while(d < 100.1) {
            String s = "atguigu.com";
            d++;
        }
    }
//字节码
 0 dconst_0
 1 dstore_1
 2 dload_1
 3 ldc2_w #18 <100.1>
 6 dcmpg
 7 ifge 20 (+13)
10 ldc #17 <atguigu.com>
12 astore_3
13 dload_1
14 dconst_1
15 dadd
16 dstore_1
17 goto 2 (-15)
20 return

 public void printFor() {
        short i;
        for (i = 0; i < 100; i++) {
            String s = "atguigu.com";
        }

    }

 0 iconst_0
 1 istore_1
 2 iload_1
 3 bipush 100
 5 if_icmpge 19 (+14)
 8 ldc #17 <atguigu.com>
10 astore_2
11 iload_1
12 iconst_1
13 iadd
14 i2s
15 istore_1
16 goto 2 (-14)
19 return

  public void whileTest(){
        int i = 1;
        while(i <= 100){

            i++;
        }
        //可以继续使用i
    }
    public void forTest(){
        for(int i = 1;i <= 100;i++){

        }
        //不可以继续使用i
    }
//两者字节码
 0 iconst_1
 1 istore_1
 2 iload_1
 3 bipush 100
 5 if_icmpgt 14 (+9)
 8 iinc 1 by 1
11 goto 2 (-9)
14 return

  public void doWhileTest(){
        int i = 1;
        do{
            i++;
        }while(i <= 100);
    }

//字节码
 0 iconst_1
 1 istore_1
 2 iinc 1 by 1
 5 iload_1
 6 bipush 100
 8 if_icmple 2 (-6)
11 return

异常处理指令

抛出异常指令

( 1)athrow指令
在Java程序中显示抛出异常的操作(throw语句)都是由athrow指令来实现。
除了使用throw语句显示抛出异常情况之外,JVN规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在 idiv或 ldiv指令中抛出ArithmeticException异常。
(2)注意
正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时,Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。
在这里插入图片描述
代码实例

 public void throwZero(int i){
        if(i == 0){
            throw new RuntimeException("参数值为0");
        }
    }
 0 iload_1
 1 ifne 14 (+13)
 4 new #2 <java/lang/RuntimeException>
 7 dup
 8 ldc #3 <参数值为0>
10 invokespecial #4 <java/lang/RuntimeException.<init>>
13 athrow
14 return

在这里插入图片描述

异常处理和异常表

1.异常护理
在Java虚拟机中,处理异常( catch语句)不是由字节码指令来实现的(早期使用jsr、ret指令),而是采用异常表来完成的。
2、异常表
如果一个方法定义了一个try-catch或者try-finally的异常处理,就会创建一个异常表。它包含了每个异常处理或者finally块的信息。异常表保存了每个异常处理信息。比如:
·起始位置
·结束位置
·程序计数器记录的代码处理的偏移地址
·被捕获的异常类在常量池中的索引
当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。
不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标
代码实例
在这里插入图片描述
该函数反回hello
在这里插入图片描述

同步控制指令

java虚拟机支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的。

方法级的同步

方法级的同步:是隐式的, 即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_ SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法:
当调用方法时,调用指令将会检查方法的ACC SYNCHRONIZED访问标志是否设置。

如果设置了,执行线程将先持有同步锁,然后执行方法。最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁。

●在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获得同一个锁。
如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。

说明:
这段代码和普通的无同步操作的代码没有什么不同,没有使用monitorenter和monitorexi进行同步区控制。这是因为,对于同步方法而言,当虚拟机通过方法的访问标示符判断是一个同步方法时,会自动在方法调用前进行加锁,当同步方法执行完毕后,不管方法是正常结束还是有异常抛出,均会由虚拟机释放这个锁。因此,对于同步方法而言,monitorenter和monitorexit指令是隐式存在的,并未直接出现在字节码中。

方法内指定指令序列的同步

同步一段指令集序列。通常是由java中的synchronized语句块来表示的。jvm的指令集有 monitorenter和monitorexit两条指令来支持synchronized关键字的语义。
当一个线程进入同步代码块时,它使用monitorenter指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为9,才会被允许进入同步块。
当线程退出同步块时,需要使用monitorexit声明退出。在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。
指令monitorenter和monitorexit在执行时,都需要在操作数栈顶压入对象,之后monitorenter和monitorexit的锁定和释放都是针对这个对象的监视器进行的。
下图展示了监视器如何保护临界区代码不同时被多个线程访问,只有当线程4离开临界区后,线程1、2、3才有可能进入。
在这里插入图片描述
在这里插入图片描述
下一篇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月屯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值