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异常处理和最佳实践(含案例分析)