Skip to content

Latest commit

 

History

History
125 lines (81 loc) · 9.35 KB

异常.md

File metadata and controls

125 lines (81 loc) · 9.35 KB

Java 异常

1. 概述

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

  • 抛出异常
    • 显式: 显式抛异常大部分是应用程序,指的是我们在代码中throw异常实例
    • 隐式: 主体是Java虚拟机,指的是Java虚拟机在执行过程中,碰到无法继续执行的异常状态,自动抛出异常.eg: ArrayIndexOutOfBoundsException
  • 捕获异常
    • try代码块 : 标记需要进行异常监控的代码
    • catch代码块 : 跟在try代码块之后,用来捕获try代码块中触发的某种指定类型的异常.除了声明所捕获异常的类型之外,catch代码块还定义了针对该异常类型的异常处理器.try代码块后面可以跟着多个catch代码块,来捕获不同类型的异常,Java虚拟机会从上至下匹配异常处理器. 因此,前面的catch代码块所能捕获的异常类型不能覆盖后边的,否则编译器会报错.
    • finally代码块 : 跟着try代码块或catch代码块之后(可以直接try..finally),用来声明一段必定会运行的代码.它的设计初衷是为了避免跳过某些关键的清理代码,例如已打开的系统资源.

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

2. 分类

所有异常都是Throwable类或者其子类的实例.Throwable有两大直接子类,第一个是Error,涵盖程序不应该捕获的异常,当程序触发Error时,它的执行状态已经无法恢复,需要终止线程甚至终止虚拟机. 第二个是Exception,涵盖程序可能需要捕获并且处理的异常.Exception又可以分为受检异常和非受检异常,受检异常是指需要显示捕获或声明的异常,非受检异常则指RuntimeException.

3. 实现原理

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

在编译生成的字节码中,每个方法都附带一个异常表,异常表中的每一个条目代表一个异常处理器.并且由from指针、to指针、target指针以及所捕获的异常类型构成.这些指针的值是字节码索引,用以定位字节码.from指针和to指针标识了该异常处理器所监控的范围,例如try代码块的范围;target指针则指向异常处理器的起始位置,例如catch代码块的起始位置.

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

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

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

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

如果catch代码块捕获了异常A,并且触发了另外一个异常B,那么finally会捕获异常B,并且重新抛出异常B,也就是说原本的异常会被忽略掉,这对代码调试来说十分不利.

4. Java 7的Suppressed异常以及语法糖

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异常的功能,来避免原异常"被消失".

public class Foo implements AutoCloseable {
  private final String name;
  public Foo(String name) { this.name = name; }

  @Override
  public void close() {
    throw new RuntimeException(name);
  }

  public static void main(String[] args) {
    try (Foo foo0 = new Foo("Foo0"); // try-with-resources
         Foo foo1 = new Foo("Foo1");
         Foo foo2 = new Foo("Foo2")) {
      throw new RuntimeException("Initial");
    }
  }
}

// 运行结果:
Exception in thread "main" java.lang.RuntimeException: Initial
        at Foo.main(Foo.java:18)
        Suppressed: java.lang.RuntimeException: Foo2
                at Foo.close(Foo.java:13)
                at Foo.main(Foo.java:19)
        Suppressed: java.lang.RuntimeException: Foo1
                at Foo.close(Foo.java:13)
                at Foo.main(Foo.java:19)
        Suppressed: java.lang.RuntimeException: Foo0
                at Foo.close(Foo.java:13)
                at Foo.main(Foo.java:19)

除了try-with-resources语法糖之外,Java 7还支持在同一个catch代码块中捕获多种异常.实际实现非常简单,生成多个异常表条目即可.

// 在同一 catch 代码块中捕获多种异常
try {
  ...
} catch (SomeException | OtherException e) {
  ...
}

5. 常见问题

5.1 使用异常捕获的代码为什么比较耗费性能?

因为构造异常的实例比较耗性能.站在JVM的角度,JVM在构造异常实例时需要生成该异常的栈轨迹.这个操作会逐一访问当前线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息.

5.2 finally是怎么实现无论异常与否都能被执行的?

这个事情是由编译器来实现的,现在的做法是这样的,编译器在编译Java代码时,会复制finally代码块的内容,然后分别放在try-catch代码块所有的正常执行路径及异常执行路径的出口中.

5.3 Exception 和 Error 区别

Exception和Error都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出(throw)或者捕获,它是异常处理机制的基本组成类型.

Exception和Error体现了Java平台设计者对不同异常情况的分类,Exception是程序正常运行中,可以预料的意外情况,可能并且应该捕获,进行相应的处理.

Error是指正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序处于非正常状态,不可恢复状态.既然是非正常情况,所以不便于也不需要捕获,常见的如OutOfMemoryError之类的都是Error的子类.

Exception又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源码里必须显式地进行捕获处理,这里是编译期检查的一部分.不检查异常就是所谓的运行时异常,类似NullPointerException,ArrayIndexOutOfBoundsExceptin之类的,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求.

5.4 ClassNotFoundException 和 NoClassDefFoundError 区别

需要注意到的是一个是Exception一个是Error.

Java支持使用Class.forName方法来动态加载类,任意一个类的类名如果被作为参数传递给这个方法都将导致该类被加载到JVM内存中,如果这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFountException异常.

如果JVM或者ClassLoader实例尝试加载(可能通过正常的方法调用,也可能是使用new来创建新的对象)类的时候找不到类的定义.要查找的类在编译的时候是存在的,运行的时候却找不到类,这个时候就会导致NoClassDefFoundError.造成该问题的原因可能是打包过程中漏掉了部分类,或者jar包出现了损毁或者篡改.解决这个问题的方法是查找那些在开发期间存在于类路径但运行期间却不在类路径下的类.

参考