在這個專欄的第一期,我們討論了拋出異常的開銷。這個月,我們換一個角度再來看這個主題 —— JVM 如何處理所拋出的異常 —— 并且我們要考慮,最理想的異常編碼應該看成是早期的優化還是最優方法?
編碼的艱難決擇:應該這樣做還是那樣做?
性能討論組中充斥著類似于這樣的問題“我應該像大多數人那樣編寫代碼,還是為了得到更好的性能那樣編寫代碼?”一般專家會建議應該避免早期的優化,并且直到性能測試顯示需要優化的時候才使用最優方法,但實際情況是我們每寫一行代碼都在做出會影響到性能的決定。
javaRanch 上的一項討論調查了確保類型安全的兩種選擇,一種是拋出異常,另一種是用 instanceof,并提出了“哪種方法更好”的問題。清單 1 和 2 顯示了這兩種方法。
清單 1. 使用 instanceof 來分支
Listing 1: using instanceof to branch
for (int i = 0; i < max; i++)
{
Object obj = myVector.elementAt(i);
if (obj instanceof MySpecialClass)
{
// do this
}
}
清單 2. 拋出異常來分支
for (int i = 0; i < max; i++) {
try {
MySpecialClass myClass = (MySpecialClass)myVector.elementAt(i);
// do this
} catch (ClassCastException cce) {
continue; // for loop
}
}
提這種問題的危險之一是,當您想把問題濃縮成一個簡單例子時可能會失去很多上下文。沒有充分的上下文通常會把討論弄得長而混亂,因為每一個閱讀了問題的回答者都會用他們自己的上下文來聯系問題。所有這種額外的上下文都會增添含義,這會把我們從問題的出發點轉移出來。頭腦里有了這些東西之后,就讓我們來看能否從這一思路中找到的消息線索中篩選出某些真理來。
異常的特征
提起異常大多數開發者首先要說的就是它們很昂貴。假如您繼續追問為什么它們很昂貴,最普遍的答案是我們需要捕捉異常堆棧的當前狀態。盡管這是開銷的很大一部分,但通過列出異常的一些特征,我們可以知道這只是故事的開始。下面是異常的一些特征:
☆ 可以被拋出。
☆ 可以被捕捉。
☆ 可以被程序化地創建。
☆ 可以被 JVM 創建。
☆ 被表示為第一級對象。
☆ 繼續的深度從 3 開始。
☆ 由 String(和來自 1.4 的 StackTraceElements)組成。
☆ 依靠本機方法 fillInStackTrace()。
異常與其他對象的主要區別是異常可以被拋出和捕捉。讓我們從調查當異常被拋出時所觸發的事件過程來開始我們的研究。
處理異常的開銷
為了拋出異常,JVM 發出 athrow 字節碼指令。athrow 指令引起 JVM 將異常對象彈出執行堆棧。然后 JVM 搜索當前執行堆棧幀來尋找第一個 catch 子句,這個子句可以處理該類的一個異常或者其超類的一個異常。假如在當前的堆棧幀里沒有找到 catch block,那么當前堆棧幀就被釋放,異常在下一個堆棧幀的上下文中被重新拋出,如此這般,直到找到包含匹配的 catch 子句的堆棧幀,或者是到了執行堆棧的底部。最后,假如沒找到適當的 catch 塊,所有的堆棧幀都會被釋放,線程在 ThreadGroup 對象有了處理異常的機會后被終止(參考 ThreadGroup.uncaughtException)。假如找到了適當的 catch 塊,程序計數器會重置到那一塊代碼的第一行。
從這個描述中我們可以了解到處理一個拋出的異常是一個非常昂貴的主張。再看一看上面的異常特征清單。注重到除了 JVM 可以“本能地”創建一個異常外,其余剩下的開銷與在任何其他第一級對象的生命周期中所引起的開銷沒有什么區別。
新聞熱點
疑難解答