JVM如何处理异常

异常处理的两大元素:抛出异常、捕获异常,非正常处理的两个方法。

抛出异常:显式异常与隐式异常

捕获异常则涉及到:try/catch/finally

try:异常代码块的监控

catch:捕获try可能产生的异常

finally:如果没有catch代码块,则在try之后异常;如果有cath代码块,则finally代码块则在catch代码块之后执行。

异常Throwable类,两个直接子类:Exception、Error。

除了Error与RuntimeException为非检查异常,其他异常都为检查异常,最大化地使用java编译器的功能。

 

在java中创建异常实例的代价特别大。当构建异常实例的时候,java虚拟机需要为异常实例构建一个栈轨迹。需要记录很多调试信息:包括栈帧所指向方法的名字、方法所在的类、第几行抛出的异常。在生成轨迹的时候,java虚拟机会忽略异常构造器、填充栈帧的java方法,直接在新建异常的位置开始。

那么我们是否可以缓存异常实例呢,在需要的时候将其抛出?上文说道,异常的栈轨迹是在新建异常的时候生成的,而不是在throw异常的时候生成。尽管缓存实例在语法角度上是OK的,但是很容易误导开发人员,定位到错误的地方,所以我们一般都是在抛出异常的地方新建异常。

 

java虚拟机是如何捕获异常的?

看下图的代码以及反编译的字节码

package com.yang.jvm.exception;

import java.io.*;

public class MyException {

    public static void main(String[] args) {
        try {
            Class.forName("com.yang.jvm.invokevirtual");
            File file = new File("");
            InputStream in = new FileInputStream(file);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

 

public com.yang.jvm.exception.MyException();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: ldc           #2                  // String com.yang.jvm.invokevirtual
         2: invokestatic  #3                  // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
         5: pop
         6: new           #4                  // class java/io/File
         9: dup
        10: ldc           #5                  // String
        12: invokespecial #6                  // Method java/io/File."<init>":(Ljava/lang/String;)V
        15: astore_1
        16: new           #7                  // class java/io/FileInputStream
        19: dup
        20: aload_1
        21: invokespecial #8                  // Method java/io/FileInputStream."<init>":(Ljava/io/File;)V
        24: astore_2
        25: goto          41
        28: astore_1
        29: aload_1
        30: invokevirtual #10                 // Method java/lang/ClassNotFoundException.printStackTrace:()V
        33: goto          41
        36: astore_1
        37: aload_1
        38: invokevirtual #12                 // Method java/io/FileNotFoundException.printStackTrace:()V
        41: return
      Exception table:
         from    to  target type
             0    25    28   Class java/lang/ClassNotFoundException
             0    25    36   Class java/io/FileNotFoundException

每个方法都会对应一个异常表,异常表里面会包含多个异常条目。每个异常条目是由:from指针、to指针、target指针以及target指向的类型构成。每个指针代表队的相应的字节码的索引。由from——to(不包括to)指针监控的范围即try的代码块的范围。从target指针对应的索引开始即异常处理器的开始位置。

从上图中我们可以看出。

from指针0,to指针25,对应的try代码块;

由于try代码块里面有两处捕获异常,一个是classNotfound,一个是fileNotFound异常,所以异常表里面有两个异常条目。

索引28和36对应的字节码分别是classNotfound异常处理器和fileNotFound异常处理器的开始位置,前者由33goto指令跳转至return,后者直接在41处return。

当java触发异常的时候,jvm会从异常条目表从上至下进行检查,首先判断抛出异常的字节码的索引是否在其监控范围之内,若匹配的话,则会检查抛出的异常类型与该异常条目指向的异常类型是否匹配,若匹配的话,jvm会将控制流转向该条目的target指针指向的索引的字节码。

以上为try与catch的字节码的执行过程。

但是如果有finally的话,则比较复杂。finally会被复制两份,分别放在try与catch正常执行的出口。第三份则用来监控,try触发异常,并未被catch捕获、catch抛出异常的监控。

package com.yang.jvm.exception;

public class MyException {
    public static  void main(String [] args){

        try {
            Class.forName("com.yang.test");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally{
            System.out.println("ending......");
        }
    }
}

以下为字节码:

public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String com.yang.test
       2: invokestatic  #3                  // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
       5: pop
       6: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: ldc           #5                  // String ending......
      11: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      14: goto          44
      17: astore_1
      18: aload_1
      19: invokevirtual #8                  // Method java/lang/ClassNotFoundException.printStackTrace:()V
      22: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      25: ldc           #5                  // String ending......
      27: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      30: goto          44
      33: astore_2
      34: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      37: ldc           #5                  // String ending......
      39: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      42: aload_2
      43: athrow
      44: return
    Exception table:
       from    to  target type
           0     6    17   Class java/lang/ClassNotFoundException
           0     6    33   any
          17    22    33   any

 从以上字节码中我们可以得到:

try正常执行后有一段finally代码块;

catch异常处理器后面有一段finally代码块;

finally自身有一段代码块。

异常表中的监控范围分别为:try中的classnotfoundtion、其他异常any;catch中可能抛出的异常。

 

java7中的Supressed异常与语法糖

Supressed异常可以将一个异常附于另一个异常上,所以finally中抛出异常的时候可以输出多个异常的信息;

不过java语言层面没有对异常的引用,所以使用起来比较复杂。

但是try-with-resources可以很好地在字节码层次自动使用了Supressed异常,不过主要目的是为了精简的关闭资源流因为只要我们的类实现了autocloseable接口,他们会重写其中的close方法,这样的话,我们就不需要关注流是否关闭了。

还有另外一个中语法糖:try中的所有检查类型可以全部放到一个catch代码块中,以“|”分割即可。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值