异常处理背后的JVM原理

Posted by     "Eric" on Friday, March 6, 2020

异常处理背后的JVM原理

1. 异常分类

Java体系中异常的组织分类如下图所示,所有异常类型的根类为 Throwable,具体包括两大类:Error 与 Exception。Error是指程序无法处理的错误,表示运行应用程序中较严重的问题;Exception是指程序本身可以处理的错误,具体可分为运行时异常(派生于RuntimeException)和其他异常。

派生于RuntimeException的异常包含下面几种情况:

  • 错误的类型转换
  • 数组访问越界
  • 访问null指针

“如果出现了RuntimeException异常,那一定是你的问题”,例如访问数组前一定要先检测数组下标是否越界;在使用变量之前检测是否为null来杜绝NullPointerException异常的发生。

Error和RuntimeException是不受检查的异常,其他的所有异常都是受检查的异常。

异常-52.7kB

2. 异常处理在JVM中的体现

public class Demo {
    public static void main(String[] args) {
        int i=0;
        try {
            i=10;
        } catch (Exception e) {
            //TODO: handle exception
            i=20;
        }
    }
}

字节码反编译后

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: goto          12
         8: astore_2
         9: bipush        20
        11: istore_1
        12: return
      Exception table:
         from    to  target type
             2     5     8   Class java/lang/Exception
		LineNumberTable:
        line 6: 0
        line 8: 2
        line 12: 5
        line 9: 8
        line 11: 9
        line 13: 12
      StackMapTable: number_of_entries = 2
        	  frame_type = 255 /* full_frame */
          offset_delta = 8
          locals = [ class "[Ljava/lang/String;", int ]
          stack = [ class java/lang/Exception ]
          frame_type = 3 /* same */

Exception table中的一列表示try语句的范围,即2-4行(不包括第五行),如果发生的异常与Exception一致,则进入第8行。第8行的astore_2的含义是将异常对象的引用地址储存在e槽位上。

public class Demo {
    public static void main(String[] args) {
        int i=0;
        try {
            i=10;
        } catch (Exception e) {
            i=20;
        }finally{
            i=30;
        }
    }
}

字节码反编译后

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: bipush        30
         7: istore_1
         8: goto          27
        11: astore_2
        12: bipush        20
        14: istore_1
        15: bipush        30
        17: istore_1
        18: goto          27
        21: astore_3
        22: bipush        30
        24: istore_1
        25: aload_3
        26: athrow
        27: return
      Exception table:
         from    to  target type
             2     5    11   Class java/lang/Exception
             2     5    21   any
            11    15    21   any
		LocalVariableTable:
			Start   Length	Slot	Name	Signature
			   12        3     2      e   Ljava/lang/Exception;
				0        28     0    args [Ljava/lang/String;
				2        26     1       i I

finally中的代码被复制了三份,分别放入try流程,catch流程以及catch剩余异常类型流程

3. finally的执行时机

1. finally子句应该是在控制转移语句之前执行

public class Test { 
    public static void main(String[] args) { 
        System.out.println("return value of getValue(): " + getValue()); 
    } 
    public static int getValue() { 
        try { 
                return 0; 
        } finally { 
                return 1; 
            } 
     } 
 }
/* Output:
        return value of getValue(): 1
 *///:~ 

try块被执行,其finally子句一定会被执行,因此在return之前,会执行finally块中的return 1指令,自然后返回1;

2. finally对返回值影响

public class Test{
    public static void main(String[] args){
        int result=test();
        System.out.println(result);
    }
    
    public static int test(){
        int i=10;
        try{
            return i;
        }finally{
            i=20;
        }
    }
}
// Output: print 10;

3b6D2Q.png

虽然在返回i之前,会首先执行finally子句块中的i=20这一步,但是在执行finally这一步之前,会将数据保存在本地变量表中,执行完finally块后,再将其从变量表中取出并返回。因此,尽管finally中改变了i的值,但是返回的是i的一个拷贝