java异常处理机制
概述
结构不佳的代码不能运行,这是java基本理念。发现错误的理想时机是编译期,但编译器不能发现所有错误,余下的问题就需要在运行期解决。程序发生异常需异常处理,把信息发送给特定的接收者处理。
异常分类
异常分为Error和Exception。Error错误发生,系统只能记录错误成因,安全退出。检查性异常,编译期就可以发现,运行异常,只能到程序运行时才能发现。
异常表
java编译后Class文件,Code是最重要的一个属性,把一个Java程序分为代码(Code
,方法体里面的代码)和元数据(Metadata
,类、字段、方法定义及其他信息)。Code
用于描述代码,其他数据都用于描述元数据。Code
属性中有异常表exception_table:
exception_table {
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type; }
如果字节码在这个区间[start_pc,end_pc)
出现了类型为catch_type
或者其子类的异常catch_type
为指向一个CONSTANT_Class_info
型常量的索引),则转到handler_pc
继续处理,当catch_type
值为0时,代表任何异常都需要转到handler_pc
处理。
由此看出java使用异常表,而不是简单的跳转实现异常处理。
异常处理
抛出异常(throw、throws)
遇到异常时,不做处理而是把它抛给调用者,由调用者根据情况处理。有可能是直接捕获并处理,也有可能继续上抛。抛出异常方法:throw,throws、系统自动抛出
- throws作用在方法上,声明方法运行过程中可能会抛出异常,以便调用者根据不同的异常类型预先定义不同的处理方式。
- throw作用在方法内抛出封装了异常信息的对象,程序执行到throw时,后续代码不再执行,而是跳转到调用者,并将异常信息抛给调用者。throw之后语句无法被执行,但是finally除外。
捕获(try…catch…finally)
捕获异常,针对性处理每种可能出现的异常,捕获异常后根据不同情况做不同处理。
public int testFinally() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
try{
x = 3;
} catch (Exception e) {
x = 4;
}
}
}
testFinally方法体字节码:
public int testFinally();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=7, args_size=1
0: iconst_1//int类型常量1加载到栈顶
1: istore_1//将栈顶int类型元素保存到局部变量表1的位置,此时x=1
2: iload_1//将局部变量表1位置处的int类型元素加载到栈顶
3: istore_2//将栈顶int型元素保存到局部变量表2的位置,保存的是return的值,此时是1
4: iconst_3//将int常量3加载到栈顶
5: istore_1//将栈顶int值存入局部变量表1的位置,此时x=3
//finally也没发生异常跳转到字节码第12行开始执行,执行return x的动作
6: goto 12
/**看局部变量表[start_pc,end_pc)也就是4~5发生异常,跳转到第9行开
* 始。也就finally中的try语句块x=3赋值操作如果发生异常,走它的
* catch语句块
*/
9: astore_3//将栈顶引用变量e存入当前栈帧的局部变量表3的位置
10: iconst_4//将int常量4加载到栈顶
11: istore_1//将栈int元素值保存到局部变量表1的位置,此时x=4
//最外层try中的renturn x
12: iload_2//局部变量表2位置上的int型变量加载到栈顶,此时是1
13: ireturn//返回栈顶int型元素,此时返回1
/**看局部变量表[start_pc,end_pc)也就是0~3发生异常,跳转到第14行开
* 始。也就最外层的try语句块x=1赋值操作、返回值放入栈顶操作如果发生
* 异常,走它的catch语句块
*/
14: astore_2//将栈顶引用变量e存入当前栈帧的局部变量表2的位置
15: iconst_2//int常量2加载到栈顶
16: istore_1//栈顶int值保存到局部变量表1的位置,此时x=2,
17: iload_1//局部变量表1位置的int数值加载到栈顶
18: istore_3//将栈顶int类型数值存入局部变量表3的位置,存入的是2
//finnaly
19: iconst_3//int常量3加载的栈顶
20: istore_1//栈顶int值保存到局部变量表1的位置,此时x=3
//没有发生异常
21: goto 28//没有发生异常跳转到28行,执行return操作
/**看局部变量表[start_pc,end_pc)也就是19~20发生异常,跳转到第24行
* 开始。也就finnaly的try语句块x=1赋值操作、返回值放入栈顶操作如果
* 发生异常,走它的catch语句块
*/
24: astore 4//将引用类型变量保存到局部变量表4的位置
26: iconst_4//见常量4加载到栈顶
27: istore_1//将栈顶int值保存到局部变量表1的位置,x=4
28: iload_3//将局部变量表3处的int值加载到栈顶,2
29: ireturn//将栈顶元素值返回,此时return 2
/**
* 看局部变量表[start_pc,end_pc)也就是0~3或14~18或者30发生异常不属
* 于Exception及其子类的异常。最外层try语句块或者它的catch发生不属于
* Exception,比如error 从30行执行。
*/
30: astore 5//将引用类型变量(不属于Exception异常)保存到局部变量表5的位置
//finnaly
32: iconst_3//将常量3加载到栈顶
33: istore_1//将栈顶元素值保存到局部变量表1的位置,此时x=3
34: goto 41//没有发生异常转到字节码41行执行
/**看局部变量表[start_pc,end_pc)也就是32~33发生异常,跳转到第37行
* 开始。也就finnaly的try语句块x=3赋值操作如果发生异常,走它的
* catch语句块
*/
37: astore 6//异常e加载到栈顶6的位置
39: iconst_4//int类型常量4加载到栈顶
40: istore_1//栈顶元素保存到局部变量表1处,x=4的赋值操作
41: aload 5//局部变量表5处的引用类型变量加载到栈顶
43: athrow//抛出栈顶异常,不属于Exception及其子类的异常。其他Exception异常都会被捕获,没有throw不会抛出给上层调用者。
Exception table:
from to target type
4 6 9 Class java/lang/Exception
0 4 14 Class java/lang/Exception
19 21 24 Class java/lang/Exception
0 4 30 any
14 19 30 any
32 34 37 Class java/lang/Exception
30 32 30 any
finally 程序块代码进行 “子程序调用”(finally 程序块被编译为嵌入式子程序)当 finally 程序块完成后,ret 2 指令将控制权返回到位于索引 4 的 jsr 指令之后的指令。
- 1.6之前finally块靠jsr/ret两个指令完成,让代码精简,但不易读。其实知道jsr跳转前将下一条指令地址推入栈顶,跳转后第一时间将地址存入局部变量表,执行finally块结束时ret 局部变量表中存储的地址也不难
- 1.6之后,字节码的每一个控制流后都加上finally块的字节码,少了jsr/ret两个指令易读,但是膨胀了字节码大小。
不管哪一种,只要finally块中有return,无正常/异常流结果都会被吞噬。
异常丢失
finally中有return
finally
中的return
会覆盖掉catch
的异常。
public int foo() throws Exception
{
try {
int a = 5/0;
return 1;
}catch(ArithmeticException amExp) {
throw new Exception("aam");
}finally {
int b=90;
return b;
//throw new Exception("ab");
}
}
public int foo() throws java.lang.Exception;
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=3, locals=6, args_size=1
0: iconst_5
1: iconst_0
2: idiv
3: istore_1
4: iconst_1
5: istore_2
6: bipush 90
8: istore_3
9: iload_3
10: ireturn
11: astore_1
12: new #3 // class java/lang/Exception
15: dup
16: ldc #4 // String aam
18: invokespecial #5 // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
21: athrow
22: astore 4
24: bipush 90
26: istore 5
28: iload 5
30: ireturn
Exception table:
from to target type
0 6 11 Class java/lang/ArithmeticException
0 6 22 any
11 24 22 any
finally中有异常
finally
异常会覆盖掉catch
的异常。这也是reentrantLock的lock.lock()应该放到try外面的原因。如果获取锁的过程中发生异常,未能加锁成功,但最后finally始终会去unlock,这时候就会抛出IMSE(IllegalMonitorStateException)非法监视器状态异常,就会覆盖掉获取锁过程中抛出的异常
public int foo() throws Exception
{
try {
int a = 5/0;
return 1;
}catch(ArithmeticException amExp) {
throw new Exception("aam");
}finally {
int b=90;
throw new Exception("ab");
}
}
public int foo() throws java.lang.Exception;
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=3, locals=6, args_size=1
0: iconst_5
1: iconst_0
2: idiv
3: istore_1
4: iconst_1
5: istore_2
6: bipush 90
8: istore_3
9: new #2 // class java/lang/Exception
12: dup
13: ldc #3 // String ab
15: invokespecial #4 // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
18: athrow
19: astore_1
20: new #2 // class java/lang/Exception
23: dup
24: ldc #6 // String aam
26: invokespecial #4 // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
29: athrow
30: astore 4
32: bipush 90
34: istore 5
36: new #2 // class java/lang/Exception
39: dup
40: ldc #3 // String ab
42: invokespecial #4 // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
45: athrow
Exception table:
from to target type
0 6 19 Class java/lang/ArithmeticException
0 6 30 any
19 32 30 any
由字节码可以看出
- 当
finally
中return
,退出栈帧的return
在try
语句结束就会出现,所以后续的异常处理都不会走 - finally中抛出异常,抛出异常的
athrow
指令在所有抛出异常方法之前就抛出,后续代码也不会再走。 - 捕获了异常,当我们没抛出。这时异常只是在局部变量表中存储着。
try-with-resource
jdk1.7开始引入try-with-resource将try-catch-finally 简化为 try-catch,在编译时会进行转化为 try-catch-finally 语句,我们就不需要在 finally块中手动关闭资源。在try-catch嵌套的代码可以确保正确的关闭资源。