談談異常那點事

曉峰老師在第二講主要講了關於Exception和Error的區別?本人之前也寫過一篇關於異常的文章,但是沒有深入去分析,只是做了一些簡單的總結;這次從 JVM 底層異常處理的角度來談談本人對異常的認識,歡迎各位同學指正我文中不正確的地方。

一、為什麼需要異常處理機制?

我相信大多數同學跟我一樣,接觸的第一門計算機語言便是 C 語言,C 語言是面向過程的編程語言,在異常的處理上,一般通過錯誤碼或者約定俗成的方式來處理異常,而這種處理方式帶來一個問題,大多數的程序員不會對錯誤狀態進行檢測並處理,如:printf() 的返回值有幾個人在關心呢?

又比如,你寫了一個框架或者組件,在編寫的過程中預測出會在運行時發生異常「如網絡抖動,資源問題等」,但是不知道怎麼去處理它,需要「直接或間接」使用者自己去處理。因此,需要一種異常處理機制來解決這個問題。

說了這麼多,歸根結底異常處理機制最終的目的就是解決代碼的可讀性健壯性

二、Java 的異常結構

Java 很多東西都是借鑑於 C++,異常處理機制也不例外;比如,C++ 的 logic_error 類相當於 Java 中的 RuntimeException,用來表示程序中的邏輯錯誤;runtime_error 類相當於 Java 中的 Checked Exception,表示受檢測異常。

在 Java 中“一切皆是對象”,毫不例外異常在 JVM 看來也是一個對象,所有的異常類的基類便是 Throwable 。在出現異常時,就會對當前的棧幀進行快照並生成一個異常對象「這是一個比較重的操作」,因此,對性能要求比較高的系統需要對異常設計進行優化。

可能你會有疑問:生成異常對象為什麼耗時多呢?代碼能很好的說明一切,見下圖。

談談異常那點事

談談異常那點事

我們可以發現 fillInStackTrace 方法記錄異常時的棧信息是一個獨佔鎖操作,這便是引起它非常耗時的一個原因,如果我們在開發時不需要關注棧信息,則可以將其覆蓋來提升性能,見下圖。

談談異常那點事

測試後,發現性能提升在 10 倍左右。

接著,咱們通過一張圖來了解一下 Java 的異常結構「這張圖很重要,大多數面試題都是圍繞這張圖展開的」。

談談異常那點事

我們可以發現 Java 的異常對象的基類是 Throwable,然後由 Throwable 延展開來 Error 和 Exception,Exception 裡面又分為 Runtime Exception 和 Checked Exception 兩大類別。

為什麼說這裡的考點特別多呢?比如下面一些問題:

  1. 你實際開發中經常碰到哪些 Error 和 Exception?

  2. ClassNotFoundException 和 NoClassDefFoundError的區別?

  3. JVM 的哪一個內存區不會拋異常?其它內存區會拋哪些異常?

  4. 等等

對於這類型的問題還有很多,出題者的目的是要考察一個人對 Java 研究的深度和廣度,而真正能回答出來了,要麼真的很厲害,要麼就是背書過後,所以我們要經常總結學過的知識點,才好在以後的面試中游刃有餘。

怎麼回答上面的問題呢?以第一個問題為例,如果是我的話,我一般會從 JVM 的內存區的角度出發來描述每一個內存區「棧、堆、方法區或者直接內存」會產生哪些異常,並且結合線上 JVM 調優的一些案例來詳細闡述這個異常為什麼引起的,怎麼進行分析和排查的;如果你再繼續抓住一個框架「比如Dubbo、Spring」來詳細闡述它內部是怎麼處理異常,甚至還可以聊聊異常鏈追蹤系統「zipkin」的實現原理,聊到這裡,大多數面試官基本上都滿意了。

在我看來,面試跟相親差不多。面試官應提前瞭解應聘者,結合面試者最擅長的東西來探討,而不是一副欠了你錢的姿態...

三、JVM 怎麼處理異常流程的?

一般來說,異常發生時,會在堆上生成一個異常對象「包含當前棧幀的快照」;然後停止當前的執行流程,將上面的異常對象從當前的 context 丟出「便於卸掉解決問題的職責」;此刻便由異常處理機制接手,尋找能繼續執行的適當地點「即異常處理函數」,使程序繼續執行。

下面咱們通過一個例子來具體說明,見下圖:

談談異常那點事

咱們再通過 javap 工具來查看它生成的字節碼,如下圖:

談談異常那點事

從字節碼角度,我們可以發現會生成一個異常表。try 的範圍體現在異常錶行記錄的起點和終點。JVM 在 try 中捕獲到異常,就會在當前棧幀的異常表中找到相匹配的異常入口指令號,然後跳轉到該指令執行;異常指令執行完成再執行後面的代碼。如果異常表中沒有發現匹配的項,JVM會將當前棧幀從棧中彈出並拋出異常,交給異常處理機制。

其實我覺得異常處理裡面最神秘的關鍵字便是 throw,為什麼這麼說呢?通過上面的例子,我們可以發現 throw 也會返回一個異常對象,但它的返回和函數的正常返回卻又天壤之別,throw 背後會引發 JVM 進行一系列的異常處理操作,正所謂失之毫釐,差之千里。

athrow 指令的關鍵代碼實現見代碼:hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp。

有的小夥伴會問你怎麼知道它的實現在這裡呢?這要感謝 RednaxelaFX 大神寫的怎麼閱讀 openjdk 源碼的一系列文章。

查閱源碼可以發現它使用 while(1) 的方式循環 swtich 寄存器指令。

談談異常那點事

我們可以發現 hotspot 源碼的底層實現跟我們上面描述的一致,見下圖:

談談異常那點事

_athrow case語句內部,第一句提取操作棧中引用的異常對象,第二句檢測異常是否為空,第三局為當前線程設置異常對象。最後跳轉到 handle_exception 標識。

在 handle_exception 代碼塊中,我畫紅線標記的地方值得仔細品味,第一處便是 CALL_VM() 函數的調用,它主要用於異常表的查找,具體實現在函數 InterpreterRuntime::exception_handler_for_exception()。如果找到,則if (continuation_bci >= 0) 這句成立,其中 bci 表示字節索引,則進入 SET_STACK_OBJECT(except_oop(), 0) 把異常對象重新入棧,pc = METHOD->code_base() + continuation_bci 表示重置PC指針為異常handler的起始位置,然後跳轉到run處開始下一輪的循環switch過程。如果沒有找到,則重新設置 pending_exception,具體實現見 THREAD->set_pending_exception(except_oop(), NULL, 0)。

在 handle_return 代碼塊中會根據 pending_exception 這個標誌來決定方法是否出現異常,要不要退出,見下圖畫紅線的代碼。

談談異常那點事

到這裡,我們通過源碼大概理解了 JVM 底層到底怎麼來處理異常的了。既然聊到這裡,我想提醒同學們,瞭解理解貫通之間是有距離的,需要我們對一個問題不斷的回過頭來思辨和深挖。

四、異常的設計原則

擼了多年 Java 代碼的老手都知道捕獲異常的最佳時機便是編譯期,但大多數的異常都需要在運行期才能發現,並且很難恢復。

那到底該怎麼設計異常呢?其實網上有很多朋友在討論,特別是 03 年對“為什麼 Java 中要使用 Checked Exceptions ”這個主題,在網上鬧的沸沸揚揚,感興趣的小夥伴可以去搜一下。

本人從《Effective Java》這本經典著作,對異常設計的原則進行了大概的歸納,如下:

  • 不要忽略異常。

  • 對於可以恢復的情況使用檢查異常,對於編程中的錯誤使用運行異常。

  • 異常是為異常流程設計的,不要將它們用於普通控制流程。

  • 優先使用標準異常。

  • 拋出與抽象相對應的異常。

  • 每個方法拋出的異常都要有文檔。

  • 在細節消息中包含能捕獲失敗的信息。

  • 努力使失敗保持原子性。

五、引申問題

  • Spring MVC 的異常處理機制是怎麼樣的?

  • Dubbo 底層怎麼處理異常的?

  • 分佈式服務的異常鏈路追蹤是怎麼實現的?

上面這些問題,我會全部放在我的知識星球上面,跟星球裡面的同學一起討論,如果感興趣的話,可以加入我的知識星球來分享「 http://t.xiaomiquan.com/6EIQ7Y3 」。

六、參考

  • 《Effective Java》

  • 《Java 編程思想》

  • 《Java 核心技術 卷I》

  • http://openjdk.java.net

  • http://www.iteye.com/topic/2038

技術的樂趣在於分享而不是獨享,獨樂樂不如眾樂樂。如果覺得我的文字不錯,可以隨意打賞 ~~~


分享到:


相關文章: