• 从字节码角度带你彻底理解异常中catch,return和finally,再也不用死记硬背了


    目录

    try-catch字节码

    多个catch的字节码指令分析

    finally字节码分析

    finally 中带return 

    把finally中的return给去掉

    开始两个例题的分析

    先来看两道面试题:看会不会写,等讲解完相关的字节码知识点,最后还会再来分析这两道题的;

    1. public class Test {
    2. public static void main(String[] args){
    3. int result = test2();
    4. System.out.println(result);
    5. }
    6. public static int test2(){
    7. int i = 1;
    8. try{
    9. i++;
    10. throw new Exception();
    11. }catch(Exception e){
    12. i--;
    13. System.out.println("catch block i = "+i);
    14. }finally{
    15. i = 10;
    16. System.out.println("finally block i = "+i);
    17. }
    18. return i;
    19. }
    20. }

    输出结果:

    然后对刚刚到例题进行改动一下,来看看输出结果会是什么?

    1. public class Test1 {
    2. public static void main(String[] args){
    3. int result = test3();
    4. System.out.println(result);
    5. }
    6. public static int test3(){
    7. int i = 1;
    8. try{
    9. i++;
    10. System.out.println("try block, i = "+i);
    11. return i;
    12. }catch(Exception e){
    13. i ++;
    14. System.out.println("catch block i = "+i);
    15. return i;
    16. }finally{
    17. i = 10;
    18. System.out.println("finally block i = "+i);
    19. }
    20. }
    21. }

     

    try-catch字节码

    看一段简单的代码:

    1. public class TryCatchTest {
    2. public static void main(String[] args) {
    3. int i = 0;
    4. try {
    5. i = 10;
    6. }catch (Exception e) {
    7. i = 20;
    8. }
    9. }
    10. }

    对应字节码指令分析:

     字节码指令:return 表示结束程序,ireturn才表示返回数据;

    • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测 2~4 行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号

    • 第8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 2 号槽位

    • 异常表监测的范围内发生异常后,会直接跳转到异常表中target对应的字节码指令,中间的字节码指令不再执行,比如这个案例中的goto指令就没有执行

     

    多个catch的字节码指令分析

    1. public static void main(String[] args) {
    2. int i = 0;
    3. try {
    4. i = 10;
    5. }catch (ArithmeticException e) {
    6. i = 30;
    7. }catch () {
    8. i = 40;
    9. }catch(Exception e){
    10. i = 50;
    11. }
    12. }
    1. Code:
    2. stack=1, locals=3, args_size=1
    3. 0: iconst_0
    4. 1: istore_1
    5. 2: bipush 10
    6. 4: istore_1
    7. 5: goto 26 //通过异常表可以知道,发生异常跳到26行
    8. 8: astore_2 //存储异常对象的引用
    9. 9: bipush 30
    10. 11: istore_1
    11. 12: goto 26 //通过异常表可以知道,发生异常跳到26行
    12. 15: astore_2 //存储异常对象的引用
    13. 16: bipush 40
    14. 18: istore_1
    15. 19: goto 26 //通过异常表可以知道,发生异常跳到26行
    16. 22: astore_2 //存储异常对象的引用
    17. 23: bipush 50
    18. 25: istore_1
    19. 26: return
    20. Exception table:
    21. from to target type //这里的target和上面的行号对应
    22. 2 5 8 Class java/lang/ArithmeticException
    23. 2 5 15 Class java/lang/NullPointerException
    24. 2 5 22 Class java/lang/Exception
    25. LineNumberTable....
    26. LocalVariableTable: //Slot 多个变量使用同一个局部变量槽是为了复用,因为这些异常同一时刻只能发生一种,所以没必要创建多个槽位来存储异常对象,可以减少开销
    27. Start Length Slot Name Signature
    28. 9 3 2 e Ljava/lang/ArithmeticException;
    29. 16 3 2 e Ljava/lang/NullPointerException;
    30. 23 3 2 e Ljava/lang/Exception;
    31. 0 27 0 args [Ljava/lang/String;
    32. 2 25 1 i I

    因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用,这样可以节省内存空间;

    finally字节码分析

    1. public static void main(String[] args) {
    2. int i = 0;
    3. try {
    4. i = 10;
    5. } catch (Exception e) {
    6. i = 20;
    7. } finally {
    8. i = 30;
    9. }
    10. }

    对应字节码:

    从下面字节码中我们可以看到,finally的作用是把finally中的代码快复制多分,然后分别放到try代码块后,catch代码块后(goto指令前),但是有时候catch并不能完全catch你想要的exception,所以这个字节码指令会多一个保障,就是在异常表中多捕获一个异常any,和对catch多捕获一个any的异常,下面的异常表中有;

    1. Code:
    2. stack=1, locals=4, args_size=1
    3. 0: iconst_0
    4. 1: istore_1
    5. //try块
    6. 2: bipush 10 //-----------try try的范围可以从异常表中查询到
    7. 4: istore_1
    8. //try块执行完后,会执行finally,即便try中发生了异常导致try中的finally指令无法执行,但是发生异常后会跳转到catch中,catch中还是有finally中的代码指令 的
    9. 5: bipush 30 //-----------fainal 中的i = 30
    10. 7: istore_1 // 把30赋值给i,覆盖局部变量表中的1号槽位的数据
    11. 8: goto 27 //跳转到return指令,结束程序
    12. //catch块
    13. 11: astore_2 //把异常信息放入局部变量表的2号槽位
    14. 12: bipush 20 //catch中的 i = 20代码
    15. 14: istore_1 //覆盖局部变量表中的1号槽位的数据
    16. //catch块执行完后,会执行finally
    17. 15: bipush 30 //-----------fainal 中的i = 30
    18. 17: istore_1
    19. 18: goto 27 //跳转到return指令,结束程序
    20. //出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码
    21. 21: astore_3 //存储其他类型的异常
    22. 22: bipush 30 //-----------fainal 中的i = 30
    23. 24: istore_1
    24. 25: aload_3 //找到刚刚没有名字的异常
    25. 26: athrow //抛出这个没有名字的异常
    26. 27: return
    27. Exception table:
    28. from to target type
    29. 2 5 11 Class java/lang/Exception
    30. 2 5 21 any //any表示的是除了你要捕获的异常之外的异常
    31. 11 15 21 any

    可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程 注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次;

    finally 中带return

    先看一段代码:

    1. public class FinallyReturnTest {
    2. public static void main(String[] args) {
    3. int i = FinallyReturnTest.test();
    4. // 结果为 20
    5. System.out.println(i);
    6. }
    7. public static int test() {
    8. int i;
    9. try {
    10. i = 10;
    11. return i;
    12. } finally {
    13. i = 20;
    14. return i;
    15. }
    16. }
    17. }

    对应字节码文件分析:

    1. Code:
    2. stack=1, locals=3, args_size=0
    3. 0: bipush 10 //放入栈顶
    4. 2: istore_0 //slot 0 (从栈顶移除了,把该值存储到局部变量表中了)
    5. 3: iload_0 //从局部变量表中把0号槽位的数据加载到栈中
    6. 4: istore_1 //注意:暂存返回值,又把这个10存储到局部变量表中的【1号】槽位 备份使用
    7. 5: bipush 20 //------finally中代码块
    8. 7: istore_0 //20这个值对0号槽位的10进行了覆盖
    9. 8: iload_0 //把0号槽位的值加载到栈中
    10. 9: ireturn // ireturn 会【返回操作数栈顶】的整型值 20,返回的数据是操作数栈中的数据
    11. // 如果出现异常,还是会执行finally 块中的内容,没有抛出异常
    12. 10: astore_2 // 存储异常,从异常表中得出该行指令是存储移除用的
    13. 11: bipush 20 //------finally中代码块
    14. 13: istore_0 //20这个值对0号槽位的10进行了覆盖
    15. 14: iload_0 //把0号槽位的值加载到栈中
    16. 15: ireturn //注意:这里没有 athrow 了,也就是如果在 finally 块中如果有返回操作的话,那么try中代码块出现异常,会吞掉异常!并不会抛出异常
    17. Exception table:
    18. from to target type
    19. 0 5 10 any
    • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以finally的为准

    • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子(有大用)

    • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常,如果try中也有return那么try中的return和finally中的return最终在字节码层面只会执行一条return指令

    • 所以不要在finally中进行返回操作

    1. public static int test() {
    2. int i;
    3. try {
    4. i = 10;
    5. // 这里应该会抛出异常
    6. i = i/0;
    7. return i;
    8. } finally {
    9. i = 20;
    10. return i;
    11. }
    12. }

    会发现打印结果为 20 ,而且并未抛出异常;

    把finally中的return给去掉

     但是如果我们把finally中的return给去掉,那么返回的又是什么?

    1. public static int test() {
    2. int i = 10;
    3. try {
    4. return i;
    5. } finally {
    6. i = 20; //最后的结果是返回10 !!!
    7. }
    8. }

    对应的字节码:

    1. Code:
    2. stack=1, locals=3, args_size=0
    3. 0: bipush 10 //把10放入栈顶
    4. 2: istore_0 // 把10存储在局部变量表的0号槽位
    5. 3: iload_0 // 然后从局部变量表中把10又加载到操作数栈顶,按理说此时该返回了,但是明显没有立马返回,而是istore_1,把刚刚加载到操作数栈中的10又在局部变量表中的1号槽位备份一份
    6. 4: istore_1 // 加载到局部变量表的1号位置,【目的是为了固定返回值】
    7. 5: bipush 20 //------执行finally代码块
    8. 7: istore_0 // 把20赋值给i
    9. 8: iload_1 // 【加载局部变量表1号位置的数10到操作数栈】
    10. 9: ireturn // 返回操作数栈顶元素 10
    11. 10: astore_2 //存储异常对象
    12. 11: bipush 20 //------执行finally代码块
    13. 13: istore_0 //把20赋值给i
    14. 14: aload_2 // 加载异常
    15. 15: athrow // 仍然会抛出异常
    16. Exception table:
    17. from to target type
    18. 3 5 10 any

    在把finally中的return去掉后,我们发现如果在try中进行了return, 如果没有发生异常的话,那么即便finally中的变量发生了变化,那么try中返回的依旧是try中的变量值,因为我们可以从字节码指令看到try中的变量会先被备份一次用来返回;

    如果发生了异常那么返回的值就是catch中的变量:比如下面的案例;

    字节码分析: 

     

    开始两个例题的分析

    这里就不再使用字节码码来分析了,前面已经把try  catch  finally对应的字节码都分析了一遍,所以这里就不再使用字节码来分析了;

    1. public class Test {
    2. public static void main(String[] args){
    3. int result = test2();
    4. System.out.println(result);
    5. }
    6. public static int test2(){
    7. int i = 1;
    8. try{
    9. i++; //i从自增变成2
    10. throw new Exception(); //抛出异常,被catch捕获
    11. }catch(Exception e){
    12. i--; //对i进行自减操作 变为1
    13. System.out.println("catch block i = "+i); //执行这行代码 此时i为1
    14. //finally中的代码被拷贝到这里来执行 【i = 10 把之前的i进行覆盖 ,此时局部变量表中存储的i为10】
    15. //输出System.out.println("finally block i = "+10);
    16. }finally{
    17. i = 10;
    18. System.out.println("finally block i = "+i);
    19. }
    20. return i; //返回i,先从局部变量表中加载对应的变量,然后进行弹栈
    21. }
    22. }

    所以输出的是:

    1. catch block i = 1
    2. finally block i = 10
    3. 10

    第二题:

    1. public class Test1 {
    2. public static void main(String[] args){
    3. int result = test3();
    4. System.out.println(result);
    5. }
    6. public static int test3(){
    7. int i = 1;
    8. try{
    9. i++; //i从自增变成2,然后存储到局部变量表中
    10. System.out.println("try block, i = "+i); //输出
    11. //因为没有发生异常所以不会跳转到catch中,但是finally中的代码会被拷贝在return之前来执行
    12. //因为finally中没有return,所以try和catch中的变量都会自己额外拷贝一份,用来最后作为返回值返回
    13. return i;
    14. }catch(Exception e){
    15. i ++;
    16. System.out.println("catch block i = "+i);
    17. return i;
    18. }finally{
    19. i = 10;
    20. System.out.println("finally block i = "+i);
    21. }
    22. }
    23. }

    输出结果:

    1. try block, i = 2
    2. finally block i = 10
    3. 2

    字节码片段分析:如果finally中没有return,那么try 和catch中都是会对直接到 i 进行备份用于返回的,即便finally中的代码改变了其值,但是最后返回的值还是以try和catch中的值为准,因为JVM返回的是备份的值;

     

  • 相关阅读:
    未解决的notebook问题
    python+django+vue高校奖学金评定管理系统
    线上一次JVM FullGC搞得整晚都没睡,彻底崩溃
    09【C语言 & 趣味算法】再识:折半查找(二分查找):基本思想、程序流程图及完整代码、附:顺序查找
    消息摘要(数字摘要)的理解 - 查看很多资料后的感悟
    总结开发中一些数据处理方法的封装
    Hive的基本知识与操作
    C++ stack,queue,priority_queue容器适配器模拟实现
    集成电路工厂用什么ERP?哪家的集成电路ERP比较好
    分布式锁的实现- zookeeper
  • 原文地址:https://blog.csdn.net/weixin_53142722/article/details/125467334