歡迎來到“Under The Hood”第六期。本期我們介紹JVM處理異常的方式,包括如何拋出和捕獲異常及相關的字節碼指令。但本文不會討論finally子句,這是下期的主題。你可能需要閱讀往期的文章才能更好的理解本文。
異常處理
在程序運行時,異常讓你可以平滑的處理意外狀況。為了演示JVM處理異常的方式,考慮NitPickyMath類,它提供對整數進行加,減,乘,除以及取余的操作。
NitPickyMath提供的這些操作和Java語言的“+”,“-”,“*”,“/”和“%”是一樣的,除了NitPickyMath中的方法在以下情況下會拋出檢查型(checked)異常:上溢出,下溢出以及被0除。0做除數時,JVM會拋出ArithmeticException異常,但是上溢出和下溢出不會引發任何異常。NitPickyMath中拋出異常的方法定義如下:
-
class OverflowException extends Exception {
-
}
-
class UnderflowException extends Exception {
-
}
-
class DivideByZeroException extends Exception {
-
}
NitPickyMath類中的remainder()方法就是一個拋出和捕獲異常的簡單方法。
-
static int remainder(int dividend, int divisor)
-
throws DivideByZeroException {
-
try {
-
return dividend % divisor;
-
}
-
catch (ArithmeticException e) {
-
throw new DivideByZeroException();
-
}
-
}
remainder()方法,只是簡單的對當作參數傳遞進來的2個整數進行取余操作。如果取余操作的除數是0,會引發ArithmeticException異常。remainder()方法捕獲這個異常,并重新拋出DivideByZeroException異常。
DivideByZeroException和ArithmeticException的區別是,DivideByZeroException是檢查型(checked)異常,而ArithmeticException是非檢查(unchecked)型異常。由于ArithmeticException是非檢查型異常,一個方法就算會拋出該異常,也不必在其throw子句中聲明它。任何Error或RuntimeException異常的子類異常都是非檢查型異常。(ArithmeticException就是RuntimeException的子類。)通過捕獲ArithmeticException和拋出DivideByZeroException,remainder()方法強迫它的調用者去處理除數為0的可能性,要么捕獲它,要么在其throw子句中聲明DivideByZeroException異常。這是因為,像DivideByZeroException這種在方法中拋出的檢查型異常,要么在方法中捕獲,要么在其throw子句中聲明,二者必選其一。而像ArithmeticException這種非檢查型異常,就不需要去顯式捕獲和聲明。
javac為remainder()方法生成的字節碼序列如下:
-
// The main bytecode sequence for remainder:
-
0 iload_0 // Push local variable 0 (arg passed as divisor)
-
1 iload_1 // Push local variable 1 (arg passed as dividend)
-
2 irem // Pop divisor, pop dividend, push remainder
-
3 ireturn // Return int on top of stack (the remainder)
-
// The bytecode sequence for the catch (ArithmeticException) clause:
-
4 pop // Pop the reference to the ArithmeticException
-
// because it is not used by this catch clause.
-
5 new #5 < Class DivideByZeroException >
-
// Create and push reference to new object of class
-
// DivideByZeroException.
-
8 dup // Duplicate the reference to the new
-
// object on the top of the stack because it
-
// must be both initialized
-
// and thrown. The initialization will consume
-
// the copy of the reference created by the dup.
-
9 invokenonvirtual #9 < Method DivideByZeroException.< init >()V >
-
// Call the constructor for the DivideByZeroException
-
// to initialize it. This instruction
-
// will pop the top reference to the object.
-
12 athrow // Pop the reference to a Throwable object, in this
-
// case the DivideByZeroException,
-
// and throw the exception.
remainder()方法的字節碼有2個單獨的部分。第一部分是該方法的正常執行路徑,這部分從第0行開始,到第3行結束。第二部分是從第4行開始,到12行結束的catch子句。
主字節碼序列中的irem指令可能會拋出ArithmeticException異常。如果異常發生了,JVM通過在異常表中查找匹配的異常,它會知道要跳轉到相應的異常處理的catch子句的字節碼序列部分。每個捕獲異常的方法,都跟類文件中與方法字節碼一起交付的異常表關聯。每一個捕獲異常的try塊,都是異常表中的一行。每行4條信息:開始行號(from)和結束行號(to),要跳轉的字節碼序列行號(target),被捕獲的異常類的常量池索引(type)。remainder()方法的異常表如下所示:
FROM
|
TO
|
TARGET
|
TYPE
|
0 |
4 |
4 |
< Class java.lang.ArithmeticException > |
上面的異常表表明,行號1到3范圍內,ArithmeticException將被捕獲。異常表中的“to”下面的結束行號始終比異常捕獲的最大行號大1,上表中,結束行號為4,而異常捕獲的最大行號是3。行號0到3的字節碼序列對應remainder()方法中的try塊。“target”列中,是行0到3的字節碼發生ArithmeticException異常時要跳轉到的目標行號。
如果方法執行過程中產生了異常,JVM會在異常表中查找匹配行。異常表中的匹配行要符合下面的條件:當前pc寄存器的值要在該行的表示范圍之內,[from, to),且產生的異常是該行所指定的異常類或其子類。JVM按從上到下的次序查找異常表。當找到了第一個匹配行,JVM把pc寄存器設為新的跳轉行號,從此行繼續往下執行。如果找不到匹配行,JVM彈出當前棧幀,并重新拋出同一個異常。當JVM彈出當前棧幀時,它會終止當前方法的執行,返回到調用該方法的上一個方法那里。這時,在上一個方法里,并不會繼續正常的執行過程,而是拋出同樣的異常,促使JVM重新查找該方法的異常表。
Java程序員可以用throw語句拋出像remainder()方法的catch子句中的異常,DivideByZeroException。下表列出了拋出異常的字節碼:
OPCODE
|
OPERAND(S)
|
DESCRIPTION
|
athrow |
(none) |
pops Throwable object reference, throws the exception |
athrow指令把棧頂元素彈出,該元素必須是Throwable的子類或其自身的對象引用,而拋出的異常類型由棧頂彈出的對象引用所指明。
本文譯自:How the Java virtual machine handles exceptions