原創:微信公衆號 【阿Q說代碼】,歡迎分享,轉載請保留出處。
哈嘍大家好,我是阿Q!
剛剛面試回來的B哥又在吐槽了:現在的面試官太難伺候了,放着好好的堆、棧、方法區不問,上來就讓我從字節碼角度給他分析一下try-catch-finally
(以下簡稱TCF)的執行效率......
我覺得應該是面試官在面試的過程中看大家背的八股文都如出一轍,覺得沒有問的必要,便拐着彎的考大家的理解。今天趁着B哥也在,我們就來好好總結一下TCF相關的知識點,期待下次與面試官對線五五開!
環境準備: IntelliJ IDEA 2020.2.3、JDK 1.8.0_181
執行順序
我們先來寫一段簡單的代碼:
public static int test1() {
int x = 1;
try {
return x;
} finally {
x = 2;
}
}
答案是1不是2,你答對了嗎?
大家都知道在TCF中,執行到return
的時候會先去執行finally
中的操作,然後纔會返回來執行return
,那這裏爲啥會是1呢?我們來反編譯一下字節碼文件。
命令:javap -v xxx.class
字節碼指令晦澀難懂,那我們就用圖解的方式來解釋一下(我們先只看前7行指令):首先執行 int x = 1;
然後我們需要執行try
中的return x;
此時並不是真正的返回x
的值,而是將x
的值存到局部變量表中作爲臨時存儲變量進行存儲,也就是對該值進行保護操作。
最後進入finally
中執行x=2;
此時雖然x
已經被賦值爲2了,但是由於剛纔的保護操作,在執行真正的return
操作時,會將被保護的臨時存儲變量入棧返回。
爲了更好的理解上述操作,我們再來寫一段簡單代碼:
public static int test2() {
int x = 1;
try {
return x;
} finally {
x = 2;
return x;
}
}
大家思考一下執行結果是幾?答案是2不是1。
我們再來看下該程序的字節碼指令
通過對比發現,第6行一個是iload_1
,一個是iload_0
,這是由什麼決定的呢?原因就是我們上邊提到的保護機制,當在finally
中存在return
語句時,保護機制便會失效,轉而將變量的值入棧並返回。
小結
-
return
的執行優先級高於finally
的執行優先級,但是return
語句執行完畢之後並不會馬上結束函數,而是將結果保存到棧幀中的局部變量表中,然後繼續執行finally
塊中的語句; - 如果
finally
塊中包含return
語句,則不會對try
塊中要返回的值進行保護,而是直接跳到finally
語句中執行,並最後在finally
語句中返回,返回值是在finally
塊中改變之後的值;
finally 爲什麼一定會執行
細心地小夥伴應該能發現,上邊的字節碼指令圖中第4-7行和第9-12行的字節碼指令是完全一致的,那麼爲什麼會出現重複的指令呢?
首先我們來分析一下這些重複的指令都做了些什麼操作,經過分析發現它們就是x = 2;return x;
的字節碼指令,也就是finally
代碼塊中的代碼。由此我們有理由懷疑如果上述代碼中加入catch
代碼塊,finally
代碼塊對應的字節碼指令也會再次出現。
public static int test2() {
int x = 1;
try {
return x;
} catch(Exception e) {
x = 3;
} finally {
x = 2;
return x;
}
}
反編譯之後
果然如我們所料,重複的字節碼指令出現了三次。讓我們迴歸到最初的問題上,爲什麼finally
代碼的字節碼指令會重複出現三次呢?
原來是JVM
爲了保證所有異常路徑和正常路徑的執行流程都要執行finally
中的代碼,所以在try
和catch
後追加上了finally
中的字節碼指令,再加上它自己本身的指令,正好三次。這也就是爲什麼finally
一定會執行的原因。
finally一定會執行嗎?
爲什麼上邊已經說了finally
中的代碼一定會執行,現在還要再多此一舉呢?請👇看
在正常情況下,它是一定會被執行的,但是至少存在以下三種情況,是一定不執行的:
-
try
語句沒有被執行到就返回了,這樣finally
語句就不會執行,這也說明了finally
語句被執行的必要而非充分條件是:相應的try
語句一定被執行到; -
try
代碼塊中有System.exit(0);
這樣的語句,因爲System.exit(0);
是終止JVM
的,連JVM
都停止了,finally
肯定不會被執行了; - 守護線程會隨着所有非守護線程的退出而退出,當守護線程內部的
finally
的代碼還未被執行到,非守護線程終結或退出時,finally
肯定不會被執行;
TCF 的效率問題
說起TCF的效率問題,我們不得不介紹一下異常表,拿上邊的程序來說,反編譯class
文件後的異常表信息如下:
- from:代表異常處理器所監控範圍的起始位置;
- to:代表異常處理器所監控範圍的結束位置(該行不被包括在監控範圍內,是前閉後開區間);
- target:指向異常處理器的起始位置;
- type:代表異常處理器所捕獲的異常類型;
圖中每一行代表一個異常處理器
工作流程:
- 觸發異常時,
JVM
會從上到下遍歷異常表中所有的條目; - 比較觸發異常的行數是否在
from
-to
範圍內; - 範圍匹配之後,會繼續比較拋出的異常類型和異常處理器所捕獲的異常類型
type
是否相同; - 如果類型相同,會跳轉到
target
所指向的行數開始執行; - 如果類型不同,會彈出當前方法對應的
java
棧幀,並對調用者重複操作; - 最壞的情況下
JVM
需要遍歷該線程Java
棧上所有方法的異常表;
拿第一行爲例:如果位於2-4行之間的命令(即try
塊中的代碼)拋出了Class java/lang/Exception
類型的異常,則跳轉到第8行開始執行。
8: astore_1
是指將拋出的異常對象保存到局部變量表中的1位置處
從字節碼指令的角度來講,如果代碼中沒有異常拋出,TCF的執行時間可以忽略不計;如果代碼執行過程中出現了上文中的第6條,那麼隨着異常表的遍歷,更多的異常實例被構建出來,異常所需要的棧軌跡也在生成。該操作會逐一訪問當前線程的棧幀,記錄各種調試信息,包括類名、方法名、觸發異常的代碼行數等等。所以執行效率會大大降低。
看到這兒,你是否對TCF有了更加深入的瞭解呢?下次讓你對線面試官,你會五五開嗎?如果你有不同的意見或者更好的idea
,歡迎聯繫阿Q,添加阿Q還可以加入技術交流羣參與討論呦!