---
异常
---

#### 目录

1. 思维导图
2. 概述
3. 异常处理
4. 实现原理
5. Supressed 异常以及语法糖
6. 参考

#### 思维导图

![](https://i.loli.net/2019/01/29/5c5064b27d2b7.png)

#### 概述

在 Java 语言规范中，所有异常都是 Throwable 类或者其子类的实例。Throwable 有两大直接子类。第一个是 Error，涵盖程序不应捕获的异常，当程序触发 Error 时，它的执行状态已经无法恢复，需要中止线程甚至是中止虚拟机。第二个是 Exception，涵盖程序可能需要捕获并且处理的异常。Exception 又可以分为受检异常和非受检异常，受检异常是指需要显式捕获或声明的异常，非受检异常则是指 RuntimeException。

![](https://i.loli.net/2019/01/27/5c4da2c221a34.png)

#### 异常处理

异常处理的两大组成要素是抛出异常和捕获异常，这两大要素共同实现程序控制流的非正常转移。

##### 抛出异常

抛出异常可以分为显式和隐式两种，显式抛异常的主体是应用程序，它指的是程序中使用 throw 关键字，手动将异常实例抛出。

隐式抛异常的主体则是 Java 虚拟机，它指的是 Java 虚拟机在执行过程中，碰到无法继续执行的异常状态，自动抛出异常。通常都是运行时异常。

##### 捕获异常

捕获异常设计 try-catch-final 语句块。

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

在某些不幸的情况下，catch 代码块也触发了异常，那么 finally 代码块同样会运行，并会抛出 catch 代码块触发的异常。在某些极端不幸的情况下，finally 代码块也触发了异常，那么只好中断当前 finally 代码块的执行，并往外抛异常。

#### 实现原理

异常实例的构造十分昂贵。这是由于在构造异常实例时，Java 虚拟机便需要生成该异常的栈轨迹（stack trace）。该操作会逐一访问当前线程的 Java 栈帧，并且记录下各种调试信息，包括栈帧所指向方法的名字，方法所在的类名、文件名，以及在代码中的第几行触发该异常。

当然，在生成栈轨迹时，Java 虚拟机会忽略掉异常处理器以及填充栈帧的 Java 方法（Throwable.fillInStackTrace），直接从新建异常位置开始算起。

这里遍历堆栈的时候，并不用担心 inline 代码消除等编译优化对错误堆栈和代码对不上的情况，所以也就不需要先去优化之后在遍历。即时编译器生成的代码会保存原始的栈信息，以便去优化时能够复原，fillStackTrace 也会读取这些信息的，所以不需要去优化之后在 fill。

那么 Java 虚拟机是如何捕获异常的？

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

其中，from 指针和 to 指针标示了该异常处理器所指控的范围，即 try 代码块所覆盖的范围。target 指针则指向异常处理器的初始位置，例如 catch 代码块的起始位置。

当程序触发异常时，Java 虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内，Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码。

如果遍历完所有异常表条目，Java 虚拟机仍未匹配到异常处理器，那么它会弹出当前方法对应的 Java 栈帧，并且在调用者中重复上述操作。在最坏的情况下，Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。

finally 代码块的编译比较复杂，当前版本 Java 编译器的做法，是复制 finally 代码块的内容，分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。

![](https://i.loli.net/2019/01/29/5c5059fb5251e.png)

针对异常执行路径，Java 编译器会生成一个或多个异常表条目，监控整个 try-catch 代码块，并且捕获所有种类的异常。这些异常表条目的 target 指针将指向另一份复制的 finally 代码块。并且，在这个 finally 代码块的最后，Java 编译器会重新抛出所捕获的异常。

如果 catch 代码块捕获了异常，并且触发了另外一个异常，那么 finally 捕获并重抛的异常是哪个呢？答案是后者，也就是说原本的异常便会被忽略掉，这对于代码调试来说十分不利。

#### Supressed 异常以及语法糖

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

然鹅，Java 层面的 finally 代码块缺少指向所捕获异常的引用，所以这个新特性使用起来非常繁琐。

为此，Java 7 专门构造了一个名为 try-with-resources 的语法糖，在字节码层面自动使用 Supressed 异常。当然，该语法糖的主要目的并不是使用 Supressed 异常，而是精简资源打开关闭。

在 Java 7 之前，对于打开的资源，我们需要定义一个 finally 代码块，来确保该资源在正常或者异常执行状态情况下都能关闭。

资源的关闭操作本身容易触发异常。因此，如果同时打开多个资源，那么每一个资源都要对应一个独立的 try-finally 代码块，以保证每个资源都能关闭。这样一来，代码将会变得十分繁琐。

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

```java
public class Foo implements AutoCloseable {
    private String name;

    public Foo(String name) {
        this.name = name;
    }

    @Override
    public void close() throws Exception {
        throw new RuntimeException(name);
    }

    public static void main(String[] args) throws Exception {
        try (
                Foo foo0 = new Foo("Foo0");
                Foo foo1 = new Foo("Foo1");
                Foo foo2 = new Foo("Foo2")
        ) {
            throw new RuntimeException("");
        }
    }
}
//输出：
Exception in thread "main" java.lang.RuntimeException: 
	at annotation.Foo.main(Foo.java:21)
	Suppressed: java.lang.RuntimeException: Foo2
		at annotation.Foo.close(Foo.java:12)
		at annotation.Foo.$closeResource(Foo.java:22)
		at annotation.Foo.main(Foo.java:22)
	Suppressed: java.lang.RuntimeException: Foo1
		at annotation.Foo.close(Foo.java:12)
		at annotation.Foo.$closeResource(Foo.java:22)
		at annotation.Foo.main(Foo.java:22)
	Suppressed: java.lang.RuntimeException: Foo0
		at annotation.Foo.close(Foo.java:12)
		at annotation.Foo.$closeResource(Foo.java:22)
		at annotation.Foo.main(Foo.java:22)

Process finished with exit code 1
```

除了 try-with-resources 语法糖之外，Java 7 还支持在同一个 catch 代码块中捕获多种异常。实际实现非常简单，生成多个异常表条目即可。

```java
// 在同一 catch 代码块中捕获多种异常
try {
  ...
} catch (SomeException | OtherException e) {
  ...
}
```

#### 参考

[JVM是如何处理异常的？](https://time.geekbang.org/column/article/12134)

[try-with-resources 的理解](https://www.jianshu.com/p/08d03356ff8a)