异常处理的两大元素:抛出异常、捕获异常,非正常处理的两个方法。
抛出异常:显式异常与隐式异常
捕获异常则涉及到:try/catch/finally
try:异常代码块的监控
catch:捕获try可能产生的异常
finally:如果没有catch代码块,则在try之后异常;如果有cath代码块,则finally代码块则在catch代码块之后执行。
异常Throwable类,两个直接子类:Exception、Error。
除了Error与RuntimeException为非检查异常,其他异常都为检查异常,最大化地使用java编译器的功能。
在java中创建异常实例的代价特别大。当构建异常实例的时候,java虚拟机需要为异常实例构建一个栈轨迹。需要记录很多调试信息:包括栈帧所指向方法的名字、方法所在的类、第几行抛出的异常。在生成轨迹的时候,java虚拟机会忽略异常构造器、填充栈帧的java方法,直接在新建异常的位置开始。
那么我们是否可以缓存异常实例呢,在需要的时候将其抛出?上文说道,异常的栈轨迹是在新建异常的时候生成的,而不是在throw异常的时候生成。尽管缓存实例在语法角度上是OK的,但是很容易误导开发人员,定位到错误的地方,所以我们一般都是在抛出异常的地方新建异常。
java虚拟机是如何捕获异常的?
看下图的代码以及反编译的字节码
package com.yang.jvm.exception;
import java.io.*;
public class MyException {
public static void main(String[] args) {
try {
Class.forName("com.yang.jvm.invokevirtual");
File file = new File("");
InputStream in = new FileInputStream(file);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
public com.yang.jvm.exception.MyException();
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 5: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: ldc #2 // String com.yang.jvm.invokevirtual
2: invokestatic #3 // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
5: pop
6: new #4 // class java/io/File
9: dup
10: ldc #5 // String
12: invokespecial #6 // Method java/io/File."<init>":(Ljava/lang/String;)V
15: astore_1
16: new #7 // class java/io/FileInputStream
19: dup
20: aload_1
21: invokespecial #8 // Method java/io/FileInputStream."<init>":(Ljava/io/File;)V
24: astore_2
25: goto 41
28: astore_1
29: aload_1
30: invokevirtual #10 // Method java/lang/ClassNotFoundException.printStackTrace:()V
33: goto 41
36: astore_1
37: aload_1
38: invokevirtual #12 // Method java/io/FileNotFoundException.printStackTrace:()V
41: return
Exception table:
from to target type
0 25 28 Class java/lang/ClassNotFoundException
0 25 36 Class java/io/FileNotFoundException
每个方法都会对应一个异常表,异常表里面会包含多个异常条目。每个异常条目是由:from指针、to指针、target指针以及target指向的类型构成。每个指针代表队的相应的字节码的索引。由from——to(不包括to)指针监控的范围即try的代码块的范围。从target指针对应的索引开始即异常处理器的开始位置。
从上图中我们可以看出。
from指针0,to指针25,对应的try代码块;
由于try代码块里面有两处捕获异常,一个是classNotfound,一个是fileNotFound异常,所以异常表里面有两个异常条目。
索引28和36对应的字节码分别是classNotfound异常处理器和fileNotFound异常处理器的开始位置,前者由33goto指令跳转至return,后者直接在41处return。
当java触发异常的时候,jvm会从异常条目表从上至下进行检查,首先判断抛出异常的字节码的索引是否在其监控范围之内,若匹配的话,则会检查抛出的异常类型与该异常条目指向的异常类型是否匹配,若匹配的话,jvm会将控制流转向该条目的target指针指向的索引的字节码。
以上为try与catch的字节码的执行过程。
但是如果有finally的话,则比较复杂。finally会被复制两份,分别放在try与catch正常执行的出口。第三份则用来监控,try触发异常,并未被catch捕获、catch抛出异常的监控。
package com.yang.jvm.exception;
public class MyException {
public static void main(String [] args){
try {
Class.forName("com.yang.test");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally{
System.out.println("ending......");
}
}
}
以下为字节码:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String com.yang.test
2: invokestatic #3 // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
5: pop
6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #5 // String ending......
11: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: goto 44
17: astore_1
18: aload_1
19: invokevirtual #8 // Method java/lang/ClassNotFoundException.printStackTrace:()V
22: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
25: ldc #5 // String ending......
27: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: goto 44
33: astore_2
34: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
37: ldc #5 // String ending......
39: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
42: aload_2
43: athrow
44: return
Exception table:
from to target type
0 6 17 Class java/lang/ClassNotFoundException
0 6 33 any
17 22 33 any
从以上字节码中我们可以得到:
try正常执行后有一段finally代码块;
catch异常处理器后面有一段finally代码块;
finally自身有一段代码块。
异常表中的监控范围分别为:try中的classnotfoundtion、其他异常any;catch中可能抛出的异常。
java7中的Supressed异常与语法糖
Supressed异常可以将一个异常附于另一个异常上,所以finally中抛出异常的时候可以输出多个异常的信息;
不过java语言层面没有对异常的引用,所以使用起来比较复杂。
但是try-with-resources可以很好地在字节码层次自动使用了Supressed异常,不过主要目的是为了精简的关闭资源流因为只要我们的类实现了autocloseable接口,他们会重写其中的close方法,这样的话,我们就不需要关注流是否关闭了。
还有另外一个中语法糖:try中的所有检查类型可以全部放到一个catch代码块中,以“|”分割即可。