爲何不建議在 for 循環裏捕捉異常?

在回答標題這個問題以前,咱們先試想一下,在沒有 try…catch 的狀況下,若是想要對函數的異常結果進行判斷,咱們應該怎麼作?html

異常

第一個想法確定就是 if…else 了,通常狀況下,相關的代碼段咱們都是放在一塊兒的,若是此時你的程序中有大量的代碼段要作這作判斷,這就意味着後面執行的邏輯會依賴你前面語句的執行狀況,也就意味着你每調用一個可能會出現錯誤的函數的時候,都要先判斷是否成功,而後再繼續執行後面的語句。這就會致使你的代碼中會充斥着大量的 if…else。java

Java 是一門工程性的語言,而工程也是一種藝術,所以採用這樣的作法顯然是很不優雅的。《Thinking in Java》中提到「badly formed code will not be run.」,意思是結構不優雅的代碼不該該被執行,因而一個適用於 Java 的異常處理機制便應運而生了。程序員

Java 的異常處理其目的在於經過使用少於目前數量的代碼來簡化大型程序,舉個簡單的例子 🌰web

不用 try…catch微信

FileReader fr = new FileReader("path");if (fr == null) { System.err.println("Open File Error");} else { BufferedReader br = new BufferedReader(fr); while (br.ready()) { String line = br.readLine(); if (line == null) { System.err.println("Read Line Error"); } else { System.out.println(line); } }}

用了 try…catchoracle

try { FileReader fr = new FileReader("path"); BufferedReader br = new BufferedReader(fr); while (br.ready()) { String line = br.readLine(); System.out.println(line); }} catch (IOException e) { e.printStackTrace();}

很明顯咱們能夠看出來,下面這種寫法主線明確,可讀性更高。框架

固然,try…catch 也並非百利而無一害。若是程序員在代碼中濫用了 try…catch,而且沒有作好異常處理,頗有可能會致使一些 bug 被隱藏,沒法跟蹤。不過這些不是本文的重點。有興趣的能夠去閱讀下《Thinking in Java》的第 12 章「經過異常處理錯誤」。jvm

單獨捕獲異常

在探究將異常捕獲與循環結合起來以前,咱們先看一下單獨捕獲一個異常會發生什麼?這是一段異常代碼函數

咱們用 javap -c ExceptionDemo.class 來打印出他的字節碼來看一下性能

指令含義不是本文的重點,因此這裏就不介紹具體的含義,感興趣能夠到 Oracle 官網查看相應指令的含義 👉The Java Virtual Machine Instruction Set[1]

異常表的四個參數

從輸出看,字節碼分兩部分,code(指令)和 exception table(異常表)兩部分。當將 java 源碼編譯成相應的字節碼的時候,若是方法內有 try catch 異常處理,就會產生與該方法相關聯的異常表,也就是Exception table:部分。

每個條目有四列信息: 異常聲明的開始行, 結束行, 異常捕獲後跳轉到的代碼計數器(PC)所指向的行數, 還有一個表示捕獲的異常類的常量池索引。

那這些信息是從哪來得到的呢?這裏咱們先來來複習一下 JVM 的相關知識:

一個線程就是一個棧,由棧幀組成,一個方法就是一個棧幀,內部保存着:局部變量表、操做數棧、動態連接、方法出口。

JVM 在構造異常實例時須要生成該異常的棧軌跡。這個操做會逐一訪問當前線程的棧幀,而且記錄下各類調試信息,包括棧幀所指向方法的名字,方法所在的類名、文件名,以及在代碼中的第幾行觸發該異常等信息。而這些信息就會存儲在剛纔所說的Exception table:中。

四個參數的做用

那剛纔所說的那些信息又有什麼用呢?

若是在執行方法時有一個異常被拋出, JVM 就會從異常表中按照條目所出現的順序查找對應的條目。若是異常拋出時 PC 計數器所指向的行數正好落在異常表中某一條目包含的範圍內, 而且所拋出的異常正好是異常表中 type 列所指定的異常(或者所指定異常的子類), 那麼 JVM 就會將 PC 計數器指向 Target 偏移量所指向的地址, (進入 catch 塊)繼續執行。

若是沒有在異常表中找到異常, JVM 就會將當前棧幀彈出並從新拋出這個異常。當 JVM 彈出當前棧幀的時候, 它就會停止當前方法的執行, 返回到調用當前方法的外部方法中, 不過並不會像正常沒有異常發生時那樣繼續執行外部方法, 而是在外部方法中拋出相同的異常, 這樣將會致使 JVM 會在外部方法中重複查詢異常表並處理異常的過程。

爲何捕獲異常消耗性能

其實從上面的分析中,咱們就已經能夠理解爲何捕獲異常是一個消耗性能的操做了,當你 new 一個 exception 的時候,JVM 已經在 exception 裏構建好了全部的 stacktrace:

如今 Java 領域最火的框架莫過於 Spring 系列了,在一個 web 項目中,調用棧的深度是至關大的,因而可知這裏花費的代價是可觀的,所以,當你對 stacktrace 不感興趣的時候,不須要這樣的信息時,最好不要隨便的 new exception。

異常+for 循環

說了那麼多其實都是前置知識,如今咱們終於來到了標題提到的問題了。

for 循環和異常有兩種結合方式:

 try+for 循環

public static void tryFor() { int j = 3; try { for (int i = 0; i < 1000; i++) { Math.sin(j); } } catch (Exception e) { e.printStackTrace(); }}

for 循環+try

public static void forTry() { int j = 3; for (int i = 0; i < 1000; i++) { try { Math.sin(j); } catch (Exception e) { e.printStackTrace(); } }}

首先我先給出結論: 在沒有發生異常時,二者性能上沒有差別。若是發生異常,二者的處理邏輯不同,雖然已經不具備比較的意義了,但 for 循環+try 的耗時更明顯。

字節碼比較

咱們對這兩種方式進行一個字節碼的比較:

經過第二節的分析咱們知道,當程序出現異常時,java 虛擬機就會查找方法對應的異常表,若是發現有聲明的異常與拋出的異常類型匹配就會跳轉到 catch 處執行相應的邏輯,若是沒有匹配成功,就會回到上層調用方法中繼續查找,如此反覆,一直到異常被處理爲止,或者中止進程。而在 for 循環中進行 try…catch 操做,會不斷的進行這一過程,性能損耗天然會很恐怖。

測試比較

說了這麼多咱們一直都是紙上談兵,口說無憑,實際的效果確定是要跑一下才知道,這裏咱們採用 Java 的一個微基準測試框架JMH[2]來進行這次測試。

測試結果

Benchmark Mode Cnt Score Error UnitsExceptionDemo.forTry thrpt 20 70.236 ± 8.945 ops/msExceptionDemo.tryFor thrpt 20 85.864 ± 3.272 ops/ms

score 的結果是 xxx ± xxx,單位是每毫秒多少個操做。最終結果也驗證了咱們的結論。tryFor 的確會比 forTry 更節省性能。

最後

本文從異常出發,分析了單獨捕獲異常和將異常與 for 循環結合的幾種不一樣的狀況,而後經過 JMH 進行了一次測試,最終驗證咱們標題所說的,不建議在 for 循環裏捕捉異常。

固然,try…catch 對性能的影響除了第二節所提到的須要維護一個異常表以外,還有一個緣由,那就是 try 塊會阻止 java 的優化(例如重排序),try catch 裏面的代碼是不會被編譯器優化重排的。固然重排序是須要必定的條件觸發。通常而言,只要 try 塊範圍越小,對 java 的優化機制的影響是就越小。因此保證 try 塊範圍儘可能只覆蓋拋出異常的地方,就可使得異常對 java 優化的機制的影響最小化。

以上就是本文的所有內容了,若是你以爲有所幫助,不妨點個贊支持一下。


References

[1] The Java Virtual Machine Instruction Set: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
[2] JMH: http://openjdk.java.net/projects/code-tools/jmh/


本文分享自微信公衆號 - 01二進制(gh_d1999add1857)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索