一、异常的概念
所有异常都是Throwable类或者其子类的实例。
Throwable有两大直接子类,第一个是ERROR,涵盖程序不应捕获的异常,当程序触发Error时,其执行状态已经无法恢复,需要中止线程甚至是终止虚拟机;第二个是Exception,涵盖程序可能需要捕获并且处理的异常。
捕获异常涉及三种代码块
(1)try代码块:用来标记需要进行异常监控的代码
(2)catch代码块:跟在try代码块之后,用来捕获在try代码块中触发的某种特定类型的异常。Java虚拟机会从上而下匹配异常处理器,因此,前面的catch代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错
(3)finally代码块:跟在try代码块和catch代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码,例如关闭已经打开的系统资源。
二、Java虚拟机是如何捕获异常的
在编译生成的字节码中,每个方法都附带一个异常表。异常表中的每个条目代表一个异常处理器,由from指针、to指针、target指针及其所捕获的异常类型构成。这些指针的值是字节码索引,用以定位字节码。
java
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 为0 to 为 3代表它的监控范围从索引为0的字节码开始,到索引为3的字节码结束(不包括3)
// target为6代表该索引处理器从索引为6的字节码开始。
//type 代表异常处理器捕获的异常类型是Exception
当程序触发异常时,Java虚拟机会遍历异常表中的所有条目。当触发异常的字节码的索引值在异常条目的监控范围内,JVM会判断所抛出的异常和该条目想要捕获的异常是否匹配。若匹配,JVM会将控制流转移至该条目target指针指向的字节码。
若遍历完所有异常表条目,JVM仍未匹配到异常处理器,则会弹出对应的Java栈帧,并且在调用者中重复上述操作。
思考:
1:使用异常捕获的代码为什么比较耗费性能?
因为构造异常的实例比较耗性能。这从代码层面很难理解,不过站在JVM的角度来看就简单了,因为JVM在构造异常实例时需要生成该异常的栈轨迹。这个操作会逐一访问当前线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息。
虽然具体不清楚JVM的实现细节,但是看描述这件事情也是比较费时费力的。
2:finally是怎么实现无论异常与否都能被执行的?
这个事情是由编译器来实现的,现在的做法是这样的,编译器在编译Java代码时,会复制finally代码块的内容,然后分别放在try-catch代码块所有的正常执行路径及异常执行路径的出口中。