JVM:基本原理之异常的捕获

【关于作者】

关于作者,我目前蚂蚁金服搬砖任职,在营销投放领域工作了多年,目前在专注于内存数据库相关的应用学习,如果你有任何技术交流或大厂内推及面试咨询,都可以从我的个人博客(https://0522-isniceday.top/)联系上我~

1.异常抛出

显式抛出:代码中使用throw关键字

隐式抛出:JVM中碰到程序无法继续执行的异常状态,自动抛出异常。

2.异常捕获

其涉及三种代码块

  • try代码块:标记需要跟踪的代码
  • catch代码块:跟在 try 代码块之后,用来捕获在 try 代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型之外,catch 代码块还定义了针对该异常类型的异常处理器。在 Java 中,try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会从上至下匹配异常处理器。因此,前面的 catch 代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错
  • finally代码块:在 try 代码块和 catch 代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码,例如关闭已打开的系统资源

3.异常

所有异常都是 Throwable 类或者其子类的实例。当程序触发 Error 时,它的执行状态已经无法恢复,需要中止线程甚至是中止虚拟机。第二子类则是 Exception,涵盖程序可能需要捕获并且处理的异常。

img

非检查异常:RuntimeException 和 Error 属于 Java 里的非检查异常,其他异常都属于检查异常

异常构造:异常构造的成本很高,构造时需要生成该异常的栈轨迹(stack trace),该操作会逐一访问当前线程的Java栈帧,并记录下各种调试信息,包括栈帧所指向的方法名称、类名、文件名,以及代码的行号,此时会当然会忽略掉异常构造器以及填充栈帧的 Java 方法(Throwable.fillInStackTrace),从新建异常位置开始算起,此外还会忽略标记为不可见的Java方法的栈帧

4.JVM是如何捕获异常的

编译生成的字节码文件中,每个方法都附带一张异常表,异常表每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index,bci),用以定位字节码

  • from和to指针:异常处理器监控的范围,例如 try 代码块所覆盖的范围。
  • target 指针:指向异常处理器的起始位置,例如 catch 代码块的起始位置

程序触发异常时,JVM会从上至下遍历异常表的所有条目,当触发异常的字节码的索引值(行号)在某个异常表条目的监控范围内,JVM会判断抛出异常和捕获异常是否匹配,匹配则将控制流转移到该条目的target指针指向的字节码

如果遍历完所有条目都未匹配,则会弹出该方法对应的栈帧,并且在调用者caller上执行上述操作,直到匹配

public static void main(String[] args) {
  try {
    mayThrowException();
  } catch (Exception e) {
    e.printStackTrace();
  }
}
// 对应的 Java 字节码
public static void main(java.lang.String[]);
  Code:
    0: invokestatic mayThrowException:()V
    3: goto 11
    6: astore_1
    7: aload_1
    8: invokevirtual java.lang.Exception.printStackTrace
   11: return
  Exception table:
    from  to target type
      0   3   6  Class java/lang/Exception  // 异常表条目
 

finally代码块的编译:

复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中

上述的复制finally代码块的内容是针对字节码而言,针对下图的异常执行路径,编译器会生成一个或多个异常表的条目,监控整个try catch代码块,并捕获所有的异常种类(在 javap 中以 any 指代),该异常条目的target将指向出口处复制的异常代码,并且,在这个 finally 代码块的最后,Java 编译器会重新抛出所捕获的异常。

img

针对上面所述的例子如下,可以看到在出口位置复制了多份finally代码,前两份用于try代码块和catch代码块,最后一份用于捕获try-catch代码块的所有异常,作为异常处理器,会在异常表中存有一个条目,但是如果在finally中又抛出异常,那么这个异常会覆盖原本(try-catch中的异常)的异常

public class Foo {
  private int tryBlock;
  private int catchBlock;
  private int finallyBlock;
  private int methodExit;
 
  public void test() {
    try {
      tryBlock = 0;
    } catch (Exception e) {
      catchBlock = 1;
    } finally {
      finallyBlock = 2;
    }
    methodExit = 3;
  }
}
 
 
$ javap -c Foo
...
  public void test();
    Code:
       0: aload_0
       1: iconst_0
       2: putfield      #20                 // Field tryBlock:I
       5: goto          30
       8: astore_1
       9: aload_0
      10: iconst_1
      11: putfield      #22                 // Field catchBlock:I
      14: aload_0
      15: iconst_2
      16: putfield      #24                 // Field finallyBlock:I
      19: goto          35
      22: astore_2
      23: aload_0
      24: iconst_2
      25: putfield      #24                 // Field finallyBlock:I
      28: aload_2
      29: athrow
      30: aload_0
      31: iconst_2
      32: putfield      #24                 // Field finallyBlock:I
      35: aload_0
      36: iconst_3
      37: putfield      #26                 // Field methodExit:I
      40: return
    Exception table:
       from    to  target type
           0     5     8   Class java/lang/Exception
           0    14    22   any
 
  ...
 

5.Java 7 的 Supressed 异常以及语法糖

Supressed 异常:允许开发人员将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息

try-with-resources:字节码层面自动使用 Supressed 异常。当然,该语法糖的主要目的并不是使用 Supressed 异常,而是精简资源打开关闭的用法。

try 关键字后声明并实例化实现了 AutoCloseable 接口的类,编译器将自动添加对应的 close() 操作。在声明多个 AutoCloseable 实例的情况下,编译生成的字节码类似于上面手工编写代码的编译结果。与手工代码相比,try-with-resources 还会使用 Supressed 异常的功能,来避免原异常“被消失”。

public class Foo implements AutoCloseable {
  private final String name;
  public Foo(String name) { this.name = name; }
 
  @Override
  public void close() {
    throw new RuntimeException(name);
  }
 
  public static void main(String[] args) {
    try (Foo foo0 = new Foo("Foo0"); // try-with-resources
         Foo foo1 = new Foo("Foo1");
         Foo foo2 = new Foo("Foo2")) {
      throw new RuntimeException("Initial");
    }
  }
}
 
// 运行结果:
Exception in thread "main" java.lang.RuntimeException: Initial
        at Foo.main(Foo.java:18)
        Suppressed: java.lang.RuntimeException: Foo2
                at Foo.close(Foo.java:13)
                at Foo.main(Foo.java:19)
        Suppressed: java.lang.RuntimeException: Foo1
                at Foo.close(Foo.java:13)
                at Foo.main(Foo.java:19)
        Suppressed: java.lang.RuntimeException: Foo0
                at Foo.close(Foo.java:13)
                at Foo.main(Foo.java:19)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哈哈哈张大侠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值