Java异常底层原理,JVM如何抛出异常,try with resources语法糖

172 阅读4分钟

Java异常

本文先介绍try catch finally的基本语法,以及try with resources语法糖原理,对这些内容熟悉的可以直接跳到「异常处理实现原理」

try catch finally基本使用

try catch finally捕获代码段的异常。

finally 即使在try catch中执行了 return操作,也会执行。

finally会先保存try尝试返回的值,然后运行finally代码块,如果finally内有return,会直接返回,无视try尝试返回的值。try的返回值又分为如下两种情况:

int num = 1;
try{
    return num;
}finally{
    num = 2;
}
//无法修改num的值,返回值仍然是1
//因为是基本数据类型,try备份的返回值是数据本身
​
-------------------------------------------------
​
String str = "123";
try{
    return str; //备份好str的地址值后,就去执行finally,然后才返回出去
}finally{
    str = "456";  //通过地址值去修改数据
}
//可以修改数据,return的地址值对应的数据是"456"
//地址就可以修改指向的对象

当然,JVM宕机了,finally块也不会执行。

try with resources基本使用

JDK 7 新增。try-with-resources本质还是try catch finally

当存在必须要关闭的资源时,应该用try with resources。

资源对象需要实现 java.lang.AutoCloseable 接口或者java.io.Closeable接口。

try-with-resources同样可以加catch和finally,但它们会在声明的资源关闭后运行

// 多个资源用 ; 间隔开 在资源关闭时,从后往前关闭
try(InputStream is = new FileInputStream("d:\1.txt")) {
    System.out.println(is);
} catch (IOException e) {
    e.printStackTrace();
}

try with resources语法糖原理

try with resources会修改你的try代码块

修改后的代码:首先try尝试加载资源,即try()内的资源开启部分,然后用一个try包裹你原本try{}内的代码,加上catch与finally,在finally中去调用资源的close方法来关闭资源

直观的感受一下:

// try with resources
try (FileInputStream inputStream = new FileInputStream(new File("test"))) {
        doSomething(); 
    } catch (IOException e) {
        throw new RuntimeException(e.getMessage(), e);
    }
// --------------------------------
 try {
     // 加载资源
        FileInputStream inputStream = new FileInputStream(new File("test"));
        Throwable var2 = null;
     // 用一个额外的try代码块执行原本的try内的逻辑
        try {
             doSomething(); 
        } catch (Throwable var12) {
            // ...
        } finally {
            // 在finally代码块内释放资源
            // 省略IOException等判断,异常处理
           inputStream.close();
        }
    // 原本的catch仍然保留
    } catch (IOException var14) {
        throw new RuntimeException(var14.getMessage(), var14);
    }

异常处理实现原理

通过javap观察字节码指令:

可以看到,有catch处理,会有一张Exception table,异常表,from ~ to是try包起来的代码块,target是catch处理异常的代码块,如果多个catch,就会有多个这样的行,另外finally的type是any,并且有x个catch,就有x+1个finally在table中,因此无论try结束,还是catch结束,都会执行finally。

异常表是怎么来的

在JVM的Class文件结构,方法表 -> 属性表Code -> exception_info,可以参考: 深度解析字节码文件

抛出异常方法如何退出

即此时异常表处理不了抛出的异常时:

  • 弹栈,即把异常抛给调用该方法的方法,看看它的异常表能不能处理
  • 如果所有的栈帧被弹出,仍然无法处理这个异常,则抛给当前的Thread,Thread则会终止
  • 如果当前Thread为最后一个非守护线程,且未处理异常,则会导致JVM终止运行

异常的性能问题

建立一个异常对象,是建立一个普通Object耗时的约20倍甚至更大;但这还不是最严重的:抛出、接住一个异常对象,所花费时间大约是建立异常对象的4倍

再来看异常与循环:

// 代码段1      
        try {
            for (int i = 0; i < 5000; i++) {
                doSomething();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
// 代码段2 
       for (int i = 0; i < 5000; i++) {
            try {
                blackhole.consume(i);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

一个try块在循环外,一个try块在循环里,它们之间的性能会有差距吗?

实际上,二者差不多,通过javap看字节码会发现字节码指令差距并不大,这是因为JVM做了一些优化操作。所以写代码时仍然业务优先。

抛出异常,JVM做了什么

通过javap看字节码,抛出异常时,字节码指令为:athrow

而athrow指令可能会做:

  • 检查栈顶异常对象类型(只检查是不是null,是否referance类型,是否Throwable的子类一般在类验证阶段的数据流分析中做,或者索性不做靠编译器保证了,编译时写到Code属性的StackMapTable中,在加载时仅做类型验证)
  • 把异常对象的引用出栈
  • 搜索异常表,找到匹配的异常handler
  • 重置PC寄存器状态
  • 清理操作栈
  • 把异常对象的引用入栈
  • 把异常方法的栈帧逐个出栈(这里的栈是VM栈)
  • 残忍地终止掉当前线程。

要想再进一步了解athrow指令,就要看HotSpot的源代码了

CASE(_athrow): {
    // 1. 获取操作栈中引用的异常对象
    oop except_oop = STACK_OBJECT(-1);
    // 2. 判断异常对象是否为null,是抛出NPE异常
    CHECK_NULL(except_oop);
    //  set pending_exception so we use common code
    //  common code 指 handle_return 即方法的返回时的处理
    THREAD->set_pending_exception(except_oop, NULL, 0);
    // handle_exception封装了处理异常的逻辑
    goto handle_exception;
}

我们要明白:不仅仅athrow会抛出异常,虚拟机运作期间也会产生异常,所以出现异常后的方法退出动作在通用的handle_return里面根据pending_exception进行处理

// 处理异常逻辑
handle_exception: {
    // 1. 查找异常表
    // 2. 找到:把异常对象重新入栈,重置PC指针为异常handler的起始位置,方法可以继续正常执行
    // 3. 没找到:重新设置上pending_exception,剩下的就交给handle_return
}

最终,handle_return中会根据pending_exception标志来决定方法是否出现异常,要不要退出。

更详细的解释:透过JVM看Exception本质 - FenixSoft 3.0 - ITeye博客

异常处理实战

可以看下这篇文章:Java异常处理和最佳实践(含案例分析)

参考文献

透过JVM看Exception本质 - FenixSoft 3.0 - ITeye博客