《深入理解Java虛擬機》- JVM如何進行異常處理

一、Java異常

在程序中,錯誤可能產生于程序員沒有預料到的各種情況,或者超出程序員可控范圍的環境,例如用戶的壞數據、試圖打開一個不存在的文件等。為了能夠及時有效地處理程序中的運行錯誤,Java 專門引入了異常類。

二、Java常見異常分類

三、為什么產生異常

在 Java 中一個異常的產生,主要有如下三種原因:

  1. Java 內部錯誤發生異常,Java 虛擬機產生的異常。
  2. 編寫的程序代碼中的錯誤所產生的異常,例如空指針異常、數組越界異常等。這種異常稱為未檢査的異常,一般需要在某些類中集中處理這些異常。
  3. 通過 throw 語句手動生成的異常,這種異常稱為檢査的異常,一般用來告知該方法的調用者一些必要的信息。

四、碰到異常怎么辦?

我們把生成異常對象,并把它提交給運行時系統的過程稱為拋出(throw)異常。運行時系統在方法的調用棧中查找,直到找到能夠處理該類型異常的對象,這一個過程稱為捕獲(catch)異常。

Java 異常強制用戶考慮程序的強健性和安全性。異常處理不應用來控制程序的正常流程,其主要作用是捕獲程序在運行時發生的異常并進行相應處理。編寫代碼處理某個方法可能出現的異常,可遵循如下三個原則:

  1. 在當前方法聲明中使用 try catch 語句捕獲異常。
  2. 一個方法被覆蓋時,覆蓋它的方法必須拋出相同的異?;蛞斐5淖永?。
  3. 如果父類拋出多個異常,則覆蓋方法必須拋出那些異常的一個子集,而不能拋出新異常。

(引用://c.biancheng.net/view/1038.html)

五、從JVM角度看異常的產生與表達

 先看示例代碼:

public class Foo {
  private int tryBlock;
  private int catchBlock;
  private int finallyBlock;
  private int methodExit;


  public void test() {
    try {
      tryBlock = 0;
    } catch (Exception e) {
      catchBlock = 1;
    } finally {
      finallyBlock = 2;
    }
    methodExit = 3;
  }
}

這段代碼是一段簡單的異常處理代碼,我們可以通過javap查看class文件的表達形式:

public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: iconst_0
         2: putfield      #2                  // Field tryBlock:I
         5: aload_0
         6: iconst_2
         7: putfield      #3                  // Field finallyBlock:I
        10: goto          35
        13: astore_1
        14: aload_0
        15: iconst_1
        16: putfield      #5                  // Field catchBlock:I
        19: aload_0
        20: iconst_2
        21: putfield      #3                  // Field finallyBlock:I
        24: goto          35
        27: astore_2
        28: aload_0
        29: iconst_2
        30: putfield      #3                  // Field finallyBlock:I
        33: aload_2
        34: athrow
        35: aload_0
        36: iconst_3
        37: putfield      #6                  // Field methodExit:I
        40: return
      Exception table:
         from    to  target type
             0     5    13   Class java/lang/Exception
             0     5    27   any
            13    19    27   any
      LineNumberTable:
        line 10: 0
        line 14: 5
        line 15: 10
        line 11: 13
        line 12: 14
        line 14: 19
        line 15: 24
        line 14: 27
        line 16: 35
        line 17: 40
      StackMapTable: number_of_entries = 3
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 7 /* same */

從字節碼中的注釋可以看到,finally塊被添加到了三個地方。也就是說,在從java代碼翻譯成字節碼文件時,jvm會為try塊和catch塊生成finally 塊里的邏輯。但是想想,為什么是三個“finally”呢? 最后一個finally 是為在catch塊中的代碼執行時發生異常而準備的。那么,有人會問,finally塊的代碼如果還有報錯怎么辦呢? 這里,引進沒有被本人證實的事實:會往外拋出去,給上一層代碼進行處理。

這里說明一下黃色部分的字節碼:

exception table 表示異常表,異常表是用于存儲代碼中涉及到的所有異常,每個類編譯后,都會跟隨一個異常表,如果發生異常,首先在異常表中查找對應的行(即代碼中相應的 try{}catch(){}代碼塊),如果找到,則跳轉到異常處理代碼執行,如果沒有找到,則返回(執行 finally 之后),并 copy 異常的應用給父調用者,接著查詢父調用的異常表,以此類推。

from...to:表示異常處理器監控的范圍(比如try塊包含的代碼)

target:表示異常處理器起始的位置(比如catch塊包含的代碼)

type:就是處理的異常

那么,發生異常后,如何對照異常表?

當程序觸發異常后,Java虛擬機會從上到下遍歷異常表中的條目。當觸發異常的字節碼的索引值在某個異常表條目的監控范圍內,Java虛擬機會判斷所拋出的異常和該條目想要捕獲的異常是否匹配。如果匹配,Java虛擬機會將控制流轉移到該條目的target指針指向的代碼上,繼續程序運行。

下面,提及的字節碼解析一下異常表:

程序開始,運行到1:iconst_0時,發生Exception異常,此時程序會去便利方法表,從第一行開始,檢測到 0<1<5,符合第一條目檢測范圍,接著再查看拋出的異常為Exception,符合該條目捕獲處理的異常,后跳轉至序號13字節碼繼續運行。若再在14:aload_0發生異常時,程序就又跳到異常表,查找匹配異常條目,最終找到target為序號為27的字節碼,然后便一直往下走完所有字節碼。

上例子中,屬于在catch塊發生異常,所以會看到字節碼后還有一個athrow的步驟,也就是往外拋出異常啦。

 

好了,Jvm看異常到此。

(引:極客時間)

posted @ 2019-08-21 22:39 一只喜鵲 閱讀(...) 評論(...) 編輯 收藏