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

  • 2019 年 10 月 3 日
  • 筆記

一、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看異常到此。

(引:極客時間)