目录
先来看两道面试题:看会不会写,等讲解完相关的字节码知识点,最后还会再来分析这两道题的;
- public class Test {
- public static void main(String[] args){
- int result = test2();
- System.out.println(result);
- }
-
- public static int test2(){
- int i = 1;
- try{
- i++;
- throw new Exception();
- }catch(Exception e){
- i--;
- System.out.println("catch block i = "+i);
- }finally{
- i = 10;
- System.out.println("finally block i = "+i);
- }
- return i;
- }
- }
输出结果:

然后对刚刚到例题进行改动一下,来看看输出结果会是什么?
- public class Test1 {
- public static void main(String[] args){
- int result = test3();
- System.out.println(result);
- }
-
- public static int test3(){
- int i = 1;
- try{
- i++;
- System.out.println("try block, i = "+i);
- return i;
- }catch(Exception e){
- i ++;
- System.out.println("catch block i = "+i);
- return i;
- }finally{
- i = 10;
- System.out.println("finally block i = "+i);
- }
- }
- }

看一段简单的代码:
- public class TryCatchTest {
- public static void main(String[] args) {
- int i = 0;
- try {
- i = 10;
- }catch (Exception e) {
- i = 20;
- }
- }
- }
对应字节码指令分析:

字节码指令:return 表示结束程序,ireturn才表示返回数据;
可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测 2~4 行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
第8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 2 号槽位
异常表监测的范围内发生异常后,会直接跳转到异常表中target对应的字节码指令,中间的字节码指令不再执行,比如这个案例中的goto指令就没有执行
- public static void main(String[] args) {
- int i = 0;
- try {
- i = 10;
- }catch (ArithmeticException e) {
- i = 30;
- }catch () {
- i = 40;
- }catch(Exception e){
- i = 50;
- }
- }
- Code:
- stack=1, locals=3, args_size=1
- 0: iconst_0
- 1: istore_1
- 2: bipush 10
- 4: istore_1
- 5: goto 26 //通过异常表可以知道,发生异常跳到26行
- 8: astore_2 //存储异常对象的引用
- 9: bipush 30
- 11: istore_1
- 12: goto 26 //通过异常表可以知道,发生异常跳到26行
- 15: astore_2 //存储异常对象的引用
- 16: bipush 40
- 18: istore_1
- 19: goto 26 //通过异常表可以知道,发生异常跳到26行
- 22: astore_2 //存储异常对象的引用
- 23: bipush 50
- 25: istore_1
- 26: return
- Exception table:
- from to target type //这里的target和上面的行号对应
- 2 5 8 Class java/lang/ArithmeticException
- 2 5 15 Class java/lang/NullPointerException
- 2 5 22 Class java/lang/Exception
- LineNumberTable....
- LocalVariableTable: //Slot 多个变量使用同一个局部变量槽是为了复用,因为这些异常同一时刻只能发生一种,所以没必要创建多个槽位来存储异常对象,可以减少开销
- Start Length Slot Name Signature
- 9 3 2 e Ljava/lang/ArithmeticException;
- 16 3 2 e Ljava/lang/NullPointerException;
- 23 3 2 e Ljava/lang/Exception;
- 0 27 0 args [Ljava/lang/String;
- 2 25 1 i I
因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用,这样可以节省内存空间;
- public static void main(String[] args) {
- int i = 0;
- try {
- i = 10;
- } catch (Exception e) {
- i = 20;
- } finally {
- i = 30;
- }
- }
对应字节码:
从下面字节码中我们可以看到,finally的作用是把finally中的代码快复制多分,然后分别放到try代码块后,catch代码块后(goto指令前),但是有时候catch并不能完全catch你想要的exception,所以这个字节码指令会多一个保障,就是在异常表中多捕获一个异常any,和对catch多捕获一个any的异常,下面的异常表中有;
- Code:
- stack=1, locals=4, args_size=1
- 0: iconst_0
- 1: istore_1
- //try块
- 2: bipush 10 //-----------try try的范围可以从异常表中查询到
- 4: istore_1
- //try块执行完后,会执行finally,即便try中发生了异常导致try中的finally指令无法执行,但是发生异常后会跳转到catch中,catch中还是有finally中的代码指令 的
- 5: bipush 30 //-----------fainal 中的i = 30
- 7: istore_1 // 把30赋值给i,覆盖局部变量表中的1号槽位的数据
- 8: goto 27 //跳转到return指令,结束程序
- //catch块
- 11: astore_2 //把异常信息放入局部变量表的2号槽位
- 12: bipush 20 //catch中的 i = 20代码
- 14: istore_1 //覆盖局部变量表中的1号槽位的数据
- //catch块执行完后,会执行finally
- 15: bipush 30 //-----------fainal 中的i = 30
- 17: istore_1
- 18: goto 27 //跳转到return指令,结束程序
- //出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码
- 21: astore_3 //存储其他类型的异常
- 22: bipush 30 //-----------fainal 中的i = 30
- 24: istore_1
- 25: aload_3 //找到刚刚没有名字的异常
- 26: athrow //抛出这个没有名字的异常
- 27: return
- Exception table:
- from to target type
- 2 5 11 Class java/lang/Exception
- 2 5 21 any //any表示的是除了你要捕获的异常之外的异常
- 11 15 21 any
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程 注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次;
先看一段代码:
- public class FinallyReturnTest {
- public static void main(String[] args) {
- int i = FinallyReturnTest.test();
- // 结果为 20
- System.out.println(i);
- }
- public static int test() {
- int i;
- try {
- i = 10;
- return i;
- } finally {
- i = 20;
- return i;
- }
- }
- }
对应字节码文件分析:
- Code:
- stack=1, locals=3, args_size=0
- 0: bipush 10 //放入栈顶
- 2: istore_0 //slot 0 (从栈顶移除了,把该值存储到局部变量表中了)
- 3: iload_0 //从局部变量表中把0号槽位的数据加载到栈中
- 4: istore_1 //注意:暂存返回值,又把这个10存储到局部变量表中的【1号】槽位 备份使用
- 5: bipush 20 //------finally中代码块
- 7: istore_0 //20这个值对0号槽位的10进行了覆盖
- 8: iload_0 //把0号槽位的值加载到栈中
- 9: ireturn // ireturn 会【返回操作数栈顶】的整型值 20,返回的数据是操作数栈中的数据
- // 如果出现异常,还是会执行finally 块中的内容,没有抛出异常
- 10: astore_2 // 存储异常,从异常表中得出该行指令是存储移除用的
- 11: bipush 20 //------finally中代码块
- 13: istore_0 //20这个值对0号槽位的10进行了覆盖
- 14: iload_0 //把0号槽位的值加载到栈中
- 15: ireturn //注意:这里没有 athrow 了,也就是如果在 finally 块中如果有返回操作的话,那么try中代码块出现异常,会吞掉异常!并不会抛出异常
- Exception table:
- from to target type
- 0 5 10 any
由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以finally的为准
至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子(有大用)
跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常,如果try中也有return那么try中的return和finally中的return最终在字节码层面只会执行一条return指令
所以不要在finally中进行返回操作
- public static int test() {
- int i;
- try {
- i = 10;
- // 这里应该会抛出异常
- i = i/0;
- return i;
- } finally {
- i = 20;
- return i;
- }
- }
会发现打印结果为 20 ,而且并未抛出异常;

但是如果我们把finally中的return给去掉,那么返回的又是什么?
- public static int test() {
- int i = 10;
- try {
- return i;
- } finally {
- i = 20; //最后的结果是返回10 !!!
- }
- }
对应的字节码:
- Code:
- stack=1, locals=3, args_size=0
- 0: bipush 10 //把10放入栈顶
- 2: istore_0 // 把10存储在局部变量表的0号槽位
- 3: iload_0 // 然后从局部变量表中把10又加载到操作数栈顶,按理说此时该返回了,但是明显没有立马返回,而是istore_1,把刚刚加载到操作数栈中的10又在局部变量表中的1号槽位备份一份
- 4: istore_1 // 加载到局部变量表的1号位置,【目的是为了固定返回值】
- 5: bipush 20 //------执行finally代码块
- 7: istore_0 // 把20赋值给i
- 8: iload_1 // 【加载局部变量表1号位置的数10到操作数栈】
- 9: ireturn // 返回操作数栈顶元素 10
- 10: astore_2 //存储异常对象
- 11: bipush 20 //------执行finally代码块
- 13: istore_0 //把20赋值给i
- 14: aload_2 // 加载异常
- 15: athrow // 仍然会抛出异常
- Exception table:
- from to target type
- 3 5 10 any
在把finally中的return去掉后,我们发现如果在try中进行了return, 如果没有发生异常的话,那么即便finally中的变量发生了变化,那么try中返回的依旧是try中的变量值,因为我们可以从字节码指令看到try中的变量会先被备份一次用来返回;
如果发生了异常那么返回的值就是catch中的变量:比如下面的案例;

字节码分析:

这里就不再使用字节码码来分析了,前面已经把try catch finally对应的字节码都分析了一遍,所以这里就不再使用字节码来分析了;
- public class Test {
- public static void main(String[] args){
- int result = test2();
- System.out.println(result);
- }
-
- public static int test2(){
- int i = 1;
- try{
- i++; //i从自增变成2
- throw new Exception(); //抛出异常,被catch捕获
- }catch(Exception e){
- i--; //对i进行自减操作 变为1
- System.out.println("catch block i = "+i); //执行这行代码 此时i为1
- //finally中的代码被拷贝到这里来执行 【i = 10 把之前的i进行覆盖 ,此时局部变量表中存储的i为10】
- //输出System.out.println("finally block i = "+10);
- }finally{
- i = 10;
- System.out.println("finally block i = "+i);
- }
- return i; //返回i,先从局部变量表中加载对应的变量,然后进行弹栈
- }
- }
所以输出的是:
- catch block i = 1
- finally block i = 10
- 10
第二题:
- public class Test1 {
- public static void main(String[] args){
- int result = test3();
- System.out.println(result);
- }
-
- public static int test3(){
- int i = 1;
- try{
- i++; //i从自增变成2,然后存储到局部变量表中
- System.out.println("try block, i = "+i); //输出
- //因为没有发生异常所以不会跳转到catch中,但是finally中的代码会被拷贝在return之前来执行
- //因为finally中没有return,所以try和catch中的变量都会自己额外拷贝一份,用来最后作为返回值返回
- return i;
- }catch(Exception e){
- i ++;
- System.out.println("catch block i = "+i);
- return i;
- }finally{
- i = 10;
- System.out.println("finally block i = "+i);
- }
- }
- }
输出结果:
- try block, i = 2
- finally block i = 10
- 2
字节码片段分析:如果finally中没有return,那么try 和catch中都是会对直接到 i 进行备份用于返回的,即便finally中的代码改变了其值,但是最后返回的值还是以try和catch中的值为准,因为JVM返回的是备份的值;

