阅读更多关于 Angular、TypeScript、Node.js/Java 、Spring 等技术文章,欢迎访问个人我的博客 —— 全栈修仙之路
本文的主要内容分为 Java 异常的定义、Java 异常的处理、JVM 基础知识(异常表、JVM 指令分类和操做数栈)及深刻剖析 try-catch-finally 四部分(图解形式)。在深刻剖析 try-catch-finally 部分会以字节码的角度分析为何 finally 语句必定会执行。第三和第四部分理解起来可能会有些难度,不感兴趣的小伙伴可直接跳过。html
异常是指在程序执行期间发生的事件,这些事件中断了正常的指令流(例如,除零,数组越界访问等)。在 Java 中,异常是一个对象,该对象包装了方法内发生的错误事件,并包含如下信息:java
此外,异常对象也能够被抛出或捕获。Java 程序在执行过程当中发生的异常可分为两大类:Error 和 Exception,它们都继承于 Throwable 类。git
An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.
Error 是 Throwable 类的子类,它表示合理的应用程序不该该尝试捕获的严重问题。大多数这样的错误都是异常状况。让咱们来看一下 Error 类的一些子类,并阅读 JavaDoc 上与它们有关的注释:github
这些错误是不可查的,由于它们在应用程序的控制和处理能力以外,并且绝大多数是程序运行时不容许出现的情况。面试
The class Exception and its subclasses are a form of Throwable that indicates conditions that a reasonable application might want to catch.
Exception 和它的子类是可抛出异常的一种形式,表示合理的应用程序可能想要捕获的异常。在 Exception 分支中有一个重要的子类 RuntimeException(运行时异常),该类型的异常会自动为你所编写的程序建立ArrayIndexOutOfBoundsException(数组下标越界异常)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、IllegalArgumentException(非法参数异常)等异常,这些异常是不检查异常,程序中能够选择捕获处理,也能够不处理。这些异常通常是由程序逻辑错误引发的,程序应该从逻辑角度尽量避免这类异常的发生。编程
Error 一般是灾难性的致命的错误,是程序没法控制和处理的,当出现这些异常时,Java 虚拟机(JVM)通常会选择终止线程;Exception 一般状况下是能够被程序处理的,而且在程序中应该尽量的去处理这些异常。数组
Unchecked Exception(不受检查的异常):多是常常出现的编程错误,好比 NullPointerException(空指针异常)或 IllegalArgumentException(非法参数异常)。应用程序有时能够处理它或今后 Throwable 类型的异常中恢复。或者至少在 Thread 的 run 方法中捕获它,记录日志并继续运行。oracle
Checked Exception(检查异常):在正确的程序运行过程当中,很容易出现的、情理可容的异常情况,在必定程度上这种异常的发生是能够预测的,而且一旦发生该种异常,就必须采起某种方式进行处理。app
除了 RuntimeException 及其子类之外,其余的 Exception 类及其子类都属于检查异常,当程序中可能出现这类异常,要么使用 try-catch 语句进行捕获,要么用 throws 子句抛出,不然编译没法经过。jvm
不受检查异常和检查异常的区别是:不受检查异常为编译器不要求强制处理的异常,检查异常则是编译器要求必须处置的异常。
在 Java 中有 5 个关键字用于异常处理:try,catch,finally,throws 和 throw(注意 throw 和 throws 之间存在一些区别)。
Java 的异常处理包含三部分:声明异常、抛出异常和捕获异常。
一个 Java 方法必须在其签名中声明可能经过 throws 关键字在其方法体中 “抛出” 的已检查异常的类型。
举个例子,假设 methodD()
的定义以下:
public void methodD() throws XxxException, YyyException { // 方法体抛出XxxException和YyyException异常 }
methodD 的方法签名表示运行 methodD 方法时,可能遇到两种 checked exceptions:XxxException 和 YyyException。换句话说,在 methodD 方法中若出现某些不正常的状况可能会触发 XxxException 或 YyyException 异常。
请注意,咱们不须要声明属于 Error,RuntimeException 及其子类的异常。这些异常称为不受检查的异常,由于编译器未检查它们。
当 Java 操做遇到异常状况时,包含错误语句的方法应建立一个适当的 Exception 对象,并经过 throw XxxException
语句将其抛到 Java 运行时。例如:
public void methodD() throws XxxException, YyyException { // 方法签名 // 方法体 ... ... // 出现XxxException异常 if ( ... ) throw new XxxException(...); // 构造一个XxxException对象并抛给JVM ... // 出现YyyException异常 if ( ... ) throw new YyyException(...); // 构造一个YyyException对象并抛给JVM ... }
请注意,在方法签名中声明异常的关键字为throws
,在方法体内抛出异常对象的关键字为throw
。
当方法抛出异常时,JVM 在调用堆栈中向后搜索匹配的异常处理程序。每一个异常处理程序均可以处理一类特殊的异常。异常处理程序能够处理特定的类,也能够处理其子类。若是在调用堆栈中未找到异常处理程序,则程序终止。
好比,假设 methodD 方法在方法签名上声明了可能抛出的 XxxException 和 YyyException 异常,具体以下:
public void methodD() throws XxxException, YyyException { ...... }
要在程序中使用 methodD 方法,好比在 methodC 方法中,你能够这样作:
try-catch
或 try-catch-finally
中,以下所示。每一个 catch 块能够包含一种类型的异常对应的异常处理程序。public void methodC() { // 未声明异常 ...... try { ...... // 调用声明XxxException和YyyException异常的methodD方法 methodD(); ...... } catch (XxxException ex) { // 处理XxxException异常 ...... } catch (YyyException ex} { // 处理YyyException异常 ...... } finally { // 可选 // 这些代码总会执行,用于执行清理操做 ...... } ...... }
public void methodC() throws XxxException, YyyException { // 让更高层级的方法来处理 ... // 调用声明XxxException和YyyException异常的methodD方法 methodD(); // 无需使用try-catch ... }
在这种状况下,若是 methodD 方法抛出 XxxException 或 YyyException,则 JVM 将终止 methodD 方法和methodC 方法并将异常对象沿调用堆栈传递给 methodC 方法的调用者。
try-catch-finally 的语法以下:
try { // 主要逻辑,使用了可能抛出异常的方法 ...... } catch (Exception1 ex) { // 处理Exception1异常 ...... } catch (Exception2 ex) { // 处理Exception2异常 ...... } finally { // finally是可选的 // 这些代码总会执行,用于执行清理操做 ...... }
若是在 try 块运行期间未发生异常,则将跳过全部 catch 块,并在 try 块以后执行 finally 块。若是 try 块中的一条语句引起异常,则 Java 运行时将忽略 try 块中的其他语句,并开始搜索匹配的异常处理程序。它将异常类型与每一个 catch 块顺序匹配。
若是 catch 块捕获了该异常类或该异常的超类,则将执行该 catch 块中的语句。而后,在该catch 块以后执行 finally 块中的语句。该程序将在 try-catch-finally 以后继续进入下一个语句,除非它被过早终止。
若是没有任何 catch 块匹配,则异常将沿调用堆栈传递。当前方法执行 finally 子句并从调用堆栈中弹出。调用者遵循相同的过程来处理异常。
前面咱们已经介绍了经过使用 try{}catch(){}finally{}
来对异常进行捕获或者处理。可是对于 JVM 来讲,在它内部是如何进行异常处理呢?实际上 Java 编译后,会在代码后附加异常表的形式来实现 Java 的异常处理及 finally 机制(JDK 1.4.2 以前,Java 编译器是使用 jsr 和 ret 指令来实现 finally 语句,JDK1.7 及以后版本,则彻底禁止在 Class 文件中使用 jsr 和 ret 指令)。
属性表(attribute_info)能够存在于 Class 文件、字段表、方法表中,用于描述某些场景的专有信息。属性表中有个 Code 属性,该属性在方法表中使用,Java 程序方法体中的代码被编译成的字节码指令存储在 Code 属性中。而异常表(exception_table)则是存储在 Code 属性表中的一个结构,这个结构是可选的。
异常表结构以下表所示。它包含 4 个字段:若是当字节码在第 start_pc 行到 end_pc 行之间(包括 start_pc 行而不包括 end_pc 行)出现了类型为 catch_type 或者其子类的异常(catch_type 为指向一个 CONSTANT_Class_info 型常量的索引),则跳转到第 handler_pc 行执行。若是 catch_type 为 0,表示任意异常状况都须要转到 handler_pc 处进行处理。
异常结构表:
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | end_pc | 1 |
u2 | handler_pc | 1 |
u2 | catch_type | 1 |
下面咱们开始来分析一下 一个 catch 语句,多个 catch 语句 和 try-catch-finally 语句 这三种情形所生成的字节码。从而加深对 JVM 内部 try-catch-finally
机制的理解。
为了节省篇幅示例代码就不贴出来了,本人已上传上传至 Gist,须要完整代码的小伙伴请自行获取。注意:经过 javap -v -p ClassName(编译后所生成 class 文件的名称) 能够查看生成的 class 文件的信息。
由于使用一字节表示操做码,因此 Java 虚拟机最多只能支持 256(2^8 )条指令。
Java 虚拟机规范已经定义了 205 条指令,操做码分别是 0(0x00)到 202(0xCA)、254(0xFE)和 255(0xFF)。这 205 条指令构成了 Java 虚拟机的指令集(instruction set)。
Java 虚拟机规范把已经定义的 205 条指令按用途分红了 11 类:
保留指令共有 3 条。其中一条是留给调试器的,用于实现断点,操做码是202(0xCA)
,助记符是breakpoint
。另外两条留给Java
虚拟机实现内使用,操做码分别是254(0xFE)
和266(0xFF)
,助记符是impdep1
和impdep2
。这三条指令不容许出如今 class 文件中。
若想了解完整的 Java 字节码指令列表,能够访问 Wiki - Java_bytecode_instruction_listings 这个页面。
操做数栈也常称为操做栈。它是各类各样的字节码操做如何得到他们的输入,以及他们如何提供他们的输出。
例如,考虑 iadd 操做,它将两个 int 添加在一块儿。要使用它,你在堆栈上推两个值,而后使用它:
iload_0 # Push the value from local variable 0 onto the stack iload_1 # Push the value from local variable 1 onto the stack iadd # Pops those off the stack, adds them, and pushes the result
如今栈上的顶值是这两个局部变量的总和。下一个操做可能须要顶层栈值,并将其存储在某个地方,或者咱们可能在堆栈中推送另外一个值来执行其余操做。
假设要将三个值添加在一块儿,堆栈使这很容易:
iload_0 # Push the value from local variable 0 onto the stack iload_1 # Push the value from local variable 1 onto the stack iadd # Pops those off the stack, adds them, and pushes the result iload_2 # Push the value from local variable 2 onto the stack iadd # Pops those off the stack, adds them, and pushes the result
如今栈上的顶值是将这三个局部变量相加在一块儿的结果。
让咱们更详细地看看第二个例子:
咱们假设:
> 堆栈是空的开始
> 局部变量 0 包含 27
> 局部变量 1 包含 10
> 局部变量 2 包含 5
因此最初 stack 的状态:
+-------+ | stack | +-------+ +-------+
而后咱们执行:
iload_0 # Push the value from local variable 0 onto the stack
当前操做数栈的状态:
+-------+ | stack | +-------+ | 27 | +-------+
接着继续执行:
iload_1 # Push the value from local variable 1 onto the stack
当前操做数栈的状态:
+-------+ | stack | +-------+ | 10 | | 27 | +-------+
如今咱们执行 iadd 指令:
iadd # Pops those off the stack, adds them, and pushes the result
该指令会将 10 和 27 出栈并对它们执行加法运算,完成计算后会把结果继续入栈。此时操做数栈的状态为:
+-------+ | stack | +-------+ | 37 | +-------+
继续执行如下指令:
iload_2 # Push the value from local variable 2 onto the stack
该指令执行以后,操做数栈的状态:
+-------+ | stack | +-------+ | 5 | | 37 | +-------+
最后咱们执行 iadd 指令:
iadd # Pops those off the stack, adds them, and pushes the result
该指令执行以后,操做数栈的最终状态:
+-------+ | stack | +-------+ | 42 | +-------+
前面咱们已经介绍了 Java 中异常和 JVM 虚拟机相关知识,以前恰好看过 字节码角度看面试题 —— try catch finally 为啥 finally 语句必定会执行 这篇文章,下面咱们来换个角度,即以 字节码 的角度来分析一下 try-catch-finally 的底层原理。
注意:如下内容须要对 Java 字节码有必定的了解,请小伙伴们选择性阅读。
红色虚线关联块(1)
tryItOut 方法编译后生成如下代码:
0: aload_0 1: invokespecial #2
上述代码的做用是从局部变量表中加载 this,并调用 tryItOut 方法。
蓝色虚线关联块(2)
catch 语句编译后生成如下代码:
7: astore_1 8: aload_0 9: aload_1 10: invokespecial #4
上述代码的做用是加载 MyException 实例,并调用 handleException 方法。
细心的小伙伴可能会发现生成的 Code 的索引是:0 - 1 - 4 -7 - 8 - 9 - 10 -13,没有看到 二、3 和 十一、12。我的猜想是由于 JVM 字节码指令 invokespecial 操做数占用了 2 个索引字节(欢迎知道真相的大佬,慷慨解答)。这里 invokespecial 字节码指令的格式定义以下:
invokespecial indexbyte1 indexbyte2
Exception table
当字节码在第 0 行到 4 行之间(包括 0 行而不包括 4 行)出现了类型为 MyException 类型或者其子类的异常,则跳转到第 7 行。若 type 的值为 0 时,表示任意异常状况都须要转向到 target 处进行处理。
从上图可知,若存在多个 catch 语句,则异常表中会生成多条记录。astore_1 字节码指令的做用是把引用(异常对象 e)存入局部变量表。
基于上图咱们来详细分析一下生成的字节码:
根据上述的分析和图中三个虚线框标出的字节码,相信你们已经知道在 Java 的 try-catch-finally 语句中 finally 语句必定会执行的最终缘由了。