深入Java异常 详细解析JVM异常原理

概述

异常是什么?

异常:程序运行时不在预期范围内的事情都可以称作为异常;异常阻止程序按照期望正常运行。

异常如果捕获与处理?

捕获异常可以使用if对预期值进行判断后输出异常码,在C++ C# JAVA 等语言中可以使用try...catch语法进行捕获与处理

C语言处理程序异常

在C语言中捕获与处理异常比较麻烦,C语言本身没有提供异常的处理机制;下面举例一种通俗异懂的方式:

int main(){
    int status=0;
  //预期值为0
  if(status==1){
    //输出或者处理异常信息
  }else{
    //处理正常的逻辑
  }
}

上面是一段C语言精典的异常处理方式if判断预期值;不符合预期进输出或者处理异常信息;这样的方式在大多数编程语言中都是能用的。

但这样的方式处理会有一个很大的缺点,试想如果业务逻辑复杂的情况下,需要处理的预期判断也就会增多;加上正常的业务逻辑判断;最后程序会成为一个if炸弹,在不知什么时候就爆炸了;这样的形式对于调用者也是不友好的,调用者需要看每个方法的使用文档才能知道被调用的程序需要什么样的预期值;返回什么样的状态码是异常的,每个异常码又代表什么意思。

对于程序的调试也是一大挑战;在异常处理只输出错误提示与状态码时并不知道程序当前的调用栈信息,如果有重复的错误码;这个工作很难做下去。

在OOP的时代,是否可以使用OOP三大精髓封装 继承 多态来解决上面的这些问题呢?

答案是可以的,现在大部分OOP语言都这样做了,有很好的异常处理机制;在Java中就可以使用try...catch...finally的语法来处理异常,抛出异常可以使用thow,指示方法会有那些异常可以使用thows;使用java.lang.Throwable封装异常状态码、异常描述、堆栈信息;

Java异常捕获与处理

try...catch...finally语法示例:

public void demoTryCatch(){
  InputStream in;
  try{
    in= new FileInputStream(new File("./test.txt"));
    //使用除数为0的错误使程序抛出异常信息
    int d=1/0;
    in.close();
  }catch (Throwable e){
	//在catch中捕获除数为0的异常并输出异常堆栈信息
    e.printStackTrace();
  }finally{
    //任何时候都会执行的块
    System.out.println("finally");
    if(in != null){
      //关闭I/O
      in.close();
    }
  }
}

示例中1/0的算式在Java中会自动的抛出ArithmeticException异常;在catch中捕获Throwable类型的异常并在代码块中进行处理;Java中所有的异常都继承处Throwable类型,上述示例中使用Throwable类型进行捕获时捕获到Java中的所有异常;Java在异常处理块中还提供了finally关键字用于程序在发生异常时做一些必要的补救,finally关键字的代码块任何时候都会执行,包括在try catch代码块中使用return返回函数结果的情况。

例如在操作I/O时,在try中打开了I/O后计算1/0的值时出现了异常,这时会执行catch块中的代码;如果没有finally块的话I/O就不会被关闭;这就会导致内存泄漏等风险最后导致系统内存被占用完毕后导致程序非正常退出;这样的情况不是我们想要结果,这时就可以使用finally代码块来处理这样的情况,在finally块中对I/O进行关闭的操作。

Throw 与Throws 关键字

在上面的示例中的异常触发点自己做判断后抛出自定义的异常;那需要怎样将将这个异常抛出呢?做法如下示例:

public void demoTryCatch() throws Exception{
  InputStream in;
  try{
    in= new FileInputStream(new File("./test.txt"));
    int i=0;
    //使用除数为0的错误使程序抛出异常信息
    if(i==0){
      throw new new Exception("无效的因子");
    }
    int d=1/i;
    in.close();
  }catch (Throwable e){
	//在catch中捕获除数为0的异常并输出异常堆栈信息
    e.printStackTrace();
  }finally{
    //任何时候都会执行的块
    System.out.println("finally");
    if(in != null){
      //关闭I/O
      in.close();
    }
  }
}

做除法运算前先使用if判断被除数是不是为0;如果为0将抛出Exception异常,并在异常中写入异常描述信息无效的因子Exception异常类型在java中是检查类异常;所以需要提示调用者捕获可能出现的异常Exception;这个提示就可以在方法后面使用throws进行标示,多个异常时使用分隔;非检查异常也可以写在throws关键字后面;对于检查异常在编译时会输出错误提示使编译不通过。

Java通过throw抛出异常;使用throws标示方法可能出现的异常;对于检查异常在没有进行捕获处理时在编译阶段给出错误的提示。通过这样的形式来避免部分人为因素导致程序崩溃。

Java 检查异常 运行时异常

Java基本的异常类类图:

image-20210511002332993

Throwable Java中所有的异常类都继承自它,角色在异常的领域中和Object一样;需要捕获所有异常时可以使用catch(Throwable e)进行捕获

Error 它表示不希望被程序捕获或者是程序无法处理的错误。它的子类常用有OutOfMemoryError StackOverFlowError

Exception 它表示程序可能捕捉的异常或者程序可以处理的异常,Exception为检查异常;方法使用throws标示Exception的异常后,调用者需要明确处理方法;

RuntimeException 运行时异常,它是Exception的子类;是一个特殊的检查异常,调用者可以忽略不处理这个异常,交给上面的调用者或者全局进行捕获处理;继承自RuntimeException的异常可以不用throws关键字在方法上进行标示也能通过编译,但通常不建议这样做。

Java异常捕获原理分析

上面讲了Java异常捕获的各种使用方法、异常的类型及作用,但Java是怎样处理异常的呢?发生异常又是怎样保证finally块的代码一定会被调用呢?异常信息在Java中又存储在那呢?

下面通过Java编译后生成的字节码来分析Java异常的处理过程(字节码看不懂没有关系,搞清楚思想就可以)

public static void main(String[] args) {
  try {
    System.out.println("enter try block");
  } catch (Exception e) {
    System.out.println("enter catch block");
  }finally{
    System.out.println("enter finally block");
  }
}

通过在编译器中进行编码后使用jclasslib 工具查看上面代码的字节码,当然也可以使用javap -c <类名>进行查看,主方法的字节码如下图:

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String enter try block
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      11: ldc           #5                  // String enter finally block
      13: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      16: goto          50
      19: astore_1
      20: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      23: ldc           #7                  // String enter catch block
      25: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      28: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      31: ldc           #5                  // String enter finally block
      33: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      36: goto          50
      39: astore_2
      40: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      43: ldc           #5                  // String enter finally block
      45: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      48: aload_2
      49: athrow
      50: return
    Exception table:
       from    to  target type
           0     8    19   Class java/lang/Exception
           0     8    39   any
          19    28    39   any
}

关键指令解析:

  • ldc: 从常量池加载对应的常量到操作数栈
  • goto: 跳转到指定的行
  • athrow:抛出栈顶的异常
  • dup:将本地变量压入栈顶
  • astore_n:将栈顶的引用类型变量放入本地变量表
  • aload_n:将本地变量压入栈顶

*下面使用’@N’代表指向上面字节码中的行

根据程序的逻辑在@3的命令ldc代表进入try代码块,@11指令代表进入finally代码块;@36行goto指令将跳转到@50行,再执行50行的return指令跳出函数;@23行进入catch异常处理块,后面的@31 @43分别为finally的代码块;所有代码块中执行的指令都是同一块。

通过上面类文件的字节码文件简单的阅读可以发现Java在try catch 块后面都会插入finally块的代码来保证finally块能被执行到;学习到这你可能会想:如果在catch块或者finally块中再次发生异常;Java又如果处理呢?

public static void main(String[] args) throws NullPointerException {
	try {
    System.out.println("enter try block");
    return;
  } catch (Exception e) {
    System.out.println("enter catch block");
    throw new NullPointerException();
  } finally {
    System.out.println("enter finally block");
    throw new NullPointerException();
  }
}

字节码:

public static void main(java.lang.String[]) throws java.lang.NullPointerException;
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String enter try block
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      11: ldc           #5                  // String enter finally block
      13: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      16: new           #6                  // class java/lang/NullPointerException
      19: dup
      20: invokespecial #7                  // Method java/lang/NullPointerException."<init>":()V
      23: athrow
      24: astore_1
      25: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      28: ldc           #9                  // String enter catch block
      30: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      33: new           #6                  // class java/lang/NullPointerException
      36: dup
      37: invokespecial #7                  // Method java/lang/NullPointerException."<init>":()V
      40: athrow
      41: astore_2
      42: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      45: ldc           #5                  // String enter finally block
      47: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      50: new           #6                  // class java/lang/NullPointerException
      53: dup
      54: invokespecial #7                  // Method java/lang/NullPointerException."<init>":()V
      57: athrow
    Exception table:
       from    to  target type
           0     8    24   Class java/lang/Exception
           0     8    41   any
          24    42    41   any
}

与上面的代码的代码对比在catch块多了抛出异常,在finally块也添加了抛出异常。在字节码中@11~@23行间多出了几行:@16行使用关键字new创建异常NullPointException,@19行使用指令dup将新创建的变量入栈;@20调用NullPointExceptioninit方法初始化实例;@23行中使用athrow指令抛出异常;再看看@28~@40行也同样多了这些指令。

Exception table,看了上面两字节码的代码;在最后都有一个Exception table;根据命名不难理解,它就是传说中的异常表作用是标示出代码中的那些行出现异常后跳转到那行执行,下面解析下Exception table中各列的表示什么意思

  • from: 监听异常的起点
  • to: 监听异常的终点,不包含标示的本行
  • target:发生异常后跳转执行的行
  • type:监听的异常类型,any表示任何异常

Exception table的每一行表示一个监听块,如上的第一行表示从字节码的第一行开始监听到第八行,监听的异常类型为Class java.lang.Exception;出现异常时跳转至字节码24行开始执行。

下面来一一回顾并解答上面提的问题:

  • Java是怎样处理异常的?
    • 从字节码可以看出,Java使用Exception table在类文件中标示监听的异常块,通过Exception table中的type列进行异常类型的匹配,匹配时跳转到target列指定的行继续执行。
  • 发生异常是怎样保证finally块的代码一定会被调用?
    • Java通过在try catch代码块的后面加入finally块的代码,在最后return指令前加入finally块,再配合Exception tabletarget的方式来保证finally块的调用
  • 异常信息在Java中又存储在那?
    • 在Java中异常也被包装成一种;异常发生后会将异常类的实例压入栈顶;再通过Exception table匹配跳转的行执行;在catch块执行前通过astore指令将异常信息写入本地变量(赋值给e这个变量)。这样分析下来异常是引用变量,异常的实例存储在堆中;在栈上存储引用(可以用C中的指针进行理解)。

throws 与 catch的顺序

分析完上面的问题后再来看看throws,异常是如果向上传递以及多个catch块执行顺序的分析

public static void main(String[] args) throws NullPointerException {
  test();
}

public static void test() throws NullPointerException {
  try {
    System.out.println("enter try block");
    throw new NullPointerException();
  } catch (NullPointerException ne) {
    System.out.println("enter catch block, NullPointerException");
  } catch (Exception e) {
    System.out.println("enter catch block");
    throw new Exception();
  } finally {
    System.out.println("enter finally block");
  }
}
public static void main(java.lang.String[]) throws java.lang.NullPointerException;    Code:       0: invokestatic  #2                  // Method test:()V       3: return  public static void test() throws java.lang.NullPointerException;    Code:       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;       3: ldc           #4                  // String enter try block       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V       8: new           #6                  // class java/lang/NullPointerException      11: dup      12: invokespecial #7                  // Method java/lang/NullPointerException."<init>":()V      15: athrow      16: astore_0      17: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;      20: ldc           #8                  // String enter catch block, NullPointerException      22: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V      25: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;      28: ldc           #9                  // String enter finally block      30: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V      33: goto          64      36: astore_0      37: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;      40: ldc           #11                 // String enter catch block      42: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V      45: new           #6                  // class java/lang/NullPointerException      48: dup      49: invokespecial #7                  // Method java/lang/NullPointerException."<init>":()V      52: athrow      53: astore_1      54: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;      57: ldc           #9                  // String enter finally block      59: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V      62: aload_1      63: athrow      64: return    Exception table:       from    to  target type           0    16    16   Class java/lang/NullPointerException           0    16    36   Class java/lang/Exception           0    25    53   any          36    54    53   any

示例代码中添加了test方法,方法上增加了throws NullPointerException,增加了两个catch块;用这份新的字节码和上面的字节码对比在test方法后面增加了throws;在Exception table中增加了一行用于捕获NullPointerException异常。

try中增加了NullPointerException抛出异常的代码;在字节码中对应@8~@15行;在Exception table中也的前两项也包含了try这段代码块的异常检查。

catch的检查顺序也是按Exception table中的顺序进行匹配;如果出现的异常不是NullPointerException Exception这两个类型时将会被第三行的any进行捕获后跳转至@53行执行:

  1. @53行astore_1将栈中的变量放入局部变量中,腾出栈中的位置
  2. @54~@59行执行finally中的输出语句
  3. @62行aload_1将局部变量中的值压入栈顶中
  4. @63行athrow将栈顶的异常变量抛出并回到调用点

从@53~@63这一段字节码可以看出finally中并不会影响return的值;但如果改的是对象中的某个属性值这就不一样了。

最后再看看main函数,main函数中没有使用try...catch块进行异常的捕获,而是通过throws关键交由调用者处理;通过运行上面的代码可得知在发生异常后整个JVM进程将退款。

总结

经过上面对异常处理字节码的分析可以得出如下JVM异常处理流程:

  1. 在方法的调用中通过Exception table表进行捕获,并找到处理异常的catch块代码
  2. 如果没有匹配到catch块将向上抛出异常交由调用者进行处理
  3. 在调用者中使用第一、第二步的方法进行处理
  4. 通过第三步处理完成后异常还是没有进行处理时将抛出出调用者的线程,线程将异常终止
  5. 如果抛出的线程为主线程则会导致主线程异常终止;JVM将会异常终止

推荐阅读:
Java String StringPool StringBuilder StringBuffer详解,面试不再难
Java 8种基本值类型
知识点:Java HashMap 原理与源码分析(下)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值