从字节码角度分析Java异常实现原理

目录

一、异常在字节码层面的实现

二、throw捕获异常解析

三、Java异常处理中的return和throw命令解析


一、异常在字节码层面的实现

示例:

public class ExceptionParse {
    public static void main(String[] args) {
        int a,b,c,d;
        try {
            a = 6;
        } catch (Exception e) {
            b = 7;
        }finally{
            c = 8;
        }
        d = 9;
    }
}

反编译:

Classfile /D:/java/workspace/JDK/bin/com/lic/test/ExceptionParse.class
  Last modified 2020-1-29; size 698 bytes
  MD5 checksum 265e314f713616a5b32c5b7206658ec5
  Compiled from "ExceptionParse.java"
public class com.lic.test.ExceptionParse
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
{
  public com.lic.test.ExceptionParse();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lic/test/ExceptionParse;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=7, args_size=1
         0: bipush        6   //将一个8位带符号整数(6)压入栈
         2: istore_1          //将int类型值(6)存入局部变量1(a) 
         3: goto          25  //跳转到指令行号为25处,继续执行
         6: astore        5   //栈顶数值出栈存入局部变量数组中指定下标5处的变量中(Exception对象的引用地址)  问题:为什么要先将栈顶元素存储在局部变量中, 因为在捕捉到异常后会将异常引用对象推入栈顶, 然后跳转到异常处理代码(catch部分)
         8: bipush        7   //将一个8位带符号整数(7)压入栈
        10: istore_2          //将int类型值(7)存入局部变量2(b) 
        11: bipush        8   //将一个8位带符号整数(8)压入栈
        13: istore_3          //将int类型值(8)存入局部变量3(c)
        14: goto          28  //跳转到指令行号为28,继续执行
        17: astore        6   //栈顶数值出栈存入局部变量数组中指定下标6处的变量中(Exception对象的引用地址)
        19: bipush        8   //将一个8位带符号整数(8)压入栈
        21: istore_3          //将int类型值(8)存入局部变量3(c) 
        22: aload         6   //从局部变量6中装载引用类型值 
        24: athrow            //抛出异常
        25: bipush        8   //将一个8位带符号整数(8)压入栈
        27: istore_3          //将int类型值(8)存入局部变量3(c) 
        28: bipush        9   //将一个8位带符号整数(9)压入栈
        30: istore        4   //将int类型值(9)存入局部变量4(d) 
        32: return
      Exception table:                   //异常表
         from    to  target type         //from: 异常捕获开始位置,即try的开始位置  to:异常捕获结束位置(不包括当前位置) target:异常处理器的开始位置, 即catch的开始位置  type:异常类型   
             0     3     6   Class java/lang/Exception    //异常捕获范围为指令行号[0,3), 一旦捕获到异常则立刻跳转到指令行号为6处继续执行
             0    11    17   any
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 8
        line 12: 11
        line 11: 17
        line 12: 19
        line 13: 22
        line 12: 25
        line 14: 28
        line 15: 32
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
           11       6     2     b   I
           14       3     3     c   I
           22       3     3     c   I
           28       5     3     c   I
           32       1     4     d   I
            8       3     5     e   Ljava/lang/Exception;
      StackMapTable: number_of_entries = 4
        frame_type = 70 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 74 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 7 /* same */
        frame_type = 255 /* full_frame */
          offset_delta = 2
          locals = [ class "[Ljava/lang/String;", top, top, int ]
          stack = []
}
SourceFile: "ExceptionParse.java"

程序指令分析: 

异常表:

Exception table:                  
         from    to  target type       
             0     3     6   Class java/lang/Exception   
             0    11    17   any

from: 异常捕获开始位置,即try的开始位置  

to: 异常捕获结束位置(不包括当前位置)

target: 异常处理器的开始位置, 即catch的开始位置  

type: 异常类型, any表示所有类型

 0     3     6 : 异常捕获范围为指令行号[0,3), 一旦捕获到异常则立刻跳转到指令行号为6处继续执行
  

1. 如果程序执行在指令范围为[0,3)中没有出现异常, 则在指令块1执行完成之后跳转到指令块4,5继续执行, 指令块4为finally语句块中的程序指令;

2. 如果程序执行在指令范围为[0,3)中出现异常, 按照异常表, 程序立刻跳转到指令行6处开始执行, 也就是执行指令块2, 在指令块2中包含了catch语句块和finally语句块中的程序指令;  语句块2执行完成之后跳转到指令行28开始继续执行, 也就是执行" d = 9; ", 执行之后返回; 

3. 如果程序执行在指令范围为[0,11)中出现异常, 且该异常不能被定义的异常捕捉, 也就是在try-catch中出现未意料的异常; 那么按照异常表, 程序立刻跳转到指令行17处开始执行, 也就是执行指令块3, 在指令块3中, 主要是执行finally语句块中的内容; 该操作主要是为了保证finally语句块在任何情况下都可以执行到; 指令块3执行之后, 抛出异常; 

异常处理的过程:

当程序触发异常时,Java虚拟机会遍历异常表中的所有条目(即try里面的所有代码)。如果异常找到异常发生的字节码条目,则会跟catch要捕捉的异常匹配,如果匹配,则开始执行catch里面的代码。
如果没有匹配到,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者(caller)中重复上述操作。在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表
如果在catch中发生了异常,那么Java虚拟机会抛弃第一个异常,尝试捕获并处理新的异常。这个在编码中是很不利于调试的。

小结:

  • JVM 采用异常表的方式来处理 try catch 的跳转逻辑
  • finally 的实现采用拷贝 finally 语句块的方式来实现 finally 一定会执行的语义逻辑

二、throw捕获异常解析

示例:

public static void main(String[] args) {
    int a,b,c,d;
    try{
      a = 6;
    }catch(Exception e){
      b = 7;
      throw e;
    }finally{
      c = 8;
    }
    d = 9;
}

反编译:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=7, args_size=1
         0: bipush        6
         2: istore_1
         3: goto          22
         6: astore        5   //当[0,3)出现异常后, 被jvm捕获后,跳转到6行开始执行, 并且会将异常对象引用压入操作数栈, 所以此处需要将异常对象引用存入局部变量5中
         8: bipush        7
        10: istore_2
        11: aload         5   //将异常对象引用从局部变量5中加载到操作数栈中
        13: athrow            //抛出异常, 注意此处抛出异常后,该异常存在于[0,14)范围, 所以也将会被捕获, 然后从14行开始执行
        14: astore        6   //先将异常对象引用存入局部变量6中
        16: bipush        8   //执行finally块指令
        18: istore_3
        19: aload         6   //将异常对象引用从局部变量6中加载到操作数栈中
        21: athrow            //抛出异常, 程序执行结束!
        22: bipush        8
        24: istore_3
        25: bipush        9
        27: istore        4
        29: return
      Exception table:
         from    to  target type
             0     3     6   Class java/lang/Exception
             0    14    14   any

为什么13行的athrow指令抛出的异常会被再次捕捉?

因为, 异常表第二行监控的范围是try-catch, 而我们在catch块中使用了throw, 将异常抛出了, 也就是catch块中出现了异常, 当然就会被捕捉到, 然后开始执行异常处理模块; 如果没有出现异常, 则在第3行执行之后, 跳转到22行执行finally块代码和try-catch-finally外的代码, 这样也就保证了finally块中的代码一定会执行

注意: 在第一示例中, 由于我们没有在catch块中使用throw, 所以jvm在编译时使用goto指令, 将执行控制权跳转到其他位置了

athrow执行过程:

1. 拿到栈顶的异常对象,

2. 从本帧开始,一个个去找帧对应方法的异常处理表, 标准是:

  • 当前pc在异常表里面指定范围[start,end)里面(异常表记录try块的起止指令)
  • 异常对象的类型属于type

target:指向了catch块开始的pc位置
3. 如果找到了:

  • 操作数栈清空
  • 异常对象引用推入栈顶
  • 跳转到异常处理代码 

4. 这帧如果不是: 则把frame弹出,继续遍历; 帧都出完,都没找到匹配的方法异常处理表项, 说明根本没有对应的catch块存在,  只好打印出Java虚拟机栈信息


三、Java异常处理中的return和throw命令解析

如果把return和throw放在一起,直接会提示错误。"Unreachable statement"(无法被执行).

然而finally却可以成功骗过编译器让两者并存(是不是可以算是编译器的一个小bug呢),结果是后执行的会覆盖前者。

finally如果有return会覆盖catch里的throw,同样如果finally里有throw会覆盖catch里的return。进而如果catch里和finally都有return finally中的return会覆盖catch中的。throw也是如此。

这样就好理解一些了,retrun和throw都是使程序跳出当前的方法,自然就是冲突的。如果非要跳出两次那么后者会覆盖前者。

示例:

public class T {
    public T() {
    }
 
    boolean testEx() throws Exception {
        boolean ret = true;
        try {
            ret = testEx1();
        } catch (Exception e) {
            System.out.println("testEx, catch exception");
            ret = false;
            throw e;
        } finally {
            System.out.println("testEx, finally; return value=" + ret);
            return ret;
        }
    }
 
    boolean testEx1() throws Exception {
        boolean ret = true;
        try {
            ret = testEx2();
            if (!ret) {
                return false;
            }
            System.out.println("testEx1, at the end of try");
            return ret;
        } catch (Exception e) {
            System.out.println("testEx1, catch exception");
            ret = false;
            throw e;
        }
        finally {
            System.out.println("testEx1, finally; return value=" + ret);
            return ret;
        }
    }
 
    boolean testEx2() throws Exception {
        boolean ret = true;
        try {
            int b = 12;
            int c;
            for (int i = 2; i >= -2; i--) {
                c = b / i;
                System.out.println("i=" + i);
            }
           return true;
        } catch (Exception e) {
            System.out.println("testEx2, catch exception");
            ret = false;
            throw e;
        }
        finally {
            System.out.println("testEx2, finally; return value=" + ret);
            //return ret;
        }
    }
 
    public static void main(String[] args) { 
        T testException1 = new T();
        try {
            testException1.testEx();
        } catch (Exception e) {
            e.printStackTrace();
        }
    } 
}

控制台输出:

i=2
i=1
testEx2, catch exception
testEx2, finally; return value=false
testEx1, catch exception
testEx1, finally; return value=false
testEx, finally; return value=false   (为什么在testEx()方法中的catch块没有捕捉到异常?)

try-catch-finally程序块的执行流程:

首先执行的是try语句块中的语句,这时可能会有以下三种情况:

 1. 如果try块中所有语句正常执行完毕,那么finally块的居于就会被执行,这时分为以下两种情况:

        1.1 如果finally块执行顺利,那么整个try-catch-finally程序块正常完成

        1.2 如果finally块由于原因R突然中止,那么try-catch-finally程序块的结局是“由于原因R突然中止(completes abruptly)”

2. 如果try语句块在执行过程中碰到异常V,这时又分为两种情况进行处理:

        2.1 如果异常V能够被与try相应的catch块catch到,那么第一个catch到这个异常的catch块(也是离try最近的一个与异常V匹配的catch块)将被执行;这时就会有两种执行结果:

              2.1.1 如果catch块执行正常,那么finally块将会被执行,这时分为两种情况:

                          (1) 如果finally块执行顺利,那么整个try-catch-finally程序块正常完成。

                          (2) 如果finally块由于原因R突然中止,那么try-catch-finally程序块的结局是“由于原因R突然中止(completes abruptly)”

             2.1.2 如果catch块由于原因R突然中止,那么finally模块将被执行,分为两种情况:

                          (1) 如果如果finally块执行顺利,那么整个try-catch-finally程序块的结局是“由于原因R突然中止(completes abruptly)”。

                          (2) 如果finally块由于原因S突然中止,那么整个try-catch-finally程序块的结局是“由于原因S突然中止(completes abruptly)”,原因R将被抛弃。

虽然我们在testEx2中使用throw e抛出了异常,但是由于testEx2中有finally块,而finally块的执行结果是complete abruptly的。因为return也是一种导致complete abruptly的原因之一,所以整个try-catch-finally程序块的结果是“complete abruptly”所以在testEx1中调用testEx2时是捕捉不到testEx1中抛出的那个异常的,而只能将finally中的return 结果获取到。当然这种情况是可以避免的,以testEx2为例:如果你一定要使用finally而且又要将catch中 throw的e在testEx1中被捕获到,那么你去掉testEx2中的finally中的return就可以了。

如果将testEx2()中的

finally {
            System.out.println("testEx2, finally; return value=" + ret);
            //return ret;
}

修改为:

finally {
            System.out.println("testEx2, finally; return value=" + ret);
            return ret;
}

程序运行结果:

i=2
i=1
testEx2, catch exception
testEx2, finally; return value=false
testEx1, finally; return value=false
testEx, finally; return value=false

try-catch-finally程序块中的return

从上面的try-catch-finally程序块的执行流程以及执行结果一节中可以看出无论try或catch中发生了什么情况,finally都是会被执行的,那么写在try或者catch中的return语句也就不会真正的从该函数中跳出了,它的作用在这种情况下就变成了将控制权(语句流程)转到 finally块中;这种情况下一定要注意返回值的处理。

例如,在try或者catch中return false了,而在finally中又return true,那么这种情况下不要期待你的try或者catch中的return false的返回值false被上级调用函数获取到,上级调用函数能够获取到的只是finally中的返回值,因为try或者catch中的return 语句只是转移控制权的作用。


那么, 为什么上级调用函数能够获取到的只是finally中的返回值, 为什么finally里的return语句会覆盖掉try 或者 catch 语句里的返回语句?

在上面提到过, 如果finally语句块中存在return语句, 那么try或者catch中的return语句将被JVM编译为跳转(goto), 而不是返回(return);  下面来分析这种情况

程序示例1:

public int test(){
    int a;
    try{
      a = 1;
      return a;
    }catch(Exception e){
        a = 2;
        return a;
    }finally{
        a = 3;
        return a;
    }
}

编译后:

public int test();
descriptor: ()I
flags: ACC_PUBLIC
Code:
  stack=1, locals=3, args_size=1
     0: iconst_1
     1: istore_1
     2: goto          12  //try语句块中的return命令编译为goto,跳转到12行开始执行finally语句块
     5: astore_2
     6: iconst_2
     7: istore_1
     8: goto          12 //catch语句块中的return命令编译为goto,跳转到12行开始执行finally语句块
    11: pop
    12: iconst_3         //由于指令无论是否出现异常都将执行到此处, 所以将finally块内容编译在此处
    13: istore_1
    14: iload_1
    15: ireturn
  Exception table:
     from    to  target type
         0     5     5   Class java/lang/Exception
         0    11    11   any

程序示例2 (去掉实例1finally块中的return语句):

public int test(){
    int a;
    try{
      a = 1;
      return a;
    }catch(Exception e){
        a = 2;
        return a;
    }finally{
        a = 3;
    }
}

编译后:

public int test();
descriptor: ()I
flags: ACC_PUBLIC
Code:
  stack=1, locals=5, args_size=1
     0: iconst_1
     1: istore_1
     2: iload_1
     3: istore        4   //将栈顶元素(a=1)存入局部变量4中, 先去执行finally块中命令
     5: iconst_3          //finally语句块内容复制一份放在try语句块内容之后,如果没有异常, 保证finally语句可以被执行
     6: istore_1 
     7: iload         4   //finally块中命令执行完成后, 加载局部变量4中数值(a=1)出来返回
     9: ireturn           //try语句块中的return命令编译为ireturn, 返回int类型数值
    10: astore_2
    11: iconst_2
    12: istore_1
    13: iload_1
    14: istore        4    //将栈顶元素(a=2)存入局部变量4中, 先去执行finally块中命令
    16: iconst_3          //finally语句块内容复制一份放在try语句块内容之后,如果出现异常, 保证finally语句也可以被执行
    17: istore_1
    18: iload         4   //finally块中命令执行完成后, 加载局部变量4中数值(a=2)出来返回
    20: ireturn           //catch语句块中的return命令编译为ireturn, 返回int类型数值
    21: astore_3
    22: iconst_3
    23: istore_1
    24: aload_3
    25: athrow
  Exception table:
     from    to  target type
         0     5    10   Class java/lang/Exception
         0     5    21   any
        10    16    21   any   //保证在try-catch块中出现异常也可以被捕获

由此可知, 对于finally块中是否存在return命令, jvm采用两种不同的编译方式; 如果finally块中存在return命令, jvm将try-catch快中的return命令编译为goto, 达到跳转作用, 而不是返回, 一旦finally块出现return也就意味着try-catch块中的return命令失效;

 

参考播客: Java异常详解-从字节码角度查看异常实现原理

                 深入理解java异常处理机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值