从字节码看异常处理
举例,有如下方法,问该方法返回值为多少?
public int a() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
return 3;
}
}
这道问题大部分都容易把返回1,还是返回3弄混了,我们通过jclasslib查看该方法的字节码:
0 goto 8 (+8)
3 astore_1 //将1从操作数栈存储到局部变量表
4 goto 8 (+4)
7 pop //从操作数栈取出1个元素
8 iconst_3 //加载常量3到操作数栈
9 ireturn //返回操作数栈顶值
第1条和第3条字节码都是无条件跳转到第8条字节码指令,也就是说,最终的操作是将3加载到操作数栈并从操作数栈中取出返回。
当我们将方法改为如下:
public int a() {
try {
return 1/0;
} catch (Exception e) {
return 2;
} finally{
return 3;
}
}
这时候,按我们刚才的想法,操作数栈顶的数字为3,应该返回3,事实上也如我们所想他返回的也是3,那么它的字节码操作是怎样的呢?
0 iconst_1 //加载1到操作数栈
1 iconst_0 //加载2到操作数栈
2 idiv //抛出异常
3 pop //将值从操作数栈取出
4 goto 12 (+8) //跳转到12行
7 astore_1
8 goto 12 (+4) //跳转到12行
11 pop
12 iconst_3 //
13 ireturn //
可以看到,无论是否会实际抛出异常,字节码指令都会跳转到finally语句块中执行。那么我们去掉finally语句块会怎样呢?将上述方法去掉finally的语句块后,它的字节码如下:
0 iconst_1
1 iconst_0
2 idiv
3 ireturn
4 astore_1
5 iconst_2
6 ireturn
由此我们可以得出结论,finally的实现是由goto和pop两条字节码指令实现的,而ireturn指令的定义如下:
当前方法必须具有返回类型boolean,byte,short,char或int。该值必须是int类型。如果当前方法是同步方法,则在调用方法时输入或重新输入的监视器将被更新并可能退出,就好像通过在当前线程中执行monitorexit指令(monitorexit)一样。如果没有抛出异常,则从当前帧(第2.6节)的操作数堆栈中弹出值,并将其推送到调用者帧的操作数堆栈。当前方法的操作数堆栈上的任何其他值都将被丢弃。
解释器然后将控制返回给方法的调用者,恢复调用者的框架。
而且要注意的时idiv指令的定义:
value1和value2都必须是int类型。值从操作数堆栈中弹出。 int结果是Java编程语言表达式value1 / value2的值。结果被推到操作数堆栈上。
一个int分组向0舍入;也就是说,n / d中int值产生的商是一个int值q,其大小尽可能大,同时满足| d·q |。 ≤| n |。此外,当| n |时,q为正≥| d |并且n和d具有相同的符号,但是当| n |时q为负≥| d |和n和d有相反的符号。
有一种特殊情况不满足此规则:如果被除数是int类型的最大可能量值的负整数,且除数为-1,则发生溢出,结果等于被除数。尽管溢出,但在这种情况下不会抛出任何异常。
也就是说1/0会抛出异常,但是Integer.MIN_VALUE/(-1)不会抛出异常。而且java中异常的处理不是用字节码指令,而是通过异常表抛出(如下,异常区域从start PC--0行到end PC--3行,如果出现了catch Type中的异常则跳到handler PC中的第4行执行)。
从字节码角度看待线程安全
例如,有如下方法:
public int a(int args) {
return args;
}
问该线程是否安全?
乍一看,该方法只有一条语句,应该会满足原子性,但事实是这样吗?我们查看其字节码指令:
0 iload_1
1 ireturn
可以看到,该方法首先是将一个整形变量从局部变量表加载到操作数栈,然后再取操作数栈顶元素返回,也就是无论是局部变量还是常量,都要先加载到操作数栈再取出返回,所以该操作并不是原子操作,该方法也不是线程安全的。