JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

JVM是如何处理各种异常的呢?

wys521 2024-11-20 22:53:19 精选教程 22 ℃ 0 评论

JVM异常处理机制

异常处理的如下要素共同实现程序控制流的非正常转移:

  • 抛出异常
  • 捕获异常

抛出异常可分为:

  • 显式显式抛异常的主体是应用程序,它指的是在程序中使用“throw”关键字,手动将异常实例抛出。
  • 隐式隐式抛异常的主体则是JVM,指JVM在执行过程中,碰到无法继续执行的异常状态,自动抛异常。如JVM在执行读取数组操作时,发现输入的索引值是负数,抛ArrayIndexOutOfBoundsException。

捕获异常涉及如下代码块:

  1. try代码块标记需异常监控的代码
  2. catch代码块跟在try代码块之后,用来捕获在try代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型之外,catch代码块还定义了针对该异常类型的异常处理器。在Java中,try代码块后面可以跟着多个catch代码块,来捕获不同类型的异常。Java虚拟机会从上至下匹配异常处理器。因此,前面的catch代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错。
  3. finally代码块跟在try代码块和catch代码块后,声明一段必运行的代码。为避免跳过某些关键的清理代码,如关闭已打开的系统资源。程序正常执行的情况下,这段代码会在try代码块后运行。否则,try代码块触发异常时,若该异常未被catch,finally块会直接运行,且在运行后重新抛该异常。
  • 若该异常被catch代码块捕获,finally代码块则在catch代码块之后运行
  • 若不幸的catch代码块也触发了异常,finally代码块同样会运行,并抛出catch代码块所触发异常
  • 再极端点,finally代码块也触发异常,则只好中断当前finally代码块的执行,并往外抛异常。

1 异常的基本概念

Java中所有异常都是Throwable类或其子类实例。Throwable有两大直接子类:

  • Error,程序不应捕获的异常当程序触发Error,其执行状态已无法恢复,需中止线程甚至是JVM
  • Exception,程序可能需捕获且处理的异常

Exception有个子类RuntimeException:程序虽无法继续执行,但还能抢救一下。

  • RuntimeException和Error属于非检查异常(unchecked exception)
  • 其它异常属于检查异常(checked exception)检查异常都要程序显式捕获或在方法声明中用throws关键字抛出。一般程序自定义的异常应都设计为检查异常,以便最大化利用Java编译器的编译时检查。

异常实例的构造十分昂贵

构造异常实例时,JVM需生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的Java栈帧,并记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名及在代码第几行触发异常。

生成栈轨迹时,JVM会忽略异常构造器及填充栈帧的Java方法(Throwable.fillInStackTrace),直接从新建异常位置开始算起。JVM还会忽略标记为不可见的Java方法栈帧。

是否可以缓存异常实例,在需要用时直接抛出?

语法上看,可以。然而,该异常对应栈轨迹并非throw语句的位置,而是新建异常的位置。因此,这可能误导开发人员定位到错误位置。这也是为何项目中都选择抛出新建的异常实例。

2 JVM如何捕获异常?

在编译生成的字节码中,每个方法都附带一个异常表。异常表中的每个条目代表一个异常处理器,且构成如下:

  • from指针
  • to指针
  • target指针
  • 所捕获的异常类型

这些指针的值是字节码索引(bytecode index,bci),用以定位字节码:

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


如以上main方法,catch代码块所捕获的异常类型为Exception。

编译后,该方法的异常表有个条目:

  • from、to指针分别为0、3,代表监控范围为索引为0~3的字节码(不包括3)
  • 该条目的target指针6,代表该异常处理器从索引为6的字节码开始
  • 条目的最后一列,代表该异常处理器所捕获的异常类型是Exception

当程序异常,JVM会从上至下遍历异常表中所有条目。当触发异常的字节码的索引值在某异常表条目的监控范围内,JVM会判断所抛异常和该条目想捕获的异常是否匹配:

  • 若匹配,JVM会将控制流转移至该条目target指针指向的字节码

若遍历完所有异常表条目,JVM仍未匹配到异常处理器,则会弹出当前方法对应的Java栈帧且在调用者重复上述操作。最坏情况下,JVM需遍历当前线程Java栈上所有方法的异常表。

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

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

javap查看下面这段包含了try-catch-finally代码块编译结果:

编译结果包含三份finally代码块:

  • 前两份分别位于try代码块和catch代码块的正常执行路径出口
  • 最后一份作为异常处理器,监控try、catch代码块。捕获:try代码块触发、未被catch代码块捕获异常catch代码块触发的异常

若catch代码块捕获异常,并触发另一个异常,那finally捕获且重抛的异常是哪个?后者。即原本的异常会被忽略,这对代码调试很不利。

3 Supressed异常及语法糖

Java 7引入了Supressed异常来解决这个问题。这个新特性允许开发人员将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息。

然而,Java层面的finally代码块缺少指向所捕获异常的引用,所以这个新特性使用起来非常繁琐。

为此,Java 7专门构造了一个名为try-with-resources的语法糖,在字节码层面自动使用Supressed异常。当然,该语法糖的主要目的并不是使用Supressed异常,而是精简资源打开关闭的用法。

在Java 7之前,对于打开的资源,我们需要定义一个finally代码块,来确保该资源在正常或者异常执行状况下都能关闭。

资源的关闭操作易触发异常。若同时打开多个资源,则每个资源都对应一个独立try-finally代码块,以保证每个资源都能够关闭,代码变得繁琐。

Java 7的try-with-resources语法糖,极大简化上述代码。可在try关键字后声明并实例化实现了AutoCloseable接口的类,编译器将自动添加对应close()操作。在声明多个AutoCloseable实例时,编译生成的字节码类似上面手工编写代码的编译结果。相比手工代码,try-with-resources还会使用Supressed异常的功能,避免原异常“被消失”。

Java 7还支持在同一catch代码块捕获多种异常,实现很简单:生成多个异常表条目。

4 总结

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

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

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表