1. 程式人生 > >《深入理解Java虛擬機器》- JVM如何進行異常處理

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

一、Java異常

在程式中,錯誤可能產生於程式設計師沒有預料到的各種情況,或者超出程式設計師可控範圍的環境,例如使用者的壞資料、試圖開啟一個不存在的檔案等。為了能夠及時有效地處理程式中的執行錯誤,Java 專門引入了異常類。

二、Java常見異常分類

三、為什麼產生異常

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

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

四、碰到異常怎麼辦?

我們把生成異常物件,並把它提交給執行時系統的過程稱為丟擲(throw)異常。執行時系統在方法的呼叫棧中查詢,直到找到能夠處理該型別異常的物件,這一個過程稱為捕獲(catch)異常。

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

  1. 在當前方法宣告中使用 try catch 語句捕獲異常。
  2. 一個方法被覆蓋時,覆蓋它的方法必須丟擲相同的異常或異常的子類。
  3. 如果父類丟擲多個異常,則覆蓋方法必須丟擲那些異常的一個子集,而不能拋出新異常。

(引用:http://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看異常到此。

(引:極客時