Exception 和 Error 都是继承了 Throwable类,只有 Throwable 类型的实例才可以被抛出 throw 或者被捕获 catch,它是异常处理机制的基本组成类型。
Error 是在正常情况下,不大可能出现的情况,绝大部分Error都会导致程序处于非正常状态、不可恢复状态,不需要捕获,如 OutOfMemoryError 之类,都是 Error 的子类。
Exception 可以分 检查型(check)异常 和 非检查型(unchecked)异常。
JVM采用异常表(Exception table)的方式来对异常进行处理,存放处理异常的信息,每个exception_table表,是由start_pc、end_pc、hangder_pc、catch_type组成
package com.lwj.bytecode; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; public class Test3 { public void test() { try { InputStream is = new FileInputStream("test.txt"); ServerSocket serverSocket = new ServerSocket(9999); serverSocket.accept(); } catch (FileNotFoundException e) { int i = 0; } catch (IOException e) { int i = 1; } catch (Exception e) { int i = 2; } finally { System.out.println("finally"); } } }
对应的反编译结果:
$ javap -v -c Test3.class Classfile /D:/Repository/Framework/JavaVirtualMachine/jvm/com/lwj/bytecode/Test3.class Last modified 2020-3-30; size 756 bytes MD5 checksum 5150b854f4ad80e98583822103ad4ac1 Compiled from "Test3.java" public class com.lwj.bytecode.Test3 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: // 省略了常量池 { // 省略构造方法 public void test(); descriptor: ()V flags: ACC_PUBLIC Code: /* * 对于 Java 类中的每一个实例方法(非 static 方法),其在编译后所生成的字节码中, * 方法参数的数量总是比源代码中方法参数的数量多一个(即为 this),它位于方法的第一个参数位置处。 * 这样我们就可以在 Java 的实例方法中使用 this 来访问当前对象的属性以及其他方法; * 该操作在编译期间完成的,即在 Javac 编译器在编译的时候将对 this 的访问转化为对一个普通实例方法的访问, * 接下来在运行期间,由 JVM 在实例方法时候,自动的向实例方法传入该 this 参数, * 所以在实例方法的局部变量表中,至少会有一个执行当前对象的局部变量。 */
//堆栈上最多存3个对象,4个局部变量,有1个参数 stack=3, locals=4, args_size=1 // 创建对象,这里就是创建了一个 FileInputStream 对象 0: new #2 // class java/io/FileInputStream // dup:复制栈顶数值并将复制值压入栈顶,相当于压栈 3: dup // ldc:从运行期的常量池中推一个 item,就是将常量池中的 test.txt 推进去,使其能构造出该对象 4: ldc #3 // String test.txt // 调用父类的相应构造方法 6: invokespecial #4 // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V // 将应用存储到一个局部变量中,就是将 FileInputStream 创建处理实例的引用存储到局部变量 is 中, // astore_1 中 a 代表操作一个引用 _1 代表存放到局部变量表中的索引为1的位置,0索引位置一般存放 this引用 9: astore_1 10: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; // 可以看出下面这部是 finally 中的内容,也就是try中的代码没有发生异常,会正常走到 finally中 13: ldc #6 // String finally 15: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V // 因为可能报错,因此真正的执行顺序在运行期才能确定,在编译期只能使用 goto 语句做可能的跳转, // 这里是try中没有发生异常,直接进行返回 18: goto 74 // 正常情况下是无法走到这里的,但是如果发生异常 // JVM会根据 Exception table(异常表)中进行相应的指令跳转到这里 21: astore_1 // 将int型0推送至栈顶,结合代码可以看到这里是捕获到 FileNotFoundException 异常的处理代码 22: iconst_0 23: istore_2 24: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
27: ldc #6 // String finally 29: invokevirtual #7 32: goto 74 35: astore_1 // 将int型1推送至栈顶,结合代码可以看到这里是捕获到 IOException 异常的处理代码 36: iconst_1 37: istore_2 38: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 41: ldc #6 // String finally 43: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 46: goto 74 // 可以看到 PC 21 36 49 都将捕获的变量保存到局部变量表中index为1的位置, // 因为这三部分在运行时只会走其中的一个分支, 49: astore_1 // 将int型2推送至栈顶,结合代码可以看到这里是捕获到 Exception 异常的处理代码 50: iconst_2 51: istore_2 52: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 55: ldc #6 // String finally 57: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 60: goto 74 63: astore_3 64: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 67: ldc #6 // String finally 69: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 72: aload_3 73: athrow
74: return Exception table: from to target type //从 0到10 之间的指令如果发生了FileNotFoundException 会跳转到21继续执行 0 10 21 Class java/io/FileNotFoundException 0 10 35 Class java/io/IOException 0 10 49 Class java/lang/Exception 0 10 63 any 21 24 63 any 35 38 63 any 49 52 63 any //省略行号表 } SourceFile: "Test3.java"
public void test0() throws FileNotFoundException { }
对于返回变量是基本类型:
public int test3() { int i; try { i = 1; return i; } finally { i = 2; } } bytecode: public int test3(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=4, args_size=1 0: iconst_1 // 将int型1推送至栈顶 1: istore_1 // 将栈顶int型数值存入第二个本地变量 2: iload_1 // 这里准备返回值,将第二个int型本地变量推送至栈顶 3: istore_2 // 由于还有finally块中的语句,所以没有直接返回, // 而是复制一份保存到第三个本地变量 4: iconst_2 // 执行finally块中的语句,将int型2推送至栈顶 5: istore_1 // 这里将第保存在第二个本地变量的i进行了修改 6: iload_2 // 返回第三个本地变量元素,也就是之前保存的1 7: ireturn // 返回一个int
// 这里是发生了异常的情况 8: astore_3 // 将捕获到的异常引用,保存在第四个本地变量中 9: iconst_2 // 执行finally块中的内容 10: istore_1 11: aload_3 // 将捕获到的异常引用推送至栈顶 // 也就是将第四个引用类型本地变量推送至栈顶 12: athrow // 将栈顶的异常抛出 Exception table: from to target type 0 4 8 any
结论:finally块 中修改基本类型的返回变量不会对其有影响。
对于返回变量是引用类型:
private static class Student { private int age; //constructor(ing age) //get() set() } public Student test5() { Student student = null; try { student = new Student(1); return student; } finally { student.setAge(2); } } bytecode: public com.lwj.bytecode.Test2$Student test5(); descriptor: ()Lcom/lwj/bytecode/Test2$Student; flags: ACC_PUBLIC Code: stack=3, locals=4, args_size=1 0: aconst_null 1: astore_1 2: new #25 // class com/lwj/bytecode/Test2$Student 5: dup 6: iconst_1
7: invokespecial #26 // Method com/lwj/bytecode/Test2$Student."<init>":(I)V 10: astore_1 // 完成初始化,并将student引用保存到局部变量表中第二个位置 11: aload_1 12: astore_2 // 将student引用复制到第三个位置,两个引用指向堆中同一个对象 13: aload_1 // 加载的是第二个位置的student引用 14: iconst_2 15: invokevirtual #27 // Method com/lwj/bytecode/Test2$Student.setAge:(I)V 18: aload_2 19: areturn // 返回第三个位置的引用,但对象已经通过第二个位置的引用被修改 // 下面是try中发生异常执行的指令 20: astore_3 21: aload_1 22: iconst_2 23: invokevirtual #27 // Method com/lwj/bytecode/Test2$Student.setAge:(I)V 26: aload_3 27: athrow Exception table: from to target type 2 13 20 any
结论:由于引用的特殊,在finally块 中修改引用类型的返回值会对其有影响,这里就像方法传参中的值传递和引用传递一样。
private static class Connection implements AutoCloseable { void sendData() throws Exception { throw new Exception("sendData() exception "); } public void close() throws Exception { throw new Exception("close() exception "); } } public static void main(String[] args) { try { Connection connection = new Connection(); try { // ... 其他的业务逻辑 connection.sendData(); } finally { connection.close(); } } catch (Exception e) { e.printStackTrace(); // 查看最终的异常信息 } } java.lang.Exception: call close exception at _12_异常处理.Test1$Connection.close(Test1.java:33) at _12_异常处理.Test1.main(Test1.java:43)
sendData()
和 conn.close()
都发生了错误,在 finally块 中首先会将 try块 抛出的异常保存到局部变量表中,然后执行自己的逻辑,如果在执行过程中没有发生异常,则会将之前保存的异常,从局部变量表加载到操作数栈顶然后抛出,以上是正常关闭资源的情况,如果关闭资源发生异常,也就是finally块 中的代码出错,则会重新抛出位于操作数栈顶的 新异常,看起来就像是新异常将老异常覆盖了,因此我们无法得知问题的根源。
要解决这个问题也很简单,只需要将新老异常关联起来即可,可以使用Throwable接口中的 initCause(Throwable exception)或者 addSuppressed(Throwable exception)。
下面的简化版代码就实现了 将新老异常关联起来抛出。
public static void main(String[] args) { try { Connection conn = new Connection(); Throwable exception = null; try { conn.sendData(); } catch (Throwable serviceException) { exception = serviceException; // finally块 中的修改代码不会影响返回的基本类型,但可以影响引用类型的内容 // serviceException 的内容可以在finally中被修改。但引用指向不会变化 throw serviceException; } finally { try { conn.close(); } catch (Throwable IOException) { // initCause() 和 addSuppressed() 的输出结果有所不同 // exception.initCause(IOException); exception.addSuppressed(IOException); } } } catch (Exception e) { e.printStackTrace(); // 查看最终的异常信息 } }
// initCause() java.lang.Exception: sendData() exception at _12_异常处理.Test1$Connection.sendData(Test1.java:27) at _12_异常处理.Test1.main(Test1.java:64) Caused by: java.lang.Exception: close() exception at _12_异常处理.Test1$Connection.close(Test1.java:31) at _12_异常处理.Test1.main(Test1.java:72) // addSuppressed() java.lang.Exception: sendData() exception at _12_异常处理.Test1$Connection.sendData(Test1.java:27) at _12_异常处理.Test1.main(Test1.java:64) Suppressed: java.lang.Exception: close() exception at _12_异常处理.Test1$Connection.close(Test1.java:31) at _12_异常处理.Test1.main(Test1.java:72)
虽然我们解决了异常屏蔽的问题,但是在处理资源关闭上有大量臃肿的代码,不过随着Java语言的发展,这一问题已经很好的被解决,在Java 1.7中新增的语法糖 try-with-resources
,我们用可它打开资源,而无需手动资源关闭代码,为了配合try-with-resources,资源必须实现 AutoClosable 接口,底层还是在编译器做的优化,帮我们自动生成了finally块,并在里边调用了close()方法。并且使用了 addSuppressed
来解决之前异常屏蔽的问题。
public static void main(String[] args) { try { // try-with-resources 方式 不仅美观,美观而且简洁 try (Connection connection = new Connection()) { // ... 其他的业务逻辑 connection.sendData(); } } catch (Exception e) { e.printStackTrace(); } } java.lang.Exception: sendData() exception at _12_异常处理.Test1$Connection.sendData(Test1.java:29) at _12_异常处理.Test1.main(Test1.java:40) Suppressed: java.lang.Exception: close() exception at _12_异常处理.Test1$Connection.close(Test1.java:33) at _12_异常处理.Test1.main(Test1.java:41)
我们可以通过反编译查看 使用 try-with-resources 时 ,编译器生成的 finally块:
public static void main(String[] var0) { try { Test1.Connection var1 = new Test1.Connection(); Throwable var2 = null; try { var1.sendData(); } catch (Throwable var12) { var2 = var12; throw var12; } finally { if (var1 != null) { if (var2 != null) { try { var1.close(); } catch (Throwable var11) { var2.addSuppressed(var11); //here } } else { var1.close(); } } } } catch (Exception var14) { var14.printStackTrace(); } }
尽量不要捕获类似 Exception 这样的通用异常,而应该捕获特定异常,保证程序不会捕获到我们不希望捕获的异常。
错误示例:
try{ // 业务代码 }catch(Exception e){ // ... }
不要生吞异常。这里的生吞是指输出到标准错误流,正确的做法应该是输出到日志系统里。
try{ //业务代码 }catch(IOException e){ e.printStackTrace(); }
throw early ,catch late。 尽量在第一时间暴露问题,捕获异常后,切勿 ”生吞异常“ ,不知道如何处理可以保留原有异常信息,直接再抛出或构建新异常跑出去,在更高层面有了清晰的逻辑,会更清楚处理方式。
public void throwEarly(String fileName){ Objects.requireNonNull(fileName); //throw early otherFun(fileName); }
Reference
《Java核心技术36讲》