Java异常层次
Error 与RuntimeException 属于非检查异常,不用在方法显示声明throw *Exception
其他异常则属于检查异常(checked exception)。在 Java 语法中,所有的检查异常都需要程序显式地捕获,或者在方法声明中用 throws 关键字标注。
通常情况下,程序中自定义的异常应为检查异常,以便最大化利用 Java 编译器的编译时检查。(注这个不一定,内部业务方法最好不这样,避免所有关联的方法都加上异常签名)
异常实例的构造十分昂贵。这是由于在构造异常实例时,Java 虚拟机便需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。
但是一般不缓存异常实例,因为需要知悉原始现场信息,需要原始的栈信息。
如何捕获异常
在编译生成的字节码中,每个方法都附带一个异常表。
异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。
其中,from 指针和 to 指针标示了该异常处理器所监控的范围,例如 try 代码块所覆盖的范围。target 指针则指向异常处理器的起始位置,例如 catch 代码块的起始位置。
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 // 异常表条目
举个例子,在上图的 main 方法中,我定义了一段 try-catch 代码。其中,catch 代码块所捕获的异常类型为 Exception。编译过后,该方法的异常表拥有一个条目。其 from 指针和 to 指针分别为 0 和 3,代表它的监控范围从索引为 0 的字节码开始,到索引为 3 的字节码结束(不包括 3)。该条目的 target 指针是 6,代表这个异常处理器从索引为 6 的字节码开始。条目的最后一列,代表该异常处理器所捕获的异常类型正是 Exception。
当程序触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码。如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者(caller)中重复上述操作。在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。
处理finally
finally 代码块的编译比较复杂。当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。
针对异常执行路径,Java 编译器会生成一个或多个异常表条目,监控整个 try-catch 代码块,并且捕获所有种类的异常(在 javap 中以 any 指代)。这些异常表条目的 target 指针将指向另一份复制的 finally 代码块。并且,在这个 finally 代码块的最后,Java 编译器会重新抛出所捕获的异常。
示例
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
...
可以看到,编译结果包含三份 finally 代码块。其中,前两份分别位于 try 代码块和 catch 代码块的正常执行路径出口。最后一份则作为异常处理器,监控 try 代码块以及 catch 代码块。它将捕获 try 代码块触发的、未被 catch 代码块捕获的异常,以及 catch 代码块触发的异常。
这里有一个小问题,如果 catch 代码块捕获了异常,并且触发了另一个异常,那么 finally 捕获并且重抛的异常是哪个呢?答案是后者。也就是说原本的异常便会被忽略掉,这对于代码调试来说十分不利。
异常表
是编译期识别的try-catch的异常类型而非所有异常或RuntimeException.
方法内无try-catch,无异常表。
public class Exceptions {
public static void main(String[] args) {
}
public static void throwEx() throws Exception {
}
public static void throwRtEx() throws RuntimeException {
}
public static void throwRtExInner() {
try {
throw new RuntimeException();
} catch (RuntimeException e) {
e.printStackTrace();
}
}
}
javap -v Exceptions.class
{
public jvm.Exceptions();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljvm/Exceptions;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
MethodParameters:
Name Flags
args
public static void throwEx() throws java.lang.Exception;
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 14: 0
Exceptions:
throws java.lang.Exception
public static void throwRtEx() throws java.lang.RuntimeException;
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 18: 0
Exceptions:
throws java.lang.RuntimeException
public static void throwRtExInner();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: new #2 // class java/lang/RuntimeException
3: dup
4: invokespecial #3 // Method java/lang/RuntimeException."<init>":()V
7: athrow
8: astore_0
9: aload_0
10: invokevirtual #4 // Method java/lang/RuntimeException.printStackTrace:()V
13: return
Exception table:
from to target type
0 8 8 Class java/lang/RuntimeException
LineNumberTable:
line 23: 0
line 24: 8
line 25: 9
line 27: 13
LocalVariableTable:
Start Length Slot Name Signature
9 4 0 e Ljava/lang/RuntimeException;
StackMapTable: number_of_entries = 1
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/RuntimeException ]
}
finally 带return
public static int throwRtExInner() {
try {
throw new RuntimeException();
} catch (RuntimeException e) {
throw new RuntimeException();
} finally {
return 100;
}
}
catch里抛的异常会被finally捕获了,再执行完finally代码后重新抛出该异常。由于finally代码块有个return语句,在重新抛出前就返回了。