什么是异常机制

本文深入探讨Java中的异常处理机制,区分非检查异常与检查异常,解析异常在JVM中的捕获过程,以及finally块的执行逻辑。同时,介绍了Java 7引入的Supressed异常特性,用于解决异常叠加的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java 中的异常意思大致就是就是控制流的非正常转移。在程序出现问题时,执行发生了转移。

在 Java 中,异常分为两种,一种是非检查异常,一种是检查异常。如果按照捕获方式,则是一种显示捕获,一种隐式捕获。

  • 非检查异常
    • 典型的就是 RuntimeException 和 Error。
    • Runtime Exception 的典型代表就是数字越界,用来表示程序虽然无法执行,但是能够拯救一下。
    • 而 Error 则是程序发生了严重的错误,无法挽回只能暂时终止。
  • 检查异常
    • 除了以上的两种代表。其他都是检查异常(checked exception )。需要显示的捕获异常(try-catch,throws 等)。
    • 通常自定义的异常也属于检查异常。

基础关系

尽然异常看似能帮我们处理一下无法发现的错误以及及时丢出来,那是不是很好呢?

答案其实不一定,因为构建一个异常的实例,是很麻烦的。在构建异常实例的时候,JVM 需要生成该异常的栈轨迹。这一操作会注意访问 Java 栈帧,以及在运行过程中的各种调试信息。包括栈帧指向的方法名,第几行代码触发的异常,方法所在的类名。看起来就已经很麻烦了。

虚拟机如何捕获异常

它会在 class 文件中生成一个 Exception Table。这里示例一段代码:

public class test{
    public static void main(String[] args) {
        try {
          int a = 0;
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
}

利用 javac 编译成 class 文件之后,我们通过javap -verbose test.class 查看源码(这里摘录部分源码)

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: goto          10
         5: astore_1
         6: aload_1
         7: invokevirtual #3                  // Method java/lang/Exception.printStackTrace:()V
        10: return
      Exception table:
         from    to  target type
             0     2     5   Class java/lang/Exception

在下方有个 Exception table,其中

  • from 指的是监控代码的起始位置
  • to 指的是监控代码的终止位置(不包括)
  • target 指的是异常处理器的起始位置
  • type 异常类型

上述例子,就是在索引为 0 ~ 2 之间为 try 包括的代码,而 5 则是 catch 代码的开始位置。type 就是异常类型为 Exception 。

触发异常时,遍历异常表,查看捕获的代码是否在条目监控的范围之内,并且异常类型是否符合,如果不符合,则弹出当前 Java 虚拟机当前方法对应的 Java 栈帧。让调用者来。

如果符合,则将控制流转移到 target 对应的代码位置。

finally 在哪?

这里再借用一段代码。

public class testt{
    int a = 1;
    int b = 2;
    int c = 3;
    int d = 4;
    public void test(){
        try {
           a = 1;
          } catch (Exception e) {
            b= 2;
          } finally {
            c = 3;
          }
          d = 4;
     }
}

同样的方法利用 Javap 指令来查看源码(这里摘取部分)。

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: iconst_1
         2: putfield      #2                  // Field a:I
         5: aload_0
         6: iconst_3
         7: putfield      #4                  // Field c:I
        10: goto          35
        13: astore_1
        14: aload_0
        15: iconst_2
        16: putfield      #3                  // Field b:I
        19: aload_0
        20: iconst_3
        21: putfield      #4                  // Field c:I
        24: goto          35
        27: astore_2
        28: aload_0
        29: iconst_3
        30: putfield      #4                  // Field c:I
        33: aload_2
        34: athrow
        35: aload_0
        36: iconst_4
        37: putfield      #5                  // Field d:I
        40: return
      Exception table:
         from    to  target type
             0     5    13   Class java/lang/Exception
             0     5    27   any
            13    19    27   any

可以看到 finally 中的 Filed c 出现了三次,第一份 try 正常的出口,第二份是 catch 的正常出口,最后一份监控整个 try-catch 代码快异常执行路径出口位置(将捕获 try 触发的异常,未被 catch 捕获到,以及 catch 中的异常)。也就是,把 finally 的代码复制到放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。

针对最后一份,Java 编译器会生成一个或多个异常表条目,捕获所有种类的异常(在 javap 中以 any 指代)。这些异常表条目的 target 指针将指向另一份复制的 finally 代码块。并且,在这个 finally 代码块的最后,Java 编译器会重新抛出所捕获的异常。

那么再对于 catch 捕获的异常,而又触发了另一个异常,finally 将捕获后者,这是有问题的。

Supressed

Java 7 引入的 Supressed 就是为了解决这个问题。这个新特性允许开发人员将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息。

但是用起来繁琐,所以 Java 7 还要另外一个语法糖,try-with-resources 来方便使用 Supressed。在字节码层面使用 Supressed 异常,并且还能资源的打开关闭用法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值