06 JVM 是如何处理异常的

在 JAVA 中,异常处理的方式主要是抛出异常和捕获异常。这两大要素共同实现程序控制流的非正常转移。html

抛出异常能够分为显示和隐式两种。显示抛出异常的主体是应用程序,它指的是在程序中使用 throw 关键字,手动将异常实例抛出。隐式抛出异常的主题是 Java 虚拟机,它指的是 Java 虚拟机在执行过程当中,碰到没法继续执行的异常状态,自动抛出异常。例如数组越界异常。数组

捕获异常主要设计一下三种代码块: 1:try 代码块,用来标记须要进行异常监控的代码。 2:catch 代码块,跟在 try 代码块以后,用来捕获在 try 代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型以外,catch 代码块还定义了针对该异常类型的异常处理。在 Java 中,try 代码块后面能够跟着多个 catch 代码块,来捕获不一样类型的异常。Java 虚拟机会自上而下匹配异常处理器,因此前面的 catch 代码块不能覆盖后面的,不然编译器报错。 3:finally 代码块,跟在 catch 代码块以后,用来声明一段一定运行的代码。它的设计是为了不跳过某些关键的清理代码。好比关闭已经打开的系统资源。缓存

在程序正常执行状况下, finally 代码块会在 try 代码块以后运行。架构

若是 try 代码块触发异常,异常没有被捕获的状况下,finally 代码块会直接运行,并在运行结束后从新抛出异常。若是该异常被 catch 代码块捕获,finally 代码块则会在 catch 代码块以后运行。在某些状况下,catch 代码块也触发了异常,那么 finally 代码一样会执行,并抛出 catch 代码块触发的异常。若是 finally 代码块也触发了异常,那就中断 finally 代码块,向上抛出异常。性能

异常的基本概念

在 Java 语言规范中,全部异常都是 Throwable 类或者其子类的实现。优化

Throwable 类有两大直接子类。一个是 Error,涵盖程序不该捕获的异常。当程序触发 Error 的时候,它的执行状态已经没法会发,须要终止线程甚至是终止虚拟机。一个是 Exception ,涵盖程序可能须要捕获而且处理的异常。spa

RuntimeException 是 Exception 的一个特殊子类,用来表示程序没法继续执行,可是还能抢救一下的状况,数组越界即是其中一种。线程

RuntimeException 和 Error 属于 Java 里的非检查异常。其余异常则属于检查异常。在Java 语法中,全部的检查异常都须要程序显示地捕获,或者在方法声明中用 throws 关键字标注。一般状况下,程序自定义的异常应为检查异常,以便最大化利用 Java 编译器的编译时检查。设计

异常实例的构造十分昂贵。在构造异常实例时,Java 虚拟机须要生成该异常的栈轨迹。该曹组会逐一访问当前线程的 Java 栈帧,而且记录下各类调试信息,包括栈帧所指向的方法的名字,方法所在的类名,文件名,以及在代码中的第几行触发该异常。在生成栈轨迹时,Java 虚拟机会忽略掉异常构造器以及填充栈帧的 Java 方法,直接重新建异常位置开始算起。此外,Java 虚拟机还会忽略标记为不可见的 Java 方法栈帧。指针

异常实例的构造昂贵,可是却没有作缓存优化。若是作了缓存优化,那么抛出的异常实例对应的栈轨迹并不是 throw 语句的位置了,而是第一次新建异常的位置。因此,为了准确的定位到错误的位置,咱们每每选择抛出新建异常实例。

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

在编译生成的字节码中,每一个方法都附带一个异常表,异常表中的每个条目表明一个异常处理器,而且由 form 指针,to 指针,target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引,用来定位字节码。

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

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

若是遍历完异常表的条目不曾匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,而且在调用者中重复上述操做。

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

针对异常执行路径,Java 编译器会生成一个(上图变种2)或者多个(上图变种1)异常表条目,监控整个 try-catch 代码块,而且捕获全部种类的异常。这些异常表条目的 target 指针将指向另外一份复制的 finally 代码块(上图变种1,变种2 中红色 finally block),而且从新抛出捕获的异常。

问题:若是 catch 代码块捕获了异常,而且触发了另外一个异常,那么 finally 捕获而且重抛的异常是 catch 代码块触发的新的异常,本来的异常就被忽略了。这对代码调试来讲,就不友好了。

Java 7 中引出了 Supressed 异常来解决上面的问题。这个新特性容许开发人员将一个异常附在另外一个异常上,这样抛出的异常就能够附带多个异常的信息。

问答

Q:为何使用异常捕获的代码比较耗费性能

单从 Java 语法上看不出来,可是从 JVM 实现的细节上来看就明白了。构造异常实例,须要生成该异常的栈轨迹。该操做会逐一访问当前线程的栈帧,记录各类调试信息,包括类名,方法名,触发异常的代码行数等等。

Q:finally 是怎么实现不管异常与否都能执行

编译器在编译代码时会复制 finally 代码块放在 try-catch 代码块全部正常执行路径以及异常执行路径的出口处。

Q:finally 中有 ruturn 语句,catch 中抛出的异常会被忽略,为何

catch 抛出的异常会被 finally 捕获,执行完 finally 后会从新抛出该异常。因为 finally 中有 return 语句,在从新抛出异常以前,代码就已经返回了。

Q:方法的异常表都包含哪些异常

方法的异常表只声明这段代码会被捕获的异常,并且是非检查异常。若是 catch 中有自定义异常,那么异常表中也会包含自定义异常的条目。

Q:检查异常和非检查异常也就是其余书籍中说的编译期异常和运行时异常?

检查异常也会在运行过程当中抛出。可是它会要求编译器检查代码有没有显式地处理该异常。非检查异常包括Error和RuntimeException,这两个则不要求编译器显式处理。

总结

本文创做灵感来源于 极客时间 郑雨迪老师的《深刻拆解 Java 虚拟机》课程,经过课后反思以及借鉴各位学友的发言总结,现整理出本身的知识架构,以便往后温故知新,查漏补缺。

关注本人公众号,第一时间获取最新文章发布,每日更新一篇技术文章。

关注我就是给我最大的鼓励和支持

原文出处:https://www.cnblogs.com/yuepenglei/p/10290161.html

相关文章
相关标签/搜索