<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從JVM異常表和字節(jié)碼角度分析try-catch-finally為什么效率低

          共 8426字,需瀏覽 17分鐘

           ·

          2021-08-23 12:26

          你知道的越多,不知道的就越多,業(yè)余的像一棵小草!

          你來,我們一起精進(jìn)!你不來,我和你的競(jìng)爭(zhēng)對(duì)手一起精進(jìn)!

          編輯:業(yè)余草

          推薦:https://www.xttblog.com/?p=5261

          有經(jīng)驗(yàn)的 Java 老司機(jī)可能告訴過你,Java 中的 try-catch-finally代碼塊范圍不要包的太大,因?yàn)樗赡軙?huì)影響 Java 程序的運(yùn)行效率

          你可能會(huì)百思不得其解其中是什么緣由?甚至是不少“老程序員”也搞不懂其中的機(jī)制!剛好最近我在 codereview 時(shí),要求同事縮小 try-catch-finally代碼塊的范圍,并從字節(jié)碼的角度給他們講了異常表以及 JVM 的處理機(jī)制。

          本文將我在公司內(nèi)部的講解整理成文稿,分享給大家!

          希望在這之后,不會(huì)有人再將下面這張表情包發(fā)給你……

          try-catch-finally

          環(huán)境介紹

          • 本文內(nèi)容基于 jdk 1.8.0_127
          • IntelliJ IDEA 2018 以及 jclasslib 字節(jié)碼插件

          字節(jié)碼中的 try-catch

          Talk is cheap, show you my code!

          反編譯后的字節(jié)碼

          為了方便演示,我將整個(gè)業(yè)務(wù)代碼移除,簡(jiǎn)化成下面的 demo 代碼。

          下面是第一段測(cè)試代碼,這段代碼里有一個(gè) try-catch 代碼塊,每個(gè)代碼塊中都有一行輸出,然后會(huì)在 catch 代碼塊中捕獲 Exception 異常。

           public static void main(String[] args) {
                  try {
                      System.out.println("enter try block");
                  } catch (Exception e) {
                      System.out.println("enter catch block");
                  }
              }

          然后在命令行中先定位到這個(gè)類的字節(jié)碼文件目錄中,執(zhí)行主方法后敲下javap -c 類名(注意??,這里不要帶上文件的后綴名)進(jìn)行反編譯,或者直接在編譯器中選擇Build Project,然后打開 jclasslib 工具就可以看到這個(gè)類的字節(jié)碼。查看字節(jié)碼的方式比較多,不習(xí)慣使用 jclasslib 的網(wǎng)友可以安裝 idea 的其他插件。

          通過上面的操作后,main 方法對(duì)應(yīng)的字節(jié)碼如下圖所示:

          主方法的字節(jié)碼

          從上圖可以看出 0 ~ 3 行是 try 代碼塊中的輸出語句,12 ~ 17 行是 catch 代碼塊中的輸出語句。最后的第 20 行的 return 是所有方法都會(huì)存在的指令。

          業(yè)余草敲黑板,重點(diǎn)來了

          上圖中的第 8 行對(duì)應(yīng)的字節(jié)碼 goto,語義是8 goto 20,即這個(gè)字節(jié)碼指令就是從 8 跳轉(zhuǎn)到第 20 行的意思,前提是第 5 行的輸出代碼有異常。說白了,就是 try 代碼塊中如果沒有出現(xiàn)異常,那么就跳轉(zhuǎn)到第 20 行,也就是整個(gè)方法行完成后執(zhí)行 return 了。

          這是一段再正常不過的代碼了,假設(shè)運(yùn)行過程中出現(xiàn)異常了,那么虛擬機(jī)是如何知道應(yīng)該處理 try 代碼塊的呢?JVM 又是如何知道該捕獲何種異常的呢?

          答案就是:異常表 Exception table。

          異常表

          當(dāng)一個(gè)類被編譯成字節(jié)碼之后,它的每個(gè)方法中只要有 try-catch 都會(huì)有一張異常表。異常表中包含了“監(jiān)控”的范圍,“監(jiān)控”各種異常以及拋出異常后去哪里處理。比如上述的示例代碼,在 jclasslib 中它的異常表如下圖。

          jclasslib查看字節(jié)碼異常表

          或者在javap -c命令下異常表是這樣的:

          Exception table:
             from    to  target type
                 0     8    11   Class java/lang/Exception

          無論是哪種形式的異常表,可以確定的是,異常表中每一行就代表一個(gè)異常處理器。

          下面解釋一下 jclasslib 異常表中,各個(gè)列所代表的含義。

          • Nr. 列:代表異常處理器的序號(hào)
          • Start PC (也就是 from):代表異常處理器所監(jiān)控范圍的起始位置
          • End PC (對(duì)應(yīng) to):代表異常處理器所監(jiān)控范圍的結(jié)束位置(注意:該行不被包括在監(jiān)控范圍內(nèi),一般是 goto 指令。就是一個(gè)前包區(qū)間"[)")
          • Handler PC (對(duì)應(yīng) target):指向異常處理器的起始位置,在這里就是 catch 代碼塊的起始位置。
          • Catch Type (對(duì)應(yīng) type):代表異常處理器所捕獲的異常類型。如 Exception,any 等。

          如果程序觸發(fā)了異常,Java 虛擬機(jī)會(huì)按照序號(hào)遍歷異常表,當(dāng)觸發(fā)的異常在這條異常處理器的監(jiān)控范圍內(nèi)(from 和 to),且異常類型(type)與該異常處理器一致時(shí),Java 虛擬機(jī)就會(huì)跳轉(zhuǎn)到該異常處理器的起始位置(target)開始執(zhí)行字節(jié)碼。

          如果程序沒有觸發(fā)異常,那么虛擬機(jī)會(huì)使用 goto 指令跳過 catch 代碼塊,執(zhí)行 finally 語句或者方法返回。

          字節(jié)碼中的 finally

          接下來在上述的代碼中再加入一個(gè) finally 代碼塊,然后再次執(zhí)行反編譯的命令看看有什么不一樣。

          // 源代碼
          public static void main(String[] args) {
                  try {
                      // dosomething
                      System.out.println("enter try block");
                  } catch (Exception e) {
                      System.out.println("enter catch block");
                  } finally {
                      System.out.println("enter finally block");
                  }
              }

          上面這段 Java 源碼對(duì)應(yīng)的字節(jié)碼如下:

          // 字節(jié)碼
           0 getstatic #2     <java/lang/System.out>
           3 ldc #3           <enter try block>
           5 invokevirtual #4 <java/io/PrintStream.println>
           8 getstatic #2     <java/lang/System.out>
          11 ldc #5           <enter finally block>
          13 invokevirtual #4 <java/io/PrintStream.println>
          16 goto 50 (+34)
          19 astore_1
          20 getstatic #2     <java/lang/System.out>
          23 ldc #7           <enter catch block>
          25 invokevirtual #4 <java/io/PrintStream.println>
          28 getstatic #2     <java/lang/System.out>
          31 ldc #5           <enter finally block>
          33 invokevirtual #4 <java/io/PrintStream.println>
          36 goto 50 (+14)
          39 astore_2
          40 getstatic #2     <java/lang/System.out>
          43 ldc #5           <enter finally block>
          45 invokevirtual #4 <java/io/PrintStream.println>
          48 aload_2
          49 athrow
          50 return

          finally 代碼塊在當(dāng)前版本(jdk 1.8)的 JVM 中的處理機(jī)制是比較特殊的。從上面的字節(jié)碼中也可以明顯看到,只是加了一個(gè) finally 代碼塊而已,字節(jié)碼指令增加了很多行,goto 和 ldc 指令分別出現(xiàn)了多次。指令的增多,意味著棧軌跡復(fù)雜化了,效率或多或少有些影響。

          如果再仔細(xì)觀察一下,我們可以發(fā)現(xiàn)。在字節(jié)碼指令中,有三塊重復(fù)的字節(jié)碼指令,分別是 8 ~ 13 行、28 ~ 33 行和 40 ~ 45 行,如果對(duì)字節(jié)碼有些了解的同學(xué)或許已經(jīng)知道了,這三塊重復(fù)的字節(jié)碼就是 finally 代碼塊對(duì)應(yīng)的代碼。

          出現(xiàn)三塊重復(fù)字節(jié)碼指令的原因是在 JVM 虛擬機(jī)中,所有異常路徑(如 try、catch)以及所有正常執(zhí)行路徑的出口都會(huì)被附加一份 finally 代碼塊。也就是說,在上述的示例代碼中,try 代碼塊后面會(huì)跟著一份 finally 的代碼,catch 代碼塊后面也是如此,再加上原本正常流程會(huì)執(zhí)行的 finally 代碼塊,共有 3 個(gè)地方會(huì)出現(xiàn) finally,因此在字節(jié)碼中一共也有三份 finally 代碼塊代碼塊。

          而針對(duì)每一條可能出現(xiàn)的異常的路徑,JVM 都會(huì)在異常表中多生成一條異常處理器,用來監(jiān)控整個(gè) try-catch 代碼塊,同時(shí)它會(huì)捕獲所有種類的異常,并且在執(zhí)行完 finally 代碼塊之后會(huì)重新拋出剛剛捕獲的異常。

          上述示例代碼的異常表如下:

          Exception table:
             from    to  target type
                 0     8    19   Class java/lang/Exception
                 0     8    39   any
                19    28    39   any

          可以看到與原來相比異常表增加了兩條,第 2 條異常處理器異常監(jiān)控 try 代碼塊,第 3 條異常處理器監(jiān)控 catch 代碼塊,如果出現(xiàn)異常則會(huì)跳轉(zhuǎn)到第39行的 finally 代碼塊執(zhí)行。

          這就是 finally 一定會(huì)在 try-catch 代碼塊之后執(zhí)行的原因了(某些能中斷程序運(yùn)行的操作除外:95% 的人都答錯(cuò)的一道阿里面試題:finally 中的代碼一定會(huì)被執(zhí)行嗎?)。

          如果 finally 也拋出異常

          上文說到虛擬機(jī)會(huì)對(duì)整個(gè) try-catch 代碼塊生成一個(gè)或多個(gè)異常處理器,如果在 catch 代碼塊中拋出了異常,這個(gè)異常會(huì)被捕獲,并且在執(zhí)行完 finally 代碼塊之后被重新拋出。

          那么在這里有一個(gè)額外的問題需要提及,假設(shè)在 catch 代碼塊中拋出了異常 A,當(dāng)執(zhí)行 finally 代碼塊時(shí)又拋出了異常 B,那么最后拋出的是什么異常呢?

          如果有同學(xué)自己嘗試過這個(gè)操作,就會(huì)知道最后拋出的異常 B。也就是說,在捕獲了 catch 代碼塊中的異常后,如果 finally 代碼塊中也拋出了異常,那么最終將會(huì)拋出 finally 中拋出的異常,而原來 catch 代碼塊中的異常將會(huì)被忽略。

          如果代碼塊中有 return

          講完了異常在各個(gè)代碼塊中的情況,接下來再來考慮一下 return 關(guān)鍵字吧。如果 try 或者 catch 中有 return,那么 finally 還會(huì)執(zhí)行嗎?如果 finally 中也有 return,那么最終返回的值是什么?這是很多初級(jí)程序員面試必考的問題,想象大家初入職場(chǎng)的時(shí)候,都被拷問過。

          下面為了說明這個(gè)問題,我們?cè)谕ㄟ^一段測(cè)試代碼,然后查看它的字節(jié)碼指令,通過指令來揭開它的面紗。

          public static int get() {
              try {
                  return 1;
              } catch (Exception e) {
                  return 2;
              } finally {
                  return 3;
              }
          }

          // 字節(jié)碼指令
           0 iconst_1
           1 istore_0
           2 iconst_3
           3 ireturn
           4 astore_0
           5 iconst_2
           6 istore_1
           7 iconst_3
           8 ireturn
           9 astore_2
          10 iconst_3
          11 ireturn

          正如上文所述,finally 代碼塊會(huì)在所有正常及異常的路徑上都復(fù)制一份,在這段字節(jié)碼中,iconst_3就是對(duì)應(yīng)著 finally 代碼塊,共三份,所以即便在 try 或者 catch 代碼塊中有 return 語句,最終還是會(huì)會(huì)執(zhí)行 finally 代碼塊中的內(nèi)容。除非有例外情況發(fā)生,參考:95% 的人都答錯(cuò)的一道阿里面試題:finally 中的代碼一定會(huì)被執(zhí)行嗎?

          也就是說,這個(gè)方法最終的返回結(jié)果是 3。

          下面整理了一個(gè)流程圖,分享給大家。通過查看流程圖加深理解。

          最后總結(jié)了幾個(gè)面試題,大家可以試著不看答案先回答一下,方便確認(rèn)是否真的理解了本文!

          為什么使用異常捕獲的代碼比較耗費(fèi)性能

          單從 Java 語法上看不出來,但是從 JVM 實(shí)現(xiàn)的細(xì)節(jié)上來看就明白了。構(gòu)造異常實(shí)例,需要生成該異常的棧軌跡。該操作會(huì)逐一訪問當(dāng)前線程的棧幀,記錄各種調(diào)試信息,包括類名,方法名,觸發(fā)異常的代碼行數(shù)等等。

          finally 是怎么實(shí)現(xiàn)無論異常與否都能執(zhí)行

          編譯器在編譯代碼時(shí)會(huì)復(fù)制 finally 代碼塊放在 try-catch 代碼塊所有正常執(zhí)行路徑以及異常執(zhí)行路徑的出口處。

          finally 中有 ruturn 語句,catch 中拋出的異常會(huì)被忽略,為什么

          catch 拋出的異常會(huì)被 finally 捕獲,執(zhí)行完 finally 后會(huì)重新拋出該異常。由于 finally 中有 return 語句,在重新拋出異常之前,代碼就已經(jīng)返回了。

          方法的異常表都包含哪些異常

          方法的異常表只聲明這段代碼會(huì)被捕獲的異常,而且是非檢查異常。如果 catch 中有自定義異常,那么異常表中也會(huì)包含自定義異常的條目。

          檢查異常和非檢查異常也就是其他書籍中說的編譯期異常和運(yùn)行時(shí)異常?

          檢查異常也會(huì)在運(yùn)行過程中拋出。但是它會(huì)要求編譯器檢查代碼有沒有顯式地處理該異常。非檢查異常包括 Error 和 RuntimeException,這兩個(gè)則不要求編譯器顯式處理。

          以上內(nèi)容,希望能夠幫助到大家。如果感覺本文內(nèi)容還可以,歡迎點(diǎn)贊??!

          瀏覽 72
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产日产久久高清欧美 | 亚洲成人无码网站 | 青草超碰| 十八禁网站免费看 | 影音先锋成人影院 |