1. 程式人生 > >JVM對Java異常的處理原理

JVM對Java異常的處理原理

最初我們用 Java 寫 JSP 的時候,幾乎可以不觸及異常,因為 Servlet 容器會把 API 丟擲的異常包裝成 ServletException 丟給容器去處理。再後來應用分層,程式碼中要處理的異常便多了,一般會轉換成自定義的業務異常類,用 try-catch-throw customerException-finally。再到如今各種框架日臻成熟,程式碼中顯式的異常處理又漸漸少了些,藉助於 AOP 橫行,異常對業務的影響描述被移入到了配置檔案中了,例如,事物處理、許可權的控制等。

這頗有些像手機的發展,當通訊技術不甚發達的時候,手裡抓的是磚頭,訊號是模擬的。後來慢慢瘦身成兩三根手指大小,甚至是就一支筆似的,可如今資訊量大了,螢幕要大,再配上 QWERT 鍵盤,機身自然就肥碩了。

當然與手機的個頭變遷略有不同的是,任憑你怎麼對待 Java 中異常,切入 AOP 也好,在 JVM 中處理異常的內在機制始終未變。

說到 Java 異常,無外乎就是 try、catch、finally、throw、throws 這麼幾個關鍵字,這些個的用法是沒必要在這裡講了。我們這裡主要關鍵一下 catch 和 finally 是如何在編譯後的 class 位元組碼中的。

異常的丟擲與捕獲,Catch 子句的表現,來看看一段 Java 程式碼及生成的相應位元組碼指令。

  1. package com.unmi;   
  2. import java.io.UnsupportedEncodingException;   
  3. public class AboutCatch {   
  4.     public static void main(String[] args){   
  5.         try {   
  6.             transfer("JVM 對 Java 異常的處理","gbk");   
  7.         } catch (Exception e) {   
  8.             //e.printStackTrace();   
  9.         }   
  10.     }   
  11.     //字符集轉換的方法 
      
  12.     public static void transfer(String src, String charset)   
  13.             throws Exception{   
  14.         String result = "";   
  15.         try{   
  16.             //這行程式碼可能會丟擲空指標,不支援的字符集,陣列越界的異常   
  17.             result = new String(src.getBytes(),0,10,charset);   
  18.         }catch(NullPointerException ne){   
  19.             System.out.println("捕獲到異常 ArithemticExcetipn"
    );   
  20.             throw ne;   
  21.         }catch(UnsupportedEncodingException uee){   
  22.             System.out.println("捕獲到異常 UnsupportedEncodingException");   
  23.             throw uee;   
  24.         }catch(Exception ex){ //比如陣列越界時在這裡可捕獲到   
  25.             System.out.println("捕獲到異常 Exception");   
  26.             throw ex;   
  27.         }   
  28.         System.out.println(result);   
  29.     }   
  30. }   


來看看上面程式碼中的 transfer() 方法相應的位元組碼指令,編譯器是 Eclipse 3.3.2 的,它所用的 JDK 是 1.6.0_06,編譯相容級別設定為 6.0。用命令 javap -c com.unmi.AboutCatch 在 Dos 視窗中就能輸出:

public static void transfer(java.lang.String, java.lang.String)   throws java.lang.Exception;
  Code:
   0:   ldc     #30; //String
   2:   astore_2
3:   new     #32; //class java/lang/String
   6:   dup
   7:   aload_0
   8:   invokevirtual   #34; //Method java/lang/String.getBytes:()[B
   11:  iconst_0
   12:  bipush  10
   14:  aload_1
   15:  invokespecial   #38; //Method java/lang/String."<init>":([BIILjava/lang/String;)V
   18:  astore_2
19:  goto    55//依據異常表執行完異常處理塊後,再回到這裡,然後 goto 到 55 號指令繼續執行
22:  astore_3
   23:  getstatic       #41; //Field java/lang/System.out:Ljava/io/PrintStream;
   26:  ldc     #47; //String 捕獲到異常 ArithemticExcetipn
   28:  invokevirtual   #49; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   31:  aload_3
   32:  athrow    //丟擲 ArthemticException 異常
33:  astore_3
   34:  getstatic       #41; //Field java/lang/System.out:Ljava/io/PrintStream;
   37:  ldc     #55; //String 捕獲到異常 UnsupportedEncodingException
   39:  invokevirtual   #49; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   42:  aload_3
   43:  athrow    //丟擲 UnsupportedEncodingException 異常
44:  astore_3
   45:  getstatic       #41; //Field java/lang/System.out:Ljava/io/PrintStream;
   48:  ldc     #57; //String 捕獲到異常 Exception
   50:  invokevirtual   #49; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   53:  aload_3
   54:  athrow   //丟擲 Exception 異常
55:  getstatic       #41; //Field java/lang/System.out:Ljava/io/PrintStream;
   58:  aload_2
   59:  invokevirtual   #49; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   62:  return
  Exception table:  //這下面是一個異常表,所以異常不像普通程式碼那樣是靠 goto 語句來跳轉的
   from   to  target type
//0-19 號指令中,碰到 NullPointerException時,跳到 22 號指令
31922   Class java/lang/NullPointerException

     //0-19 號指令中,碰到 UnsupportedEncodingException 時,跳到 33 號指令 
     3    19    33   Class java/io/UnsupportedEncodingException

     //0-19 號指令中,碰到 NullPointerException時,跳到 44 號指令
     3    19    44   Class java/lang/Exception 

說明:

對於上面的程式,我們可以用下面程式碼來呼叫看看輸出

1) transfer("JVM 對 Java 異常的處理","gbk");  //正常
2) transfer(null, "gbk");                                         //空指標異常
3) transfer("JVM 對","gbk");                               //陣列越界異常
4) transfer("JVM 對","gbk-1");                            //不支援的字符集異常

最後可以把程式碼中的
catch(Exception ex){ //比如陣列越界時在這裡可捕獲到
   System.out.println("捕獲到異常 Exception");
   throw ex;
  }

或是 main() 方法寫成

 public static void main(String[] args) throws Exception{
  transfer("JVM 對 Java 異常的處理","gbk");
 }

來試試,異常一直未得到處理對 JVM 的影響

位元組碼中,紅色部分是我加上去的註釋,著重描了要關注的地方,其他的出入棧、方法呼叫的指令可不予以理會,關鍵是隻要知曉有一個異常表的存在,try 的範圍就是體現在異常錶行記錄的起點和終點。JVM 在 try 住的程式碼區間內如有異常丟擲的話,就會在當前棧楨的異常表中,找到匹配型別的異常記錄的入口指令號,然後跳到該指令處執行。異常指令塊執行完後,再回來繼續執行後面的程式碼。JVM 按照每個入口在表中出現的順序進行檢索,如果沒有發現匹配的項,JVM 將當前棧幀從棧中彈出,再次丟擲同樣的異常。當 JVM 彈出當前棧幀時,JVM 馬上終止當前方法的執行,並且返回到呼叫本方法的方法中,但是並非繼續正常執行該方法,而是在該方法中丟擲同樣的異常,這就使得 JVM 在該方法中再次執行同樣的搜尋異常表的操作。

上面那樣的內層方法無法處理異常的層層向外拋,層層壓棧,這樣就形成一個異常棧。異常棧十分有利於我們透析問題之所在,例如 e.printStackTrace(); 或者帶引數的 e.printStackTrace(); 方法可將異常棧資訊定向輸出到他處,還有 log4j 的 log.error(Throwable) 也有此功效。若是在行徑的哪層有能力處理該異常則已,否則直至 JVM,直接造成 JVM 崩潰掉。例如當 main() 方法也把異常拋了出去,JVM 此刻也就到了生命的盡頭。