夯实 Java 基础3 - 异常机制

Java 提供了完备的异常机制,在 Java7 引入了 try-with-resources 语法糖。

  • 一、Java 异常分类
  • 二、try-catch-finally 在字节码层面的实现
  • 三、try-with-resources
  • 四、try-catch 逻辑为什么慢

一、Java 异常分类

image.png

图片来自《Java 36讲》
Java异常分为 Error 和 Exception,绝大部分的 Error 会导致程序处于非正常不可恢复状态,不应捕获;Exception 表示可能需要捕获并且处理的异常。Exception 中只有 RuntimeException(运行时异常)是不需要捕获或声明抛出的,其他的 checked exception(检查型异常)需要捕获或声明抛出。

二、try-catch-finally 在字节码层面的实现

测试代码如下:

public class TestException {
    public static void main(String[] args) {
        try {
            if (1 == 1) {
                System.out.println("==========normal");
                return;
            }
            throw new RuntimeException();
        } catch (RuntimeException e) {
            System.out.println("===========InterruptedException");
        } finally {
            System.out.println("===========finally");
        }
    }
}

javap -c TestException > TestException8.txt,查看字节码:

image.png
  • 方法异常表:from ~ to 异常处理器监控的范围 [from, to);target 异常处理器的起始位置;type 所捕获的异常类型
  • 注意上述的两行 any 异常,第一行捕获 try([0, 8))中的异常,第二行捕获 catch([17, 26))中的异常

可以看出实际上 finally 的代码会复制在 try 和 catch 块中的每一个路径出口处。那路径出口是什么?

image.png

图片来自《深入拆解 Java 虚拟机》

  1. 正常路径执行 try 正常 - finally 正常(黑色实线)
  2. try 异常 - catch 住了异常 - finally 正常(黄色实线 + 黑丝虚线)
  3. try 异常 - catch 住了异常 - finally 抛出异常(黄色实线 + 黄色虚线)
  4. try 异常 - catch 也异常(例如又显式的跑出了异常) - 则 finally 重新抛出异常(红色实线 - 下半括号)
  5. try 异常 - catch 没有捕获住该异常 - 则 finally 重新抛出异常(红色实线 - 上半括号)
  • 其中复制的红色的 finally 块,就是 jvm 捕获所有的异常,最后执行完 finally 块后(如果 finally 块发生异常,则直接抛出 finally 块发生的异常,快速失败了)再抛出这些异常(这也是字节码中的方法异常表中的 any 异常,注意上述的两行 any 异常,第一行捕获 try 中的异常,第二行捕获 catch 中的异常);
  • try 和 catch 中如果是 throw 语句,则后边不会给复制 finally 块了,就只有最后的 any 异常的 finally 块了。

三、try-with-resources

首先来看 Java7 之前的异常处理机制:

try{
    System.out.println("======= try-normal ======");
    throw new RuntimeException("==== try-exception ====");
} catch (Exception e) {
    throw new RuntimeException("====== catch-exception ======");
} finally {
    throw new RuntimeException("====== finally-exception ======");
}

该方法有两个问题:

  1. 此时抛出的异常时 finally 的异常(原理见上边的执行路径图中的红色实线部分),通常 finally 中只是做一些资源关闭操作,当资源关闭发生异常时会覆盖更有价值的 catch 或者 try 中的异常信息
  2. try-catch-finally 一旦发生多层嵌套,可能会发生“try-catch-finally hell”,类似于“callback hell”,可读性很差

Java7 的 try-with-resources 在一定程度上解决了上述问题(try-with-resources 只支持实现了 AutoCloseable 的类)。

try-with-resources 使用姿势:

public class MyCloseable implements AutoCloseable {
    public void doSomeThing() {
        System.out.println("========== doSomething ==========");
    }

    @Override
    public void close() throws Exception {
        System.out.println("========== close ==========");
        throw new RuntimeException("======= close-exception =======");
    }
}

========== use =========
try(MyCloseable mc = new MyCloseable()){
    mc.doSomeThing();
} catch (Exception e) {
    throw new RuntimeException("====== catch-exception ======");
}
  1. 最终执行 close() 之后,抛出的异常是 catch 中的异常,不会被 close() 中的异常覆盖;
  2. 使用语法糖可以将多个 AutoCloseable 资源写在 try 括号中(用";"隔开),避免了“try-catch-finally hell”
  3. 值得注意的是,try-with-resources 只支持实现了 AutoCloseable 的类

四、try-catch 逻辑为什么慢

1、异常实例的构造十分昂贵:构造异常实例时,Java 虚拟机需要生成该异常的栈轨迹。该操作会逐一访问当前线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。(《深入拆解 Java 虚拟机》,具体代码实现见《自己动手实现 Java 虚拟机》第10章)
2、try-catch 流程会影响 JVM 对代码的优化(?)。

你可能感兴趣的:(夯实 Java 基础3 - 异常机制)