字节码角度看异常处理与线程安全

从字节码看异常处理

举例,有如下方法,问该方法返回值为多少?

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

可以看到,该方法首先是将一个整形变量从局部变量表加载到操作数栈,然后再取操作数栈顶元素返回,也就是无论是局部变量还是常量,都要先加载到操作数栈再取出返回,所以该操作并不是原子操作,该方法也不是线程安全的。

猜你喜欢

转载自blog.csdn.net/yinweicheng/article/details/81269105