大家好,我是考100分的小小码 ,祝大家学习进步,加薪顺利呀。今天说一说Java SE基础巩固(十一):异常[亲测有效],希望您对编程的造诣更进一步.
1 什么是异常
Oracle官方对异常给出了如下定义:
Definition: An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the program’s instructions.
简单翻译就是一个异常是在程序在执行过程中出现的事件,它扰乱了正常的指令流(翻译的不好,见谅)。程序在运行的过程中会因为各种各样的因素导致程序无法继续执行,例如找不到文件、网络连接超时、解析文件失败等等,Java将这种导致程序无法正常执行的因素抽象成“异常”,并以此细分各种各样的“异常”,再结合“异常处理”构成了整个异常体系,所谓“异常处理”指的就是当程序发生异常的时候,程序能自己处理异常,并尝试恢复异常,使程序能继续正常的运行而不需要外界认为的干预。下面我将逐步深入的介绍Java异常体系中几个重要的点,包括但不限于:
- Java异常类继承体系结构
- 异常的分类
- 异常处理机制
实际上,异常和异常处理机制在计算机硬件上就有的机制,各种编程语言对其做了抽象,使得异常的检测、处理更加方便、高效。
2 Java异常类继承体系结构
上图是Java异常类结构图,从图中可以看到Throwable是整个异常类体系的父类,它有两个最主要的子类,分别是Error和Exception。
2.1 Exception
Exception即异常,是应用程序本身可以处理的,Java将其分为两大类:
- 非受检异常。可以理解为运行时异常,即运行时会发生的异常,这种类型的异常不强求程序必须捕获或者抛出,实际上也非常不建议在程序中捕获运行时异常,因为运行时异常往往指代了某种系统异常,难以处理,如果捕获了还很有可能导致程序不打印错误堆栈,使得错误难以排查。
- 受检异常。除了RuntimeException及其子类,其他Exception都是受检异常,Java编译器要求程序必须捕获(是否处理取决于需求)或者在方法签名上加上throws声明抛出该类型异常。
2.2 Error
Error即错误,因为Error往往是虚拟机相关的比较严重的错误,应用程序一般是没有能力恢复的,例如StackOverflowError(栈溢出)、OutOfMemoryError(内存溢出)等,虚拟机对这种错误的处理方法一般是直接停止相关线程(也就是说,如果应用程序是多线程并发程序,那么即使出现了Error,应用程序也很可能不会直接退出)。实际上,Java虽然没有禁止应用程序捕获Error,但我们也应该尽量不要去做这事,因为这种错误并不是程序逻辑错误,而是虚拟机发生的错误,基本是不可修复的,如果捕获了但无法处理的话,我们将无法得到错误堆栈,导致难以排查问题。
3 异常处理机制
Java中异常处理机制包含三个方面:检测异常,捕获异常以及处理异常。
3.1 try-catch块检测、捕获并处理异常
我们可以使用try关键字来指定一个范围,该范围就是异常检测的范围,然后使用catch创建一个异常处理块(在Java中,如果只有try而没有catch则无法通过通过编译)假设有如下代码:
public static void method1() {
try {
method2();
} catch (IOException e) {
System.out.println("catch io exception");
}
}
先用javac将其编译,然后使用javap -verbose XXX.class 将字节码信息翻译并打印出来,结果如下所示:
public static void method1() throws java.io.IOException;
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: invokestatic #6 // Method method2:()V
3: goto 15
6: astore_0
7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #8 // String catch io exception
12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: return
Exception table: //异常表
from to target type
0 3 6 Class java/io/IOException
LineNumberTable:
line 19: 0
line 22: 3
line 20: 6
line 21: 7
line 23: 15
StackMapTable: number_of_entries = 2
frame_type = 70 /* same_locals_1_stack_item */
stack = [ class java/io/IOException ]
frame_type = 8 /* same */
主要看看 Exception table(即异常表)标签,发现只有一行数据,有from、to、target、type等字段。from、to即构成了异常检测的范围(例子中即0~3),target代表异常处理开始的字节码索引(例子中即索引为6的字节码),type表示异常处理器所处理的异常类型(例子中是IOException)。
现在来看看 Exception table(异常表),异常表里的一行数据表示一个异常处理器,每行数据有from、to、target、type四个字段,前三个字段的值都是字节码的索引,type的值是一个符号引用,代表了异常处理器所处理的类型。每个方法都会有一个异常表,但有时候我们没有在javap的打印结果中看到,这是因为对应的方法没有异常处理器,即异常表中没有任何数据,javap只是将其省略了而已。
当有异常发生的时候,虚拟机会遍历异常表,首先检查出现异常的位置是否在异常表中某个条目的检测范围内(from-to字段),如果有这样的一个条目,将继续检查所抛出的异常是否是和type字段描述的异常匹配,如果匹配,就跳转到target值所指向的字节码进行异常处理。如果遍历完整个表也没有找到匹配的行,那么就会弹出栈,并在此时的栈帧上继续执行如上操作,最坏的情况就是虚拟机需要遍历整个方法调用栈中所有的异常表,如果最后还是没有找到匹配的异常表条目,虚拟机将直接将异常抛出,并打印异常堆栈信息。
上面的文字描述可能会有点绕,不用担心,看看下面这张逻辑流程图,结合文字描述,应该就可以理解异常处理的流程了。
其实从上面的流程描述中,还隐含了一个重要的知识点:异常传播机制。即当前方法无法处理的时候,异常会传播到调用方,继续尝试处理异常,如此往复,知道最顶层的调用方,如果还是没有合适的异常处理,那么就直接停止线程,抛出异常并打印异常堆栈。下面的代码演示了异常传播机制:
public class Main {
public static void main(String[] args) {
method1();
System.out.println("continue...");
}
public static void method1() {
try {
method2();
} catch (IOException e) {
System.out.println("catch io exception");
}
}
public static void method2() throws IOException{
method3();
}
public static void method3() throws IOException {
throw new IOException("method3");
}
}
代码中,main方法调用method1,method1调用method2,method2调用method3,在method3中抛出了一个IOEception,因为IOException是一个受检异常,所以method2要么使用try-catch构建一个异常处理器,要么使用throws关键字将异常继续往上抛,method2选择的是往上抛出异常,method1则是构建了一个异常处理器,如果该异常处理器能正确的捕获并处理异常,则不会再往上抛异常了,所以main方法不需要做特殊处理。运行一下,结果大致如下所示:
catch io exception
continue...
发现continue能正确输出,说明main线程没有被停止,即异常已经被正确处理了。现在来修改一下代码,如下所示:
public static void method1() throws IOException {
method2();
}
//其他部分代码没有变化
此时再次运行,结果大致如下:
Exception in thread "main" java.io.IOException: method3
at top.yeonon.exception.Main.method3(Main.java:26)
at top.yeonon.exception.Main.method2(Main.java:22)
at top.yeonon.exception.Main.method1(Main.java:18)
at top.yeonon.exception.Main.main(Main.java:12)
发现打印了异常堆栈,但是没有打印continue,说明main线程并虚拟机停止了,没能继续执行。这是因为在整个方法调用栈中,没有在任何一个方法的异常表找到匹配的异常表条目,即没有找到合适的异常处理器,最终没有办法了,只能停止线程并抛出异常,指望程序员能处理了。
3.2 finally
到现在为止,我一直没有提到finally,但其实finally也是一个很重要的组件。finally可以结合try-catch块,无论是否发生异常,都会执行finally里的逻辑。finally的设计初衷是为了避免程序员忘记写上一些清理操作的代码,例如关闭网络连接、文件IO连接等。
finally代码块的编译也是比较复杂的,编译器(当前版本的编译器)并不是直接使用跳转指令来实现“无论是否发生异常都会执行finally”功能的。而是采用“复制”的方法,将finally块的代码复制到try-catch块所有正常执行路径以及异常执行路径的出口位置。如下图所示(图来自极客时间上关于JVM的一门课程,在最后我会标注):
变种1和变种2的逻辑其实是一样的,只是finally块所在的位置不太一样而已。现在假设有如下代码:
public class Main {
public static void main(String[] args) {
try {
method3();
} catch (IOException e) {
System.out.println("catch io exception");
} finally {
System.out.println("execute finally block");
}
System.out.println("continue...");
}
public static void method3() throws IOException {
throw new IOException("method3");
}
}
同样编译后,使用javap来输出可阅读的字节码,如下所示:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: invokestatic #2 // Method method3:()V
3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
6: ldc #4 // String execute finally block
8: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
11: goto 45
14: astore_1
15: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
18: ldc #7 // String catch io exception
20: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
26: ldc #4 // String execute finally block
28: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: goto 45
34: astore_2
35: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
38: ldc #4 // String execute finally block
40: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
43: aload_2
44: athrow
45: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
48: ldc #8 // String continue...
50: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
53: return
Exception table:
from to target type
0 3 14 Class java/io/IOException
0 3 34 any
14 23 34 any
注意一下6、26、38号指令和其前后两条指令,发现其实就是finally块代码的内容,即输出 execute finally block字符串。而且恰好有3份,和之前所描述的已知。然后来看看异常表,重点看看后面两行,这里比较特殊的就是type字段,该字段的值是any,javap用这个来指代所有异常,即这两个条目要处理的就是所有异常。其中的第一条form-to的范围是0~3,发现是try块的的范围,第二条from-to的范围是14~23,发现其实是catch块。为什么会这样呢?
首先说try块的,如果我们自己定义的异常处理器无法和发生的异常匹配,那么就会被捕获所有异常的异常处理器捕获,并跳转到异常处理器所在的位置,例如这里的34号指令,我们发现其实34号指令就是finally块原本所在的位置,也就是说,即使发现了没有捕获到的异常,也会走到finally块的逻辑中。对于正常的情况,则是不会走到34号开始的代码块的,而是直接goto(11号指令)到45号指令处。
然后就是catch块,因为在catch块里也有可能发生异常的,所以加上这么一个异常捕获器,并且和上面的一样,跳转到34号指令处执行finally代码,如果在catch块里没有发生异常,和try块那里一样,继续执行复制过来的finally块的代码,执行完毕后直接goto(31号指令)到45号指令处,也没有执行最后的从34号开始的finally块。
这也就是为什么在整个try-catch-finally结构中,无论是否发生异常,总是会执行finally里的逻辑。
4 小结
本文简单介绍了异常的概念、分类以及异常处理机制。尤其是异常处理机制,我们深入到字节码层面去查看整个处理机制的执行流程,相信大家会对异常处理有更深刻的认识。finally也是一个很重要的组件,其作用就是在整个try-catch-finally结构中,无论是否发生异常,都会执行finally块里的逻辑,并且我也尝试深入到字节码中分析这个功能是如何实现的。
5 参考资料
深入理解java异常处理机制
极客时间: 深入拆解 Java 虚拟机第6节 :JVM是如何处理异常的?
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13907.html