06 JVM 是如何处理异常的
在 JAVA 中,异常处理的方式主要是抛出异常和捕获异常。这两大要素共同实现程序控制流的非正常转移。
抛出异常可以分为显示和隐式两种。显示抛出异常的主体是应用程序,它指的是在程序中使用 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 ,涵盖程序可能需要捕获并且处理的异常。

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 虚拟机》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。
关注本人公众号,第一时间获取最新文章发布,每日更新一篇技术文章。

06 JVM 是如何处理异常的的更多相关文章
- 【JVM虚拟机】(9)-- JVM是如何处理异常的
[JVM虚拟机](9)-- JVM是如何处理异常的 上篇博客我们简单说过异常信息是存放在属性表集合中的Code属性表里,那么这篇博客就单独讲Code属性表中的exception_table. 在讲之前 ...
- JVM是如何处理异常的
JVM处理异常 异常处理的两大组成要素是抛出异常和捕获异常.这两大要素共同实现程序控制流的非正常转移. 抛出异常可分为显式和隐式两种.显式抛异常的主体是应用程序,指的是在程序中使用throw关键字,手 ...
- Android(java)学习笔记82:我们到底该如何处理异常?
我们到底该如何处理异常? 原则: 如果该功能内部可以将问题处理,用try,自己能解决就自己解决问题. 如果处理不了,交由调用者处理,这是用throws,自己不能解决的问题,我们就抛出去交个调用者解决这 ...
- Android(java)学习笔记22:我们到底该如何处理异常?
1. 我们到底该如何处理异常? (1)原则: 如果该功能内部可以将问题处理,用try,自己能解决就自己解决问题. 如果处理不了,交由调用者处理,这是用throws,自己不能解决的问题,我们就抛出去交个 ...
- Java如何处理异常方法?
在Java编程中,如何处理异常方法? 本例展示了如何使用System类的System.err.println()方法处理异常方法. package com.yiibai; public class E ...
- Java如何处理异常层次结构?
在Java编程中,如何处理异常层次结构? 以下是异常层次结构的示例图 - 此示例显示如何通过扩展Exception类来处理异常层次结构. package com.yiibai; class Anima ...
- “全栈2019”Java异常第二章:如何处理异常?
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java异 ...
- jvm不打印异常栈
生产环境抛异常,但却没有将堆栈信息输出到日志,确认打印日志方法正确logger.error("somthing error", ex); JVM启动参数加上-XX:-OmitSta ...
- JV默认是如何处理异常
main函数收到这个问题时,有两种处理方式: a:自己将该问题处理,然后继续运行 b:自己没有针对的处理方式,只有交给调用main的jvm来处理 jvm有一个默认的异常处理机制,就将该异常进行处理. ...
随机推荐
- javascript字符串格式化string.format
String.prototype.format = function () { var values = arguments; return this.replace(/\{(\d+)\}/g, fu ...
- docker使用centos7系统构建tomcat镜像
FROM shansongxian/centos-oraclejdk8:latest #此镜像使用centos7系统,精简jdk,只运行java程序,无法编译/构建 MAINTAINER huqian ...
- 慎用python的pop和remove方法
申明:转载请注明出处!!! Python关于删除list中的某个元素,一般有两种方法,pop()和remove(). 如果删除单个元素,使用基本没有什么问题,具体如下. 1.pop()方法,传递的是待 ...
- V2EX 神回复 #1
"抠图"用英文怎么说 今天突然被"抠图"这个单词给难住了," image segmentation "," image cut & ...
- Codeforces Round #327 (Div. 2) C Median Smoothing(找规律)
分析: 三个01组合只有八种情况: 000 s001 s010 0011 s100 s101 1110 s111 s 可以看出只有010,101是不稳定的.其他都是稳定的,且连续地出现了1或0,标记为 ...
- iOS开发学习之大牛们的博客
http://blog.csdn.net/iosbird/article/details/51981023 唐巧:http://blog.devtang.com/blog/archives/ 王巍:h ...
- 2018.2.2 JavaScript中的封装
JavaScript中的封装 1.封装的概念 通过将一个方法或者属性声明为私用的,可以让对象的实现细节对其他对象保密以降低对象之间的耦合程度,可以保持数据的完整性并对其修改方式加以约束,这样可以使代码 ...
- Unity的sendmessage用法
刚学完sendmessage用法,自己也尝试测试了一下,用法如下: 1.在unity2017新建一个场景test 2.在场景中添加一个立方体cube作为主角,另添加一个胶囊体capsule,调整为如图 ...
- pandas 常用统计方法
统计方法 pandas 对象有一些统计方法.它们大部分都属于约简和汇总统计,用于从 Series 中提取单个值,或从 DataFrame 的行或列中提取一个 Series. 比如 DataFrame. ...
- Oracle 系统表
--如果一个表拥有DBA\\ALL\\USERS三个前缀 --DBA_前缀表示DBA拥有的或者可以访问的所有关系表 --ALL_前缀表示当前用户做拥有的或者可以访问的所有关系表 --USERS-前缀表 ...