1.异常抛出
显式抛出:代码中使用throw关键字
隐式抛出:JVM中碰到程序无法继续执行的异常状态,自动抛出异常。
2.异常捕获
其涉及三种代码块
- try代码块:标记需要跟踪的代码
- catch代码块:跟在 try 代码块之后,用来捕获在 try 代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型之外,catch 代码块还定义了针对该异常类型的异常处理器。在 Java 中,try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会从上至下匹配异常处理器。因此,前面的 catch 代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错
- finally代码块:在 try 代码块和 catch 代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码,例如关闭已打开的系统资源
3.异常
所有异常都是 Throwable 类或者其子类的实例。当程序触发 Error 时,它的执行状态已经无法恢复,需要中止线程甚至是中止虚拟机。第二子类则是 Exception,涵盖程序可能需要捕获并且处理的异常。

非检查异常: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 编译器会重新抛出所捕获的异常。

针对上面所述的例子如下,可以看到在出口位置复制了多份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)