Java虚拟机-异常的处理

1 异常处理

异常处理的两大组成要素是抛出异常捕获异常。这两大要素共同实现程序控制流的非正常转移。

2 抛出异常

抛出异常可分为显式隐式两种。

  • 显式抛异常的主体是应用程序,它指的是在程序中使用“throw”关键字,手动将异常实例抛出。

  • 隐式抛异常的主体则是 Java 虚拟机,它指的是 Java 虚拟机在执行过程中,碰到无法继续执行的异常状态,自动抛出异常。举例来说,Java 虚拟机在执行读取数组操作时,发现输入的索引值是负数,故而抛出数组索引越界异常(ArrayIndexOutOfBoundsException)。

3 捕获异常

捕获异常则涉及了如下三种代码块。

  • try 代码块:用来标记需要进行异常监控的代码。
  • catch 代码块:跟在 try 代码块之后,用来捕获在 try 代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型之外,catch 代码块还定义了针对该异常类型的异常处理器。在 Java 中,try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会从上至下匹配异常处理器。因此,前面的 catch 代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错。
  • finally 代码块:跟在 try 代码块和 catch 代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码,例如关闭已打开的系统资源。
3.1 案例
public class TryCatchFinally {
    private String tryBlock = "tryBlock";
    private String catchBlock = "catchBlock";
    private String finallyBlock = "finallyBlock";

    public String test() {
        try {
            System.out.println(tryBlock);
            //int a = 5/0;  //异常抛出点
            return tryBlock;
        } catch (Exception e) {
            //int a = 5/0;  //异常抛出点
            System.out.println(catchBlock);
            return catchBlock;
        } finally {
            //int a = 5/0;  //异常抛出点
            System.out.println(finallyBlock);
            //return finallyBlock;
        }
    }

    public static void main(String[] args) {
        System.out.println(new TryCatchFinally().test());
    }
}
3.1 程序正常运行
tryBlock
finallyBlock
tryBlock    
3.2 程序异常运行
打开try中注释//int a = 5/0;

tryBlock
catchBlock
finallyBlock
catchBlock    
3.2 finally中返回

无论正常运行还是异常运行都返回"finallyBlock"

打开注释 //return finallyBlock;

//正常运行
tryBlock
finallyBlock
finallyBlock

//异常运行
tryBlock
catchBlock
finallyBlock
finallyBlock 
3.2 finally中抛出异常

中断finally中语句,对外抛出异常

打开finally中注释//int a = 5/0;

//正常运行
tryBlock

//异常运行
tryBlock
catchBlock
3.2 catch中抛出异常

中止发生异常后中语句,执行finally块中语句,并对外抛出异常

打开catch中注释//int a = 5/0;
打开try中注释//int a = 5/0;

tryBlock
finallyBlock

4 throws声明异常

使用throws声明在方法上声明可能出现的异常。throws是另一种处理异常的方式,它不同于try...catch...finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。

throws声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,否则编译不通过。

public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN
{ 
     //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。
}

4 异常类型

在 Java 语言规范中,所有异常都是 Throwable 类或者其子类的实例。Throwable 有两大直接子类。第一个是 Error,涵盖程序不应捕获的异常。第二子类则是 Exception,涵盖程序可能需要捕获并且处理的异常。

image

Exception 有一个特殊的子类 RuntimeException,用来表示“程序虽然无法继续执行,但是还能抢救一下”的情况。前边提到的数组索引越界便是其中的一种。

RuntimeException 和 Error 属于 Java 里的非检查异常(unchecked exception)。其他异常则属于检查异常(checked exception)。在 Java 语法中,所有的检查异常都需要程序显式地捕获,或者在方法声明中用 throws 关键字标注。通常情况下,程序中自定义的异常应为检查异常,以便最大化利用 Java 编译器的编译时检查。

5 异常的链化

查看Throwable类源码,可以发现里面有一个Throwable字段cause,就是以一个异常对象为参数构造新的异常对象。

public class Throwable implements Serializable {
    private Throwable cause = this;
   
    public Throwable(String message, Throwable cause) {
        fillInStackTrace();
        detailMessage = message;
        this.cause = cause;
    }
     public Throwable(Throwable cause) {
        fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }
    
    //........
}

案例

try {

    }catch(InputMismatchException immExp){
        throw new Exception("计算失败",immExp);  /////////////////////////////链化:以一个异常对象为参数构造新的异常对象。
    }

java.lang.Exception: 计算失败
    at practise.ExceptionTest.add(ExceptionTest.java:53)
    at practise.ExceptionTest.main(ExceptionTest.java:18)
Caused by: java.util.InputMismatchException
    at java.util.Scanner.throwFor(Scanner.java:864)
    at java.util.Scanner.next(Scanner.java:1485)
    at java.util.Scanner.nextInt(Scanner.java:2117)
    at java.util.Scanner.nextInt(Scanner.java:2076)
    at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)
    at practise.ExceptionTest.add(ExceptionTest.java:48)
    ... 1 more    

6 异常的信息

异常实例的构造十分昂贵。这是由于在构造异常实例时,Java 虚拟机便需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。

image

7 java虚拟机中异常处理

程序源文件

public class TryCatchFinally {
    private String tryBlock = "tryBlock";
    private String catchBlock = "catchBlock";
    private String finallyBlock = "finallyBlock";

    public String test() {
        try {
            System.out.println(tryBlock);
            //throw new RunTimeException();
            return tryBlock;
        } catch (Exception e) {
            System.out.println(catchBlock);
            return catchBlock;
        } finally {
            System.out.println(finallyBlock);
            //return finallyBlock;
        }
    }

    public static void main(String[] args) {
        System.out.println(new TryCatchFinally().test());
    }
}

程序编译后字节码指令

  public java.lang.String test();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: getfield      #3                  // Field tryBlock:Ljava/lang/String;
         7: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: aload_0
        11: getfield      #3                  // Field tryBlock:Ljava/lang/String;
        14: astore_1
        15: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: aload_0
        19: getfield      #7                  // Field finallyBlock:Ljava/lang/String;
        22: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: aload_1
        26: areturn
        27: astore_1
        28: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        31: aload_0
        32: getfield      #5                  // Field catchBlock:Ljava/lang/String;
        35: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        38: aload_0
        39: getfield      #5                  // Field catchBlock:Ljava/lang/String;
        42: astore_2
        43: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        46: aload_0
        47: getfield      #7                  // Field finallyBlock:Ljava/lang/String;
        50: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        53: aload_2
        54: areturn
        55: astore_3
        56: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        59: aload_0
        60: getfield      #7                  // Field finallyBlock:Ljava/lang/String;
        63: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        66: aload_3
        67: athrow
      Exception table:
         from    to  target type
             0    15    27   Class java/lang/Exception
             0    15    55   any
            27    43    55   any
7.1 Jvm如何实现异常捕获
  • 在编译生成的字节码中,每个方法都附带一个异常表。异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。

  • 其中,from 指针和 to 指针标示了该异常处理器所监控的范围,例如 try 代码块所覆盖的范围。target 指针则指向异常处理器的起始位置,例如 catch 代码块的起始位置。

  • 如此程序发生异常JVM会按照顺序遍历异常表中每一个声明的异常,首先从第一行开始,查看触发异常是否是type类指定的异常或其子类,如果是则从target处开始执行。如果不是则继续查看下一条。

 Exception table:
         from    to  target type
             0    15    30   Class java/lang/Exception
             0    15    61   any
            30    46    61   any
  • 其中Exception类型的异常是我们在应用程序源代码中定义的需要捕获的异常,该异常处理器所监控的范围为字节码指令0~15行。
from    to  target  type
   0    15    30    Class java/lang/Exception

//对应Java源文件代码
try {
     System.out.println(tryBlock);
     return tryBlock;
} catch (Exception e) {            
  • any类型表示任意类型异常,是JVM负责捕获的异常,当抛出程序没有捕获的异常时由any类型处理。
from    to  target type
   0    15    61   any
  30    46    61   any
7.2 程序的正常运行
  • 1 如下指令相当于执行try语句中System.out.println(tryBlock);
0: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield      #3                  // Field tryBlock:Ljava/lang/String;
7: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

//对应Java源文件代码
try {
     System.out.println(tryBlock);
     ...
}     

  • 2 如下指令相当于tryBlock变量保存到局部变量表中,待返回时使用
10: aload_0
11: getfield      #3                  // Field tryBlock:Ljava/lang/String;
14: astore_1

//对应Java源文件代码
try {
     ...
     return tryBlock;
}     
  • 3 如下指令指令相当于执行finally语句中System.out.println(finallyBlock)。【将finally代码块语句插入到try代码块返回前
15: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
18: aload_0
19: getfield      #7                  // Field finallyBlock:Ljava/lang/String;
22: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

//对应Java源文件代码
finally {
    System.out.println(finallyBlock);
}
  • 4 加载步骤2中保存的变量返回
25: aload_1
26: areturn

//对应Java源文件代码
try {
     ...
     return tryBlock;
} 
7.3 捕获异常处理

按照异常表到发生Exception异常程序会跳到27行指令运行

from    to  target   type
   0    15      27   Class java/lang/Exception
  • 1 如下指令指令相当于执行catch块语句中System.out.println(catchBlock);,并将异常对象e放入局部变量表的第一个。
27: astore_1
28: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
31: aload_0
32: getfield      #5                  // Field catchBlock:Ljava/lang/String;
35: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

//对应Java源文件代码
} catch (Exception e) {
    System.out.println(catchBlock);
    ...
}
  • 2 如下指令相当于finallyBlock变量保存到局部变量表中第一个位置,待返回时使用
38: aload_0
39: getfield      #5                  // Field catchBlock:Ljava/lang/String;

//对应Java源文件代码
} catch (Exception e) {
    ...
    return catchBlock;
}
  • 3 如下指令指令相当于执行finally语句中System.out.println(finallyBlock【将finally代码块语句插入到catch代码块返回前
43: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
46: aload_0
47: getfield      #7                  // Field finallyBlock:Ljava/lang/String;
50: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

//对应Java源文件代码
finally {
    System.out.println(finallyBlock);
}
  • 4 加载步骤2中保存的变量返回
53: aload_2
54: areturn

//对应Java源文件代码
} catch (Exception e) {
    ...
    return catchBlock;
}
7.4 未捕获异常处理

如果try语句发生了catch中未定义的异常,或者catch代码块发生了异常则叫由JVM捕获执行,跳到61行指令运行

from    to  target type
   0    15    61   any
  30    46    61   any
  • 1 如下指令指令相当于执行finally语句中System.out.println(finallyBlock,同时将异常对象放入局部变量表中。
55: astore_3
56: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
59: aload_0
60: getfield      #7                  // Field finallyBlock:Ljava/lang/String;
63: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  • 2 读取步骤1中异常,向方法外抛出。
66: aload_3
67: athrow
7.5 小结
  • 可以看出编译器将 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。

  • 包含三份 finally 代码块。其中,前两份分别位于 try 代码块和 catch 代码块的正常执行路径出口。最后一份在try 代码块触发的、未被 catch 代码块捕获的异常,以及 catch 代码块触发的异常时执行。

image

8 Java 7 的 Supressed 异常以及语法糖

  • Java 7 引入了 Supressed 异常来解决这个问题。这个新特性允许开发人员将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息。然而,Java 层面的 finally 代码块缺少指向所捕获异常的引用,所以这个新特性使用起来非常繁琐。为此,Java 7 专门构造了一个名为 try-with-resources的语法糖,
  • 在字节码层面自动使用 Supressed异常。当然,该语法糖的主要目的并不
  • 是使用 Supressed异常,而是精简资源打开关闭的用法。
  • 在 Java 7 之前,对于打开的资源,我们需要定义一个 finally 代码块,来确保该资源在正常或者异常执行状况下都能关闭。
  • 资源的关闭操作本身容易触发异常。因此,如果同时打开多个资源,那么每一个资源都要对应一个独立的 try-finally 代码块,以保证每个资源都能够关闭。这样一来,代码将会变得十分繁琐。
  FileInputStream in0 = null;
  FileInputStream in1 = null;
  FileInputStream in2 = null;
  ...
  try {
    in0 = new FileInputStream(new File("in0.txt"));
    ...
    try {
      in1 = new FileInputStream(new File("in1.txt"));
      ...
      try {
        in2 = new FileInputStream(new File("in2.txt"));
        ...
      } finally {
        if (in2 != null) in2.close();
      }
    } finally {
      if (in1 != null) in1.close();
    }
  } finally {
    if (in0 != null) in0.close();
  }

Java 7 的 try-with-resources 语法糖,极大地简化了上述代码。程序可以在 try 关键字后声明并实例化实现了 AutoCloseable 接口的类,编译器将自动添加对应的 close() 操作。在声明多个 AutoCloseable 实例的情况下,编译生成的字节码类似于上面手工编写代码的编译结果。与手工代码相比,try-with-resources 还会使用 Supressed 异常的功能,来避免原异常“被消失”。

public class Foo implements AutoCloseable {
  private final String name;
  public Foo(String name) { this.name = name; }

  @Override
  public void close() {
    throw new RuntimeException(name);
  }

  public static void main(String[] args) {
    try (Foo foo0 = new Foo("Foo0"); // try-with-resources
         Foo foo1 = new Foo("Foo1");
         Foo foo2 = new Foo("Foo2")) {
      throw new RuntimeException("Initial");
    }
  }
}

// 运行结果:
Exception in thread "main" java.lang.RuntimeException: Initial
        at Foo.main(Foo.java:18)
        Suppressed: java.lang.RuntimeException: Foo2
                at Foo.close(Foo.java:13)
                at Foo.main(Foo.java:19)
        Suppressed: java.lang.RuntimeException: Foo1
                at Foo.close(Foo.java:13)
                at Foo.main(Foo.java:19)
        Suppressed: java.lang.RuntimeException: Foo0
                at Foo.close(Foo.java:13)
                at Foo.main(Foo.java:19)


除了 try-with-resources 语法糖之外,Java 7 还支持在同一 catch 代码块中捕获多种异常。实际实现非常简单,生成多个异常表条目即可。

// 在同一 catch 代码块中捕获多种异常
try {
  ...
} catch (SomeException | OtherException e) {
  ...
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352

推荐阅读更多精彩内容