目录
一、异常在字节码层面的实现
示例:
public class ExceptionParse {
public static void main(String[] args) {
int a,b,c,d;
try {
a = 6;
} catch (Exception e) {
b = 7;
}finally{
c = 8;
}
d = 9;
}
}
反编译:
Classfile /D:/java/workspace/JDK/bin/com/lic/test/ExceptionParse.class
Last modified 2020-1-29; size 698 bytes
MD5 checksum 265e314f713616a5b32c5b7206658ec5
Compiled from "ExceptionParse.java"
public class com.lic.test.ExceptionParse
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
{
public com.lic.test.ExceptionParse();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lic/test/ExceptionParse;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=7, args_size=1
0: bipush 6 //将一个8位带符号整数(6)压入栈
2: istore_1 //将int类型值(6)存入局部变量1(a)
3: goto 25 //跳转到指令行号为25处,继续执行
6: astore 5 //栈顶数值出栈存入局部变量数组中指定下标5处的变量中(Exception对象的引用地址) 问题:为什么要先将栈顶元素存储在局部变量中, 因为在捕捉到异常后会将异常引用对象推入栈顶, 然后跳转到异常处理代码(catch部分)
8: bipush 7 //将一个8位带符号整数(7)压入栈
10: istore_2 //将int类型值(7)存入局部变量2(b)
11: bipush 8 //将一个8位带符号整数(8)压入栈
13: istore_3 //将int类型值(8)存入局部变量3(c)
14: goto 28 //跳转到指令行号为28,继续执行
17: astore 6 //栈顶数值出栈存入局部变量数组中指定下标6处的变量中(Exception对象的引用地址)
19: bipush 8 //将一个8位带符号整数(8)压入栈
21: istore_3 //将int类型值(8)存入局部变量3(c)
22: aload 6 //从局部变量6中装载引用类型值
24: athrow //抛出异常
25: bipush 8 //将一个8位带符号整数(8)压入栈
27: istore_3 //将int类型值(8)存入局部变量3(c)
28: bipush 9 //将一个8位带符号整数(9)压入栈
30: istore 4 //将int类型值(9)存入局部变量4(d)
32: return
Exception table: //异常表
from to target type //from: 异常捕获开始位置,即try的开始位置 to:异常捕获结束位置(不包括当前位置) target:异常处理器的开始位置, 即catch的开始位置 type:异常类型
0 3 6 Class java/lang/Exception //异常捕获范围为指令行号[0,3), 一旦捕获到异常则立刻跳转到指令行号为6处继续执行
0 11 17 any
LineNumberTable:
line 8: 0
line 9: 3
line 10: 8
line 12: 11
line 11: 17
line 12: 19
line 13: 22
line 12: 25
line 14: 28
line 15: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
11 6 2 b I
14 3 3 c I
22 3 3 c I
28 5 3 c I
32 1 4 d I
8 3 5 e Ljava/lang/Exception;
StackMapTable: number_of_entries = 4
frame_type = 70 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 74 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 7 /* same */
frame_type = 255 /* full_frame */
offset_delta = 2
locals = [ class "[Ljava/lang/String;", top, top, int ]
stack = []
}
SourceFile: "ExceptionParse.java"
程序指令分析:
异常表:
Exception table:
from to target type
0 3 6 Class java/lang/Exception
0 11 17 anyfrom: 异常捕获开始位置,即try的开始位置
to: 异常捕获结束位置(不包括当前位置)
target: 异常处理器的开始位置, 即catch的开始位置
type: 异常类型,
any
表示所有类型0 3 6 : 异常捕获范围为指令行号[0,3), 一旦捕获到异常则立刻跳转到指令行号为6处继续执行
1. 如果程序执行在指令范围为[0,3)中没有出现异常, 则在指令块1执行完成之后跳转到指令块4,5继续执行, 指令块4为finally语句块中的程序指令;
2. 如果程序执行在指令范围为[0,3)中出现异常, 按照异常表, 程序立刻跳转到指令行6处开始执行, 也就是执行指令块2, 在指令块2中包含了catch语句块和finally语句块中的程序指令; 语句块2执行完成之后跳转到指令行28开始继续执行, 也就是执行" d = 9; ", 执行之后返回;
3. 如果程序执行在指令范围为[0,11)中出现异常, 且该异常不能被定义的异常捕捉, 也就是在try-catch中出现未意料的异常; 那么按照异常表, 程序立刻跳转到指令行17处开始执行, 也就是执行指令块3, 在指令块3中, 主要是执行finally语句块中的内容; 该操作主要是为了保证finally语句块在任何情况下都可以执行到; 指令块3执行之后, 抛出异常;
异常处理的过程:
当程序触发异常时,Java虚拟机会遍历异常表中的所有条目(即try里面的所有代码)。如果异常找到异常发生的字节码条目,则会跟catch要捕捉的异常匹配,如果匹配,则开始执行catch里面的代码。
如果没有匹配到,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者(caller)中重复上述操作。在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表
如果在catch中发生了异常,那么Java虚拟机会抛弃第一个异常,尝试捕获并处理新的异常。这个在编码中是很不利于调试的。
小结:
- JVM 采用异常表的方式来处理 try catch 的跳转逻辑
- finally 的实现采用拷贝 finally 语句块的方式来实现 finally 一定会执行的语义逻辑
二、throw捕获异常解析
示例:
public static void main(String[] args) {
int a,b,c,d;
try{
a = 6;
}catch(Exception e){
b = 7;
throw e;
}finally{
c = 8;
}
d = 9;
}
反编译:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=7, args_size=1
0: bipush 6
2: istore_1
3: goto 22
6: astore 5 //当[0,3)出现异常后, 被jvm捕获后,跳转到6行开始执行, 并且会将异常对象引用压入操作数栈, 所以此处需要将异常对象引用存入局部变量5中
8: bipush 7
10: istore_2
11: aload 5 //将异常对象引用从局部变量5中加载到操作数栈中
13: athrow //抛出异常, 注意此处抛出异常后,该异常存在于[0,14)范围, 所以也将会被捕获, 然后从14行开始执行
14: astore 6 //先将异常对象引用存入局部变量6中
16: bipush 8 //执行finally块指令
18: istore_3
19: aload 6 //将异常对象引用从局部变量6中加载到操作数栈中
21: athrow //抛出异常, 程序执行结束!
22: bipush 8
24: istore_3
25: bipush 9
27: istore 4
29: return
Exception table:
from to target type
0 3 6 Class java/lang/Exception
0 14 14 any
为什么13行的athrow指令抛出的异常会被再次捕捉?
因为, 异常表第二行监控的范围是try-catch, 而我们在catch块中使用了throw, 将异常抛出了, 也就是catch块中出现了异常, 当然就会被捕捉到, 然后开始执行异常处理模块; 如果没有出现异常, 则在第3行执行之后, 跳转到22行执行finally块代码和try-catch-finally外的代码, 这样也就保证了finally块中的代码一定会执行
注意: 在第一示例中, 由于我们没有在catch块中使用throw, 所以jvm在编译时使用goto指令, 将执行控制权跳转到其他位置了
athrow执行过程:
1. 拿到栈顶的异常对象,
2. 从本帧开始,一个个去找帧对应方法的异常处理表, 标准是:
- 当前pc在异常表里面指定范围
[start,end)
里面(异常表记录try块的起止指令) - 异常对象的类型属于
type
target:
指向了catch块开始的pc位置
3. 如果找到了:
- 操作数栈清空
- 异常对象引用推入栈顶
- 跳转到异常处理代码
4. 这帧如果不是: 则把frame弹出,继续遍历; 帧都出完,都没找到匹配的方法异常处理表项, 说明根本没有对应的catch块存在, 只好打印出Java虚拟机栈信息
三、Java异常处理中的return和throw命令解析
如果把return和throw放在一起,直接会提示错误。"Unreachable statement"(无法被执行).
然而finally却可以成功骗过编译器让两者并存(是不是可以算是编译器的一个小bug呢),结果是后执行的会覆盖前者。
finally如果有return会覆盖catch里的throw,同样如果finally里有throw会覆盖catch里的return。进而如果catch里和finally都有return finally中的return会覆盖catch中的。throw也是如此。
这样就好理解一些了,retrun和throw都是使程序跳出当前的方法,自然就是冲突的。如果非要跳出两次那么后者会覆盖前者。
示例:
public class T {
public T() {
}
boolean testEx() throws Exception {
boolean ret = true;
try {
ret = testEx1();
} catch (Exception e) {
System.out.println("testEx, catch exception");
ret = false;
throw e;
} finally {
System.out.println("testEx, finally; return value=" + ret);
return ret;
}
}
boolean testEx1() throws Exception {
boolean ret = true;
try {
ret = testEx2();
if (!ret) {
return false;
}
System.out.println("testEx1, at the end of try");
return ret;
} catch (Exception e) {
System.out.println("testEx1, catch exception");
ret = false;
throw e;
}
finally {
System.out.println("testEx1, finally; return value=" + ret);
return ret;
}
}
boolean testEx2() throws Exception {
boolean ret = true;
try {
int b = 12;
int c;
for (int i = 2; i >= -2; i--) {
c = b / i;
System.out.println("i=" + i);
}
return true;
} catch (Exception e) {
System.out.println("testEx2, catch exception");
ret = false;
throw e;
}
finally {
System.out.println("testEx2, finally; return value=" + ret);
//return ret;
}
}
public static void main(String[] args) {
T testException1 = new T();
try {
testException1.testEx();
} catch (Exception e) {
e.printStackTrace();
}
}
}
控制台输出:
i=2
i=1
testEx2, catch exception
testEx2, finally; return value=false
testEx1, catch exception
testEx1, finally; return value=false
testEx, finally; return value=false (为什么在testEx()方法中的catch块没有捕捉到异常?)
try-catch-finally程序块的执行流程:
首先执行的是try语句块中的语句,这时可能会有以下三种情况:
1. 如果try块中所有语句正常执行完毕,那么finally块的居于就会被执行,这时分为以下两种情况:
1.1 如果finally块执行顺利,那么整个try-catch-finally程序块正常完成。
1.2 如果finally块由于原因R突然中止,那么try-catch-finally程序块的结局是“由于原因R突然中止(completes abruptly)”
2. 如果try语句块在执行过程中碰到异常V,这时又分为两种情况进行处理:
2.1 如果异常V能够被与try相应的catch块catch到,那么第一个catch到这个异常的catch块(也是离try最近的一个与异常V匹配的catch块)将被执行;这时就会有两种执行结果:
2.1.1 如果catch块执行正常,那么finally块将会被执行,这时分为两种情况:
(1) 如果finally块执行顺利,那么整个try-catch-finally程序块正常完成。
(2) 如果finally块由于原因R突然中止,那么try-catch-finally程序块的结局是“由于原因R突然中止(completes abruptly)”
2.1.2 如果catch块由于原因R突然中止,那么finally模块将被执行,分为两种情况:
(1) 如果如果finally块执行顺利,那么整个try-catch-finally程序块的结局是“由于原因R突然中止(completes abruptly)”。
(2) 如果finally块由于原因S突然中止,那么整个try-catch-finally程序块的结局是“由于原因S突然中止(completes abruptly)”,原因R将被抛弃。
虽然我们在testEx2中使用throw e抛出了异常,但是由于testEx2中有finally块,而finally块的执行结果是complete abruptly的。因为return也是一种导致complete abruptly的原因之一,所以整个try-catch-finally程序块的结果是“complete abruptly”,所以在testEx1中调用testEx2时是捕捉不到testEx1中抛出的那个异常的,而只能将finally中的return 结果获取到。当然这种情况是可以避免的,以testEx2为例:如果你一定要使用finally而且又要将catch中 throw的e在testEx1中被捕获到,那么你去掉testEx2中的finally中的return就可以了。
如果将testEx2()中的
finally {
System.out.println("testEx2, finally; return value=" + ret);
//return ret;
}
修改为:
finally {
System.out.println("testEx2, finally; return value=" + ret);
return ret;
}
程序运行结果:
i=2
i=1
testEx2, catch exception
testEx2, finally; return value=false
testEx1, finally; return value=false
testEx, finally; return value=false
try-catch-finally程序块中的return
从上面的try-catch-finally程序块的执行流程以及执行结果一节中可以看出无论try或catch中发生了什么情况,finally都是会被执行的,那么写在try或者catch中的return语句也就不会真正的从该函数中跳出了,它的作用在这种情况下就变成了将控制权(语句流程)转到 finally块中;这种情况下一定要注意返回值的处理。
例如,在try或者catch中return false了,而在finally中又return true,那么这种情况下不要期待你的try或者catch中的return false的返回值false被上级调用函数获取到,上级调用函数能够获取到的只是finally中的返回值,因为try或者catch中的return 语句只是转移控制权的作用。
那么, 为什么上级调用函数能够获取到的只是finally中的返回值, 为什么finally里的return语句会覆盖掉try 或者 catch 语句里的返回语句?
在上面提到过, 如果finally语句块中存在return语句, 那么try或者catch中的return语句将被JVM编译为跳转(goto), 而不是返回(return); 下面来分析这种情况
程序示例1:
public int test(){
int a;
try{
a = 1;
return a;
}catch(Exception e){
a = 2;
return a;
}finally{
a = 3;
return a;
}
}
编译后:
public int test();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: iconst_1
1: istore_1
2: goto 12 //try语句块中的return命令编译为goto,跳转到12行开始执行finally语句块
5: astore_2
6: iconst_2
7: istore_1
8: goto 12 //catch语句块中的return命令编译为goto,跳转到12行开始执行finally语句块
11: pop
12: iconst_3 //由于指令无论是否出现异常都将执行到此处, 所以将finally块内容编译在此处
13: istore_1
14: iload_1
15: ireturn
Exception table:
from to target type
0 5 5 Class java/lang/Exception
0 11 11 any
程序示例2 (去掉实例1finally块中的return语句):
public int test(){
int a;
try{
a = 1;
return a;
}catch(Exception e){
a = 2;
return a;
}finally{
a = 3;
}
}
编译后:
public int test();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: istore 4 //将栈顶元素(a=1)存入局部变量4中, 先去执行finally块中命令
5: iconst_3 //finally语句块内容复制一份放在try语句块内容之后,如果没有异常, 保证finally语句可以被执行
6: istore_1
7: iload 4 //finally块中命令执行完成后, 加载局部变量4中数值(a=1)出来返回
9: ireturn //try语句块中的return命令编译为ireturn, 返回int类型数值
10: astore_2
11: iconst_2
12: istore_1
13: iload_1
14: istore 4 //将栈顶元素(a=2)存入局部变量4中, 先去执行finally块中命令
16: iconst_3 //finally语句块内容复制一份放在try语句块内容之后,如果出现异常, 保证finally语句也可以被执行
17: istore_1
18: iload 4 //finally块中命令执行完成后, 加载局部变量4中数值(a=2)出来返回
20: ireturn //catch语句块中的return命令编译为ireturn, 返回int类型数值
21: astore_3
22: iconst_3
23: istore_1
24: aload_3
25: athrow
Exception table:
from to target type
0 5 10 Class java/lang/Exception
0 5 21 any
10 16 21 any //保证在try-catch块中出现异常也可以被捕获
由此可知, 对于finally块中是否存在return命令, jvm采用两种不同的编译方式; 如果finally块中存在return命令, jvm将try-catch快中的return命令编译为goto, 达到跳转作用, 而不是返回, 一旦finally块出现return也就意味着try-catch块中的return命令失效;
参考播客: Java异常详解-从字节码角度查看异常实现原理