Java SE基础巩固(十一):异常

1 什么是异常

Oracle官方对异常给出了以下定义:java

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异常体系中几个重要的点,包括但不限于:shell

  • Java异常类继承体系结构
  • 异常的分类
  • 异常处理机制

实际上,异常和异常处理机制在计算机硬件上就有的机制,各类编程语言对其作了抽象,使得异常的检测、处理更加方便、高效。编程

2 Java异常类继承体系结构

iNBBoq.png

上图是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则没法经过经过编译)假设有以下代码:ui

public static void method1() {
    try {
        method2();
    } catch (IOException e) {
        System.out.println("catch io exception");
    }
}
复制代码

先用javac将其编译,而后使用javap -verbose XXX.class 将字节码信息翻译并打印出来,结果以下所示:spa

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值所指向的字节码进行异常处理。若是遍历完整个表也没有找到匹配的行,那么就会弹出栈,并在此时的栈帧上继续执行如上操做,最坏的状况就是虚拟机须要遍历整个方法调用栈中全部的异常表,若是最后仍是没有找到匹配的异常表条目,虚拟机将直接将异常抛出,并打印异常堆栈信息。

上面的文字描述可能会有点绕,不用担忧,看看下面这张逻辑流程图,结合文字描述,应该就能够理解异常处理的流程了。

iUKU2V.png

其实从上面的流程描述中,还隐含了一个重要的知识点:异常传播机制。即当前方法没法处理的时候,异常会传播到调用方,继续尝试处理异常,如此往复,知道最顶层的调用方,若是仍是没有合适的异常处理,那么就直接中止线程,抛出异常并打印异常堆栈。下面的代码演示了异常传播机制:

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的一门课程,在最后我会标注):

iUQ1ts.png

变种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
复制代码

注意一下六、2六、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是如何处理异常的?

相关文章
相关标签/搜索