通过字节码分析fanally面试题

通过字节码分析finally面试题

首先我们先来看两道面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo2_2 {

public static void main(String[] args) {
System.out.println(test());
}
public static int test(){
try {
return 10;
} catch (Exception e){
return 20;
}finally {
return 30;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo2_3 {
public static void main(String[] args) {
System.out.println(test());
}

public static int test(){
int i ;
try {
i = 10;
return i;
} catch (Exception e) {
// e.printStackTrace();
i = 20;
return i;
} finally {
i =30;
}
}
}

问两道题分别输出什么?为什么?


我们分开来看,先看第一道题

利用javap -v 指令生成字节码, 为了方便观看,我们只留test方法.

这里相比之前的字节码多了一个Exception table, 是用来实现异常处理.

我们来逐行分析字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0 // 栈帧深度为1 局部变量表长度为3 因为是static 所以没有this引用,也没有行参,所以行参数量为0
0: bipush 10 // 准备一个为10的常量,放入栈中 ------------------
2: istore_0 // 将10放入局部变量表1的槽位中 try部分代码 |
3: bipush 30 // 准备一个为30的常量,放入栈中 ---------
5: ireturn // ireturn 代表返回一个int类型的数据,也就是栈顶的30
6: astore_0 // 将Exception对象放入局部变量表 -------------------
7: bipush 20 // 准备一个20的常量放入栈中 cache部分 |
9: istore_1 // 将栈顶的20放入局部变量表 代码 |
10: bipush 30 // 准备一个30的常量放入栈中 -------------------
12: ireturn // 返回栈顶的30 -
13: astore_2 // 将异常类型放入局部变量表 ---------------------
14: bipush 30 // 准备一个30的常量放入栈中 finally部分 |
16: ireturn // 返回栈顶的30 ---------------------

/**
* 异常表,用来实现异常功能.
* from 代表 监听其实代码.(包含)
* to 代表监听终止代码.(不包含)
* target 代表如果出现异常,则跳转到字节码序号
* type 代表监听类型
*/
Exception table:
from to target type
0 3 6 Class java/lang/Exception //监听0-3,如果出现Exception类型的异常,则跳转到6
0 3 13 any //监听0-3,如果出现非Exception类型的异常,如error,则跳转到13
6 10 13 any //监听6-10,也就是cache代码块,如果出现任意异常,则跳转到13
LineNumberTable:
.....
LocalVariableTable:
Start Length Slot Name Signature
7 6 0 e Ljava/lang/Exception;
}

这里,我们可以看到:

  1. 无论是try代码块,还是cache代码块,在编译期,都会在其后面追加上finally代码块的代码,以保障finall一定执行,因为一个方法只能有一个return,所以前面的return在编译期被优化了.
  2. 为了保证finally代码块无论什么情况都要执行,所以利用了异常表,来保障,即使在try中发生了未cache的异常,或者在cache中发生异常,finally代码块都会执行.
  3. finally代码块,如果有retrun的话,发生异常可能被忽略,没有throw出去.这个例子中,如果cache中发生异常,或者try中发生为捕获的异常,但是却没有throw出去.所以,尽量不要在finally中写return.
  4. 在编译期,显示局部变量表长度为3,但是反编译结果却只有1个.这可能是由于后面两个变量无法确定名称导致.

现在,我们知道了第一道题的答案:输出结果为30.


然后我们再看第二道题

还是先用javap -v 来反编译

同样我们值保留test()方法

然后来逐行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=0 // 栈深度为1 局部变量表长度为4 行参数为0
0: bipush 10 //准备一个常量10 ------------------------------
2: istore_0 //放入局部变量表_0 try代码 |
3: iload_0 //从局部变量表_0去除10 |
4: istore_1 //放入局部变量表_1 这是局部变量表_1和_2的数值都是10 -----
5: bipush 30 // 准备一个常量30
7: istore_0 // 放入局部变量表_0
8: iload_1 // 将局部变量表_1放入栈顶
9: ireturn // 返回栈顶的元素
10: astore_1 // 将Exception放入局部变量_1 --------------------------
11: bipush 20 // 准备20 cache代码 |
13: istore_0 // 将20放入局部变量表_0 |
14: iload_0 // 加载局部变量表_0 |
15: istore_2 // 放入局部变量表_2 这时局部变量表_0和_2的数值都是20 ----
16: bipush 30 // 准备30
18: istore_0 // 放入局部变量表_0 此时局部变量表0为30 2为20
19: iload_2 // 载入局部变量表_2 也就是数值20
20: ireturn // 返回栈顶数据
21: astore_3 // 将异常放入局部变量表_3
22: bipush 30 // 准备30
24: istore_0 // 放入局部变量表_0
25: aload_3 // 去除局部变量表_3的异常
26: athrow // throw异常
Exception table:
........
LineNumberTable:
...
LocalVariableTable:
Start Length Slot Name Signature
3 7 0 i I
11 10 1 e Ljava/lang/Exception;
14 7 0 i I
25 2 0 i I
}

这里,我们可以看出:

  1. 在finally中修改return的属性时,我们会先复制一份放入局部变量表,然后修改原有数据,将复制的数据return
  2. 如果finally中没有retrurn,则出现异常时,finally中会主动throw掉.

以上仅为个人观点,如有错误,欢迎指正.谢谢!

0%