控制转移指令
程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为
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才有可能进入。
下一篇