https://time.geekbang.org/column/article/12134
异常处理方式
1.抛出异常
显式抛出异常主体是应用程序,在程序中使用"throw"关键字手动抛出异常
隐式抛出异常指Java虚拟机在执行过程中,碰到无法继续执行的异常状态,自动抛出异常,常见的有数组越界异常
2.捕获异常
- tyy-catch-finally
FileInputStream in0 = null;
FileInputStream in1 = null;
FileInputStream in2 = null;
...
try {
in0 = new FileInputStream(new File("in0.txt"));
...
try {
in1 = new FileInputStream(new File("in1.txt"));
...
try {
in2 = new FileInputStream(new File("in2.txt"));
...
} finally {
if (in2 != null) in2.close();
}
} finally {
if (in1 != null) in1.close();
}
} finally {
if (in0 != null) in0.close();
}
- try-with-resources
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");
}
}
异常基本概念
Java语言规范中,所有的异常都是Throwable类或者它的子类的实例,Throwable有两个子类Error与Exception
- Error:通俗理解,当出现Error,程序已经没救了,需要终止线程或者虚拟机
- Exception:涵盖程序可能需要捕获并处理的异常
Throwable
-Error
-Exception
-RuntimeException
-OtherException
Exception有一个特殊的子类是RuntimeException,它与Error属于Java里的非检查异常(unchecked exception),其他的exception都属于检查时异常(checked exception).在Java语法中,所有的检查时异常都需要显式捕获或者在方法声明时用"throw"关键字标注. 通常情况下,程序的自定义异常都应该定义为检查时异常,方便利用Java编译器的编译时检查.
异常实例的构造十分昂贵.这是由于在构造异常实例时,Java 虚拟机便需要生成该异常的栈轨迹(stack trace),该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名,文件名,以及在代码中的第几行触发该异常.
在生成栈轨迹时,Java 虚拟机不记录异常构造器以及填充栈帧的 Java 方法(Throwable.fillInStackTrace),直接从新建异常位置开始算起.此外,Java 虚拟机还会忽略标记为不可见的 Java 方法栈帧.
Java虚拟机是怎么捕捉异常的
在编译生成的字节码中,每个方法都附带一个异常表. 异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成. 这些指针的值是字节码索引(bytecode index,bci),用以定位字节码.
其中from和to指针标示异常处理器的监控范围(try代码块),target指向异常处理器的起始位置
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 // 异常表条目
在上述例子中,经过编译过,该方法的异常表拥有一个条目.其 from 指针和 to 指针分别为 0 和 3,代表它的监控范围从索引为 0 的字节码开始,到索引为 3 的字节码结束(不包括 3). 该条目的 target 指针是 6,代表这个异常处理器从索引为 6 的字节码开始. 条目的最后一列,代表该异常处理器所捕获的异常类型正是 Exception.
当程序触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目. 当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配. 如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码.如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者(caller)中重复上述操作.在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表.
finally 代码块的编译比较复杂.当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中.
针对异常执行路径,Java 编译器会生成一个或多个异常表条目,监控整个 try-catch 代码块,并且捕获所有种类的异常(在 javap 中以 any 指代).这些异常表条目的 target 指针将指向另一份复制的 finally 代码块.并且,在这个 finally 代码块的最后,Java 编译器会重新抛出所捕获的异常.