異常的代價
最近在dynaTrace上出現了一場關于異常(Exception)的代價的大討論。在跟一些客戶的接觸中,我們經常的發現他們的代碼里有大量的異常處理,自己都不知道。在移除了這些異常后,程序的運行速度比以前有大幅度的提高。這讓我們產生了一種假想,程序中的異常處理語句是否給性能帶來了巨大的開銷?由此得出的推理會是,應該避免使用異常處理。由于異常處理是一個非常重要的處理錯誤情況的概念,完全的避免使用異常處理看起來并不是一種好的辦法。總的來說,我們有充足的理由來近距離的觀察一下異常的成本代價。
試驗
我的試驗是一段很簡單的代碼,隨機的拋出異常。這并不是一個具有什么重要意義的性能測量,而且我們也不知道HotSpot編譯器在程序運行時會對它做什么操作。不管怎樣,這多少會給我們提供一些基本的可觀察的信息。
public class ExceptionTest { public long maxLevel = 20; public static void main (String ... args){ ExceptionTest test = new ExceptionTest(); long start = System.currentTimeMillis(); int count = 10000; for (int i= 0; i < count; i++){ try { test.doTest(2, 0); }catch (Exception ex){ // ex.getStackTrace(); } } long diff = System.currentTimeMillis() - start; System.out.println(String.format("Average time for invocation: %1$.5f",((double) diff)/count)); } public void doTest (int i, int level){ if (level < maxLevel){ try { doTest (i, ++level); } catch (Exception ex){ // ex.getStackTrace(); throw new RuntimeException ("UUUPS", ex); } } else { if (i > 1) { throw new RuntimeException("Ups".substring(0, 3)); } } } }
結果
結果非常的有趣。拋出和捕捉異常的成本看起來非常的低。在我們這個測試中,每個異常大概用去0.002毫秒。這差不多可以忽略不計,除非你真的拋出了太多的異常——太多是指10萬個或更多。
雖然這些結果向我們展示了異常捕捉本身并不會給程序帶來性能問題,但它讓我們打開了另外一個問題:那究竟是什么使這些異常產生了巨大的性能壓力?所以,很顯然,我遺漏了某些東西——一些重要的東西。
對這個問題重新思考后,我認識到我遺漏了異常捕捉中的一個重要的部分。我遺漏的這塊是當異常發生后人們會做的事情。大多數情況下——希望如此——你并不只是把異常捕捉到,然后就完了。通常你會試圖糾正出現的問題,讓程序的功能能繼續滿足最終用戶。所以我的遺漏點是捕捉到異常后采用挽救措施的代碼。依賴于你的這段代碼究竟做了什么,它對性能的影響會有很大的不同。在一些情況下這意味著你需要重新連接某個服務,而另一些情況下可能意味著要調用缺省的預案,可能是一種操作簡化的方案。
這對我們在很多場景中見到的現象是一個很好的解釋,而我沒有做這樣的分析。我預感到,應該還有什么東西被我遺漏了。
堆棧跟蹤(Stack Traces)
對這個問題好奇不減,我觀察了一下當收集這些堆棧跟蹤信息時,情況會發生什么變化。這種情況很常見。你會記錄一個異常和它的堆棧跟蹤信息,用來找出是什么問題。
因此我修改了代碼,讓它獲取異常的堆棧跟蹤信息。情況發生了顯著的變化。獲取異常的堆棧跟蹤信息要比只是簡單的捕捉、拋出它們能產生10倍大的性能壓力。所以,堆棧跟蹤信息一方面能幫助我們理解什么地方出了問題甚至為什么會發生這個異常,但同時,它也帶來了性能上的懲罰。
這通常對性能的沖擊非常的大,因為我們并不是只面對一條堆棧跟蹤信息。大多數情況下異常的拋出——捕捉——會發生在多個層級上。讓我們看一下一個簡單的Web Service連接服務器的例子。首先連接失敗的異常會在Java類庫基層產生。然后客戶端的失敗會被框架捕捉到,然后在應用層面上某些業務邏輯調用失敗會拋出異常。現在加起來有會收集到3種堆棧跟蹤信息。
大多數情況下你會查看這些日志文件和應用輸出信息。記錄這些很長的堆棧信息也會帶來性能上的沖擊。如果你有規律的查看你的日志文件,你通常會研究它們,對問題作出反應——這是你要做的事情,不是嗎?;-)
有時我還能看到由于不正確的日志寫法導致的更嚴重的性能問題。開發人員不首先調用log.isxxEnabled ()來檢查中某個log級別的log行為是否開放,而是直接調用logging方法。當你這樣做時,日志代碼總是在執行時返回異常的堆棧跟蹤信息。但由于日志級別設置的太低,你可能永遠看不到這些信息,你可能根本不知道什么事情發生了。首先檢查日志記錄級別,這應該被當作一個基本習慣,這會讓你避免產生不必要的對象。
結論
由于擔心造成潛在的性能問題而不使用異常處理是一個不明智的做法。異常提供了一個標準方式來處理運行時的問題,幫助你寫出清晰的代碼。可是對于這程序中拋出的異常你需要跟蹤。盡管異常可以捕捉到,但它們仍然會產生很大的性能壓力。在dynaTrace公司,我們——缺省情況下——會跟蹤拋出的異常——在很多情況下客戶會對程序中的這些代碼產生的影響以及解決它們的方法感到很驚訝。
異常處理是個有用的東西,但你應該避免捕捉過多的堆棧跟蹤信息。大多數情況下它們對理解問題的發生沒有必要的用處——特別是你捕捉一個已經預期到的異常。這時簡單異常信息已經足夠讓你明白問題的原因。看到一個連接被拒絕信息,我已經知道什么問題了,所以我不需要java.net內部調用堆棧跟蹤。