众所周知,异常处理的两大组成要素是抛出异常和捕获异常。这两大要素共同实现程序控制流的非正常转移。

抛出异常可分为显式和隐式两种。

显式抛异常的主体是应用程序,它指的是在程序中使用“throw”关键字,手动将异常实例抛出。

隐式抛异常的主体则是 Java 虚拟机,它指的是 Java 虚拟机在执行过程中,碰到无法继续执行的异常状态,自动抛出异常。举例来说,Java 虚拟机在执行读取数组操作时,发现输入的索引值是负数,故而抛出数组索引越界异常(ArrayIndexOutOfBoundsException)。

捕获异常则涉及了如下三种代码块。

  1. try 代码块:用来标记需要进行异常监控的代码。

  2. catch 代码块:跟在 try 代码块之后,用来捕获在 try 代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型之外,catch 代码块还定义了针对该异常类型的异常处理器。在 Java 中,try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会从上至下匹配异常处理器。因此,前面的 catch 代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错。

  3. finally 代码块:跟在 try 代码块和 catch 代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码,例如关闭已打开的系统资源。

如果该异常被 catch 代码块捕获,finally 代码块则在 catch 代码块之后运行。在某些不幸的情况下,catch 代码块也触发了异常,那么 finally 代码块同样会运行,并会抛出 catch 代码块触发的异常。在某些极端不幸的情况下,finally 代码块也触发了异常,那么只好中断当前 finally 代码块的执行,并往外抛异常。

异常的基本概念

在 Java 语言规范中,所有异常都是 Throwable 类或者其子类的实例。Throwable 有两大直接子类。第一个是 Error,涵盖程序不应捕获的异常。当程序触发 Error 时,它的执行状态已经无法恢复,需要中止线程甚至是中止虚拟机。第二子类则是 Exception,涵盖程序可能需要捕获并且处理的异常。

Exception 有一个特殊的子类 RuntimeException,用来表示“程序虽然无法继续执行,但是还能抢救一下”的情况。前边提到的数组索引越界便是其中的一种。

RuntimeException 和 Error 属于 Java 里的非检查异常(unchecked exception)。其他异常则属于检查异常(checked exception)。在 Java 语法中,所有的检查异常都需要程序显式地捕获,或者在方法声明中用 throws 关键字标注。通常情况下,程序中自定义的异常应为检查异常,以便最大化利用 Java 编译器的编译时检查。

异常实例的构造十分昂贵。这是由于在构造异常实例时,Java 虚拟机便需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。

当然,在生成栈轨迹时,Java 虚拟机会忽略掉异常构造器以及填充栈帧的 Java 方法(Throwable.fillInStackTrace),直接从新建异常位置开始算起。此外,Java 虚拟机还会忽略标记为不可见的 Java 方法栈帧。我们在介绍 Lambda 的时候会看到具体的例子。

Java 虚拟机是如何捕获异常的?

在编译生成的字节码中,每个方法都附带一个异常表。异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。

其中,from 指针和 to 指针标示了该异常处理器所监控的范围,例如 try 代码块所覆盖的范围。target 指针则指向异常处理器的起始位置,例如 catch 代码块的起始位置。

public static void main(String[] args) {
try {
mayThrowException();
} catch (Exception e) {
e.printStackTrace();
}
}

编译过后,该方法的异常表拥有一个条目。

当程序触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码。

如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者(caller)中重复上述操作。在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。

finally 代码块的编译比较复杂。当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。

针对异常执行路径,Java 编译器会生成一个或多个异常表条目,监控整个 try-catch 代码块,并且捕获所有种类的异常(在 javap 中以 any 指代)。这些异常表条目的 target 指针将指向另一份复制的 finally 代码块。并且,在这个 finally 代码块的最后,Java 编译器会重新抛出所捕获的异常。

这里有一个小问题,如果 catch 代码块捕获了异常,并且触发了另一个异常,那么 finally 捕获并且重抛的异常是哪个呢?答案是后者。也就是说原本的异常便会被忽略掉,这对于代码调试来说十分不利。

总结与实践

今天我介绍了 Java 虚拟机的异常处理机制。

Java 的异常分为 Exception 和 Error 两种,而 Exception 又分为 RuntimeException 和其他类型。RuntimeException 和 Error 属于非检查异常。其他的 Exception 皆属于检查异常,在触发时需要显式捕获,或者在方法头用 throws 关键字声明。

Java 字节码中,每个方法对应一个异常表。当程序触发异常时,Java 虚拟机将查找异常表,并依此决定需要将控制流转移至哪个异常处理器之中。Java 代码中的 catch 代码块和 finally 代码块都会生成异常表条目。

JVM总结-异常处理的更多相关文章

  1. JVM的异常处理

    异常处理的两大组成要素:抛出异常和捕获异常.这两大要素共同实现程序控制流的非正常转移. 抛出异常分为:显式和隐式两种. 显式抛异常的主题是应用程序,它指的是在程序中使用 “throw”  关键字.手动 ...

  2. JVM 学习(一)反射、垃圾回收、异常处理--- 2019年4月

    1.JVM 基础知识点 JVM 虚拟机包含了:自动内存管理器.垃圾回收(垃圾回收调优). 执行顺序:Java 代码 --- .class 字节码文件(加载到虚拟机中) --- Java 类放在方法区中 ...

  3. 四:JVM调优原理与常见异常处理方案

    在jvm调优之前,我们必须先了解jvm的内存模型与GC回收机制,这些在我前面的文章里面有介绍!接下来我们通过一个案例来调整jvm性能. 一测试案例: 1.1 编写demo import java.te ...

  4. 基本数据类型 异常 数组排序 JVM区域划分

               Day01 1.基本数据类型各占几个字节 Byte 1 short2 int4 long8 float4 double6 char2 boolean1 Byte b1=3,b2= ...

  5. java.lang.Throwable 异常/深入

    有几个现象是需要总结的: -------------------------------------- 在java语言中,错误类的基类是java.lang.Error,异常类的基类是java.lang ...

  6. 黑马程序员_Java面向对象_异常

    6.异常 1.异常: 就是程序在运行时出现不正常的情况.问题也是现实生活中一个具体的事物,也可以通过Java的类进行描述,并封装成对象.Exception和Error的子类名都是以父类名作为后缀名. ...

  7. Java并发学习 & Executor学习 & 异常逃逸 & 同步互斥Best Practice & wait/notify, conditon#await/signal

    看了这篇文章:http://www.ciaoshen.com/2016/10/28/tij4-21/ 有一些Java并发的内容,另外查了一些资料. 朴素的Thread 首先,Java中关于线程Thre ...

  8. Java异常模块

    JAVA异常的捕获与处理 视频链接:https://edu.aliyun.com/lesson_1011_8939#_8939 java语言提供最为强大的支持就在于异常的处理操作上. 1,认识异常对程 ...

  9. 流言粉碎机:JAVA使用 try catch 会严重影响性能

    目录 一.JVM 异常处理逻辑 二.关于JVM的编译优化 1. 分层编译 2. 即时编译器 1. 解释模式 2. 编译模式 3. 提前编译器:jaotc 三.关于测试的约束 执行用时统计 编译器优化的 ...

随机推荐

  1. jmeter ---处理Cookie与Session

    有些网站保存信息是使用Cookie,有些则是使用Session.对于这两种方式,JMeter都给予一定的支持. 1.Cookie 添加方式:线程组-配置元件-HTTP Cookie 管理器,如下图: ...

  2. Elasticsearch集成HanLP分词器

    1.通过git下载分词器代码. 连接如下:https://gitee.com/hualongdata/hanlp-ext hanlp官网如下:http://hanlp.linrunsoft.com/ ...

  3. 代理本地局域网其他PC上网的设置

  4. Configure Virtual Serial Port Driver (vspd)注册表

    [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\VSBC7\Ports\COM3COM4] “Port1”=”COM3” “Port2”=” ...

  5. 黄聪:AngularJS如何在filter中相互调用filter

    调用方式如下: app.filter('filter2', function( $filter ) { return function( input) { return $filter('filter ...

  6. Qt QDateTime QTimer的简单实用

    转载:N3verL4nd qttimer.h #ifndef QTTIMER_H #define QTTIMER_H #include <QDialog> namespace Ui { c ...

  7. 【剑指offer】字符串替换

    请实现一个函数,将一个字符串中的每个空格替换成“%20”.例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy. *StringBuffer 扩容 str ...

  8. DP 01背包 七夕模拟赛

    问题 D: 七夕模拟赛 时间限制: 1 Sec  内存限制: 128 MB提交: 60  解决: 23[提交][状态][讨论版] 题目描述 " 找啊找啊找GF,找到一个好GF,吃顿饭啊拉拉手 ...

  9. Bisecting KMeans (二分K均值)算法讲解及实现

    算法原理 由于传统的KMeans算法的聚类结果易受到初始聚类中心点选择的影响,因此在传统的KMeans算法的基础上进行算法改进,对初始中心点选取比较严格,各中心点的距离较远,这就避免了初始聚类中心会选 ...

  10. 读取 Excel 之 Epplus

    using (OpenFileDialog fd = new OpenFileDialog()) { fd.Filter = "Excel 2007文件(*.xlsx)|*.xlsx|所有文 ...