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:()V3: goto 116: astore_17: aload_18: invokevirtual java.lang.Exception.printStackTrace11: returnException table:from to target type0 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_01: iconst_02: putfield #20 // Field tryBlock:I5: goto 308: astore_19: aload_010: iconst_111: putfield #22 // Field catchBlock:I14: aload_015: iconst_216: putfield #24 // Field finallyBlock:I19: goto 3522: astore_223: aload_024: iconst_225: putfield #24 // Field finallyBlock:I28: aload_229: athrow30: aload_031: iconst_232: putfield #24 // Field finallyBlock:I35: aload_036: iconst_337: putfield #26 // Field methodExit:I40: returnException table:from to target type0 5 8 Class java/lang/Exception0 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: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Ljvm/Exceptions;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=0, locals=1, args_size=10: returnLineNumberTable:line 10: 0LocalVariableTable:Start Length Slot Name Signature0 1 0 args [Ljava/lang/String;MethodParameters:Name Flagsargspublic static void throwEx() throws java.lang.Exception;descriptor: ()Vflags: ACC_PUBLIC, ACC_STATICCode:stack=0, locals=0, args_size=00: returnLineNumberTable:line 14: 0Exceptions:throws java.lang.Exceptionpublic static void throwRtEx() throws java.lang.RuntimeException;descriptor: ()Vflags: ACC_PUBLIC, ACC_STATICCode:stack=0, locals=0, args_size=00: returnLineNumberTable:line 18: 0Exceptions:throws java.lang.RuntimeExceptionpublic static void throwRtExInner();descriptor: ()Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=00: new #2 // class java/lang/RuntimeException3: dup4: invokespecial #3 // Method java/lang/RuntimeException."<init>":()V7: athrow8: astore_09: aload_010: invokevirtual #4 // Method java/lang/RuntimeException.printStackTrace:()V13: returnException table:from to target type0 8 8 Class java/lang/RuntimeExceptionLineNumberTable:line 23: 0line 24: 8line 25: 9line 27: 13LocalVariableTable:Start Length Slot Name Signature9 4 0 e Ljava/lang/RuntimeException;StackMapTable: number_of_entries = 1frame_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语句,在重新抛出前就返回了。
