Java异常层次

image.png
Error 与RuntimeException 属于非检查异常,不用在方法显示声明throw *Exception

其他异常则属于检查异常(checked exception)。在 Java 语法中,所有的检查异常都需要程序显式地捕获,或者在方法声明中用 throws 关键字标注。
通常情况下,程序中自定义的异常应为检查异常,以便最大化利用 Java 编译器的编译时检查。(注这个不一定,内部业务方法最好不这样,避免所有关联的方法都加上异常签名)

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

但是一般不缓存异常实例,因为需要知悉原始现场信息,需要原始的栈信息。

如何捕获异常

在编译生成的字节码中,每个方法都附带一个异常表。
异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。
其中,from 指针和 to 指针标示了该异常处理器所监控的范围,例如 try 代码块所覆盖的范围。target 指针则指向异常处理器的起始位置,例如 catch 代码块的起始位置。

  1. public static void main(String[] args) {
  2. try {
  3. mayThrowException();
  4. } catch (Exception e) {
  5. e.printStackTrace();
  6. }
  7. }
  8. // 对应的Java字节码
  9. public static void main(java.lang.String[]);
  10. Code:
  11. 0: invokestatic mayThrowException:()V
  12. 3: goto 11
  13. 6: astore_1
  14. 7: aload_1
  15. 8: invokevirtual java.lang.Exception.printStackTrace
  16. 11: return
  17. Exception table:
  18. from to target type
  19. 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 代码块所有正常执行路径以及异常执行路径的出口中。
image.png
针对异常执行路径,Java 编译器会生成一个或多个异常表条目,监控整个 try-catch 代码块,并且捕获所有种类的异常(在 javap 中以 any 指代)。这些异常表条目的 target 指针将指向另一份复制的 finally 代码块。并且,在这个 finally 代码块的最后,Java 编译器会重新抛出所捕获的异常。

示例

  1. public class Foo {
  2. private int tryBlock;
  3. private int catchBlock;
  4. private int finallyBlock;
  5. private int methodExit;
  6. public void test() {
  7. try {
  8. tryBlock = 0;
  9. } catch (Exception e) {
  10. catchBlock = 1;
  11. } finally {
  12. finallyBlock = 2;
  13. }
  14. methodExit = 3;
  15. }
  16. }
  17. $ javap -c Foo
  18. ...
  19. public void test();
  20. Code:
  21. 0: aload_0
  22. 1: iconst_0
  23. 2: putfield #20 // Field tryBlock:I
  24. 5: goto 30
  25. 8: astore_1
  26. 9: aload_0
  27. 10: iconst_1
  28. 11: putfield #22 // Field catchBlock:I
  29. 14: aload_0
  30. 15: iconst_2
  31. 16: putfield #24 // Field finallyBlock:I
  32. 19: goto 35
  33. 22: astore_2
  34. 23: aload_0
  35. 24: iconst_2
  36. 25: putfield #24 // Field finallyBlock:I
  37. 28: aload_2
  38. 29: athrow
  39. 30: aload_0
  40. 31: iconst_2
  41. 32: putfield #24 // Field finallyBlock:I
  42. 35: aload_0
  43. 36: iconst_3
  44. 37: putfield #26 // Field methodExit:I
  45. 40: return
  46. Exception table:
  47. from to target type
  48. 0 5 8 Class java/lang/Exception
  49. 0 14 22 any
  50. ...

可以看到,编译结果包含三份 finally 代码块。其中,前两份分别位于 try 代码块和 catch 代码块的正常执行路径出口。最后一份则作为异常处理器,监控 try 代码块以及 catch 代码块。它将捕获 try 代码块触发的、未被 catch 代码块捕获的异常,以及 catch 代码块触发的异常。

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

异常表

是编译期识别的try-catch的异常类型而非所有异常或RuntimeException.
方法内无try-catch,无异常表。

  1. public class Exceptions {
  2. public static void main(String[] args) {
  3. }
  4. public static void throwEx() throws Exception {
  5. }
  6. public static void throwRtEx() throws RuntimeException {
  7. }
  8. public static void throwRtExInner() {
  9. try {
  10. throw new RuntimeException();
  11. } catch (RuntimeException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }

javap -v Exceptions.class

  1. {
  2. public jvm.Exceptions();
  3. descriptor: ()V
  4. flags: ACC_PUBLIC
  5. Code:
  6. stack=1, locals=1, args_size=1
  7. 0: aload_0
  8. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  9. 4: return
  10. LineNumberTable:
  11. line 6: 0
  12. LocalVariableTable:
  13. Start Length Slot Name Signature
  14. 0 5 0 this Ljvm/Exceptions;
  15. public static void main(java.lang.String[]);
  16. descriptor: ([Ljava/lang/String;)V
  17. flags: ACC_PUBLIC, ACC_STATIC
  18. Code:
  19. stack=0, locals=1, args_size=1
  20. 0: return
  21. LineNumberTable:
  22. line 10: 0
  23. LocalVariableTable:
  24. Start Length Slot Name Signature
  25. 0 1 0 args [Ljava/lang/String;
  26. MethodParameters:
  27. Name Flags
  28. args
  29. public static void throwEx() throws java.lang.Exception;
  30. descriptor: ()V
  31. flags: ACC_PUBLIC, ACC_STATIC
  32. Code:
  33. stack=0, locals=0, args_size=0
  34. 0: return
  35. LineNumberTable:
  36. line 14: 0
  37. Exceptions:
  38. throws java.lang.Exception
  39. public static void throwRtEx() throws java.lang.RuntimeException;
  40. descriptor: ()V
  41. flags: ACC_PUBLIC, ACC_STATIC
  42. Code:
  43. stack=0, locals=0, args_size=0
  44. 0: return
  45. LineNumberTable:
  46. line 18: 0
  47. Exceptions:
  48. throws java.lang.RuntimeException
  49. public static void throwRtExInner();
  50. descriptor: ()V
  51. flags: ACC_PUBLIC, ACC_STATIC
  52. Code:
  53. stack=2, locals=1, args_size=0
  54. 0: new #2 // class java/lang/RuntimeException
  55. 3: dup
  56. 4: invokespecial #3 // Method java/lang/RuntimeException."<init>":()V
  57. 7: athrow
  58. 8: astore_0
  59. 9: aload_0
  60. 10: invokevirtual #4 // Method java/lang/RuntimeException.printStackTrace:()V
  61. 13: return
  62. Exception table:
  63. from to target type
  64. 0 8 8 Class java/lang/RuntimeException
  65. LineNumberTable:
  66. line 23: 0
  67. line 24: 8
  68. line 25: 9
  69. line 27: 13
  70. LocalVariableTable:
  71. Start Length Slot Name Signature
  72. 9 4 0 e Ljava/lang/RuntimeException;
  73. StackMapTable: number_of_entries = 1
  74. frame_type = 72 /* same_locals_1_stack_item */
  75. stack = [ class java/lang/RuntimeException ]
  76. }

finally 带return

  1. public static int throwRtExInner() {
  2. try {
  3. throw new RuntimeException();
  4. } catch (RuntimeException e) {
  5. throw new RuntimeException();
  6. } finally {
  7. return 100;
  8. }
  9. }

catch里抛的异常会被finally捕获了,再执行完finally代码后重新抛出该异常。由于finally代码块有个return语句,在重新抛出前就返回了。