LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 357|回复: 0

Java - 深入理解异常

[复制链接]
发表于 2023-12-17 22:54:44 | 显示全部楼层 |阅读模式

深入理解异常
提示
我们再深入理解下异常,看下底层实现。

# JVM处理异常的机制?

提到JVM处理异常的机制,就需要提及Exception Table,以下称为异常表。我们暂且不急于介绍异常表,先看一个简单的 Java 处理异常的小例子。

public static void simpleTryCatch() {
   try {
       testNPE();
   } catch (Exception e) {
       e.printStackTrace();
   }
}

上面的代码是一个很简单的例子,用来捕获处理一个潜在的空指针异常。

当然如果只是看简简单单的代码,我们很难看出什么高深之处,更没有了今天文章要谈论的内容。

所以这里我们需要借助一把神兵利器,它就是javap,一个用来拆解class文件的工具,和javac一样由JDK提供。

然后我们使用javap来分析这段代码(需要先使用javac编译)


  1. //javap -c Main
  2. public static void simpleTryCatch();
  3.     Code:
  4.        0: invokestatic  #3                  // Method testNPE:()V
  5.        3: goto          11
  6.        6: astore_0
  7.        7: aload_0
  8.        8: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
  9.       11: return
  10.     Exception table:
  11.        from    to  target type
  12.            0     3     6   Class java/lang/Exception
复制代码


看到上面的代码,应该会有会心一笑,因为终于看到了Exception table,也就是我们要研究的异常表。

异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下

from 可能发生异常的起始点
to 可能发生异常的结束点
target 上述from和to之前发生异常后的异常处理者的位置
type 异常处理者处理的异常的类信息

那么异常表用在什么时候呢答案是异常发生的时候,当一个异常发生时

1.JVM会在当前出现异常的方法中,查找异常表,是否有合适的处理者来处理
2.如果当前方法异常表不为空,并且异常符合处理者的from和to节点,并且type也匹配,则JVM调用位于target的调用者来处理。
3.如果上一条未找到合理的处理者,则继续查找异常表中的剩余条目
4.如果当前方法的异常表无法处理,则向上查找(弹栈处理)刚刚调用该方法的调用处,并重复上面的操作。
5.如果所有的栈帧被弹出,仍然没有处理,则抛给当前的Thread,Thread则会终止。
6.如果当前Thread为最后一个非守护线程,且未处理异常,则会导致JVM终止运行。

以上就是JVM处理异常的一些机制。

try catch -finally

除了简单的try-catch外,我们还常常和finally做结合使用。比如这样的代码

  1. public static void simpleTryCatchFinally() {
  2.    try {
  3.        testNPE();
  4.    } catch (Exception e) {
  5.        e.printStackTrace();
  6.    } finally {
  7.        System.out.println("Finally");
  8.    }
  9. }


  10. 同样我们使用javap分析一下代码

  11. public static void simpleTryCatchFinally();
  12.     Code:
  13.        0: invokestatic  #3                  // Method testNPE:()V
  14.        3: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  15.        6: ldc           #7                  // String Finally
  16.        8: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  17.       11: goto          41
  18.       14: astore_0
  19.       15: aload_0
  20.       16: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
  21.       19: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  22.       22: ldc           #7                  // String Finally
  23.       24: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  24.       27: goto          41
  25.       30: astore_1
  26.       31: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  27.       34: ldc           #7                  // String Finally
  28.       36: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  29.       39: aload_1
  30.       40: athrow
  31.       41: return
  32.     Exception table:
  33.        from    to  target type
  34.            0     3    14   Class java/lang/Exception
  35.            0     3    30   any
  36.           14    19    30   any
复制代码


和之前有所不同,这次异常表中,有三条数据,而我们仅仅捕获了一个Exception, 异常表的后两个item的type为any; 上面的三条异常表item的意思为:

如果0到3之间,发生了Exception类型的异常,调用14位置的异常处理者。
如果0到3之间,无论发生什么异常,都调用30位置的处理者
如果14到19之间(即catch部分),不论发生什么异常,都调用30位置的处理者。

再次分析上面的Java代码,finally里面的部分已经被提取到了try部分和catch部分。我们再次调一下代码来看一下

  1. public static void simpleTryCatchFinally();
  2.     Code:
  3.       //try 部分提取finally代码,如果没有异常发生,则执行输出finally操作,直至goto到41位置,执行返回操作。  

  4.        0: invokestatic  #3                  // Method testNPE:()V
  5.        3: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  6.        6: ldc           #7                  // String Finally
  7.        8: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  8.       11: goto          41

  9.       //catch部分提取finally代码,如果没有异常发生,则执行输出finally操作,直至执行got到41位置,执行返回操作。
  10.       14: astore_0
  11.       15: aload_0
  12.       16: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
  13.       19: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  14.       22: ldc           #7                  // String Finally
  15.       24: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  16.       27: goto          41
  17.       //finally部分的代码如果被调用,有可能是try部分,也有可能是catch部分发生异常。
  18.       30: astore_1
  19.       31: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  20.       34: ldc           #7                  // String Finally
  21.       36: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  22.       39: aload_1
  23.       40: athrow     //如果异常没有被catch捕获,而是到了这里,执行完finally的语句后,仍然要把这个异常抛出去,传递给调用处。
  24.       41: return
复制代码


Catch先后顺序的问题

我们在代码中的catch的顺序决定了异常处理者在异常表的位置,所以,越是具体的异常要先处理,否则就会出现下面的问题

private static void misuseCatchException() {
   try {
       testNPE();
   } catch (Throwable t) {
       t.printStackTrace();
   } catch (Exception e) { //error occurs during compilings with tips Exception Java.lang.Exception has already benn caught.
       e.printStackTrace();
   }
}
这段代码会导致编译失败,因为先捕获Throwable后捕获Exception,会导致后面的catch永远无法被执行。

Return 和finally的问题

这算是我们扩展的一个相对比较极端的问题,就是类似这样的代码,既有return,又有finally,那么finally导致会不会执行

public static String tryCatchReturn() {
   try {
       testNPE();
       return  "OK";
   } catch (Exception e) {
       return "ERROR";
   } finally {
       System.out.println("tryCatchReturn");
   }
}
答案是finally会执行,那么还是使用上面的方法,我们来看一下为什么finally会执行。

  1. public static java.lang.String tryCatchReturn();
  2.     Code:
  3.        0: invokestatic  #3                  // Method testNPE:()V
  4.        3: ldc           #6                  // String OK
  5.        5: astore_0
  6.        6: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
  7.        9: ldc           #8                  // String tryCatchReturn
  8.       11: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  9.       14: aload_0
  10.       15: areturn       返回OK字符串,areturn意思为return a reference from a method
  11.       16: astore_0
  12.       17: ldc           #10                 // String ERROR
  13.       19: astore_1
  14.       20: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
  15.       23: ldc           #8                  // String tryCatchReturn
  16.       25: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  17.       28: aload_1
  18.       29: areturn  //返回ERROR字符串
  19.       30: astore_2
  20.       31: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
  21.       34: ldc           #8                  // String tryCatchReturn
  22.       36: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  23.       39: aload_2
  24.       40: athrow  如果catch有未处理的异常,抛出去。
复制代码

# 异常是否耗时?为什么会耗时?

说用异常慢,首先来看看异常慢在哪里?有多慢?下面的测试用例简单的测试了建立对象、建立异常对象、抛出并接住异常对象三者的耗时对比:

public class ExceptionTest {  
  
    private int testTimes;  
  
    public ExceptionTest(int testTimes) {  
        this.testTimes = testTimes;  
    }  
  
    public void newObject() {  
        long l = System.nanoTime();  
        for (int i = 0; i < testTimes; i++) {  
            new Object();  
        }  
        System.out.println("建立对象:" + (System.nanoTime() - l));  
    }  
  
    public void newException() {  
        long l = System.nanoTime();  
        for (int i = 0; i < testTimes; i++) {  
            new Exception();  
        }  
        System.out.println("建立异常对象:" + (System.nanoTime() - l));  
    }  
  
    public void catchException() {  
        long l = System.nanoTime();  
        for (int i = 0; i < testTimes; i++) {  
            try {  
                throw new Exception();  
            } catch (Exception e) {  
            }  
        }  
        System.out.println("建立、抛出并接住异常对象:" + (System.nanoTime() - l));  
    }  
  
    public static void main(String[] args) {  
        ExceptionTest test = new ExceptionTest(10000);  
        test.newObject();  
        test.newException();  
        test.catchException();  
    }  
}  

运行结果:

建立对象:575817  
建立异常对象:9589080  
建立、抛出并接住异常对象:47394475  


建立一个异常对象,是建立一个普通Object耗时的约20倍(实际上差距会比这个数字更大一些,因为循环也占用了时间,追求精确的读者可以再测一下空循环的耗时然后在对比前减掉这部分),而抛出、接住一个异常对象,所花费时间大约是建立异常对象的4倍。
------

原文链接:https://pdai.tech/md/java/basic/java-basic-x-exception.html

您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表