原創:微信公眾號 【阿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還可以加入技術交流群參與討論呦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/418080.html
標籤:其他
