<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>

          使用 try-catch 捕獲異常會影響性能嗎?

          共 28348字,需瀏覽 57分鐘

           ·

          2023-03-11 05:06

          程序員的成長之路
          互聯(lián)網/程序員/技術/資料共享 
          關注


          閱讀本文大概需要 12 分鐘。

          來自:blog.csdn.net/bokerr/article/details/122655795

          前言

          不知道從何時起,傳出了這么一句話:Java中使用try catch 會嚴重影響性能。
          然而,事實真的如此么?我們對try catch 應該畏之如猛虎么?

          一、JVM 異常處理邏輯

          Java 程序中顯式拋出異常由athrow指令支持,除了通過 throw 主動拋出異常外,JVM規(guī)范中還規(guī)定了許多運行時異常會在檢測到異常狀況時自動拋出(效果等同athrow), 例如除數(shù)為0時就會自動拋出異常,以及大名鼎鼎的 NullPointerException 。
          還需要注意的是,JVM 中 異常處理的catch語句不再由字節(jié)碼指令來實現(xiàn)(很早之前通過 jsr和 ret指令來完成,它們在很早之前的版本里就被舍棄了),現(xiàn)在的JVM通過異常表(Exception table 方法體中能找到其內容)來完成 catch 語句;很多人說try catch 影響性能可能就是因為認識還停留于上古時代。
          1.我們編寫如下的類,add 方法中計算 ++x; 并捕獲異常。

          public class TestClass {
              private static int len = 779;
              public int add(int x){
                  try {
                      // 若運行時檢測到 x = 0,那么 jvm會自動拋出異常,(可以理解成由jvm自己負責 athrow 指令調用)
                      x = 100/x;
                  } catch (Exception e) {
                      x = 100;
                  }
                  return x;
              }
          }

          2.使用javap 工具查看上述類的編譯后的class文件

           # 編譯
           javac TestClass.java
           # 使用javap 查看 add 方法被編譯后的機器指令
           javap -verbose TestClass.class

          忽略常量池等其他信息,下邊貼出add 方法編譯后的 機器指令集:

          public int add(int);
            descriptor: (I)I
            flags: ACC_PUBLIC
            Code:
              stack=2, locals=3, args_size=2
                 0: bipush        100   //  加載參數(shù)100
                 2: iload_1             //  將一個int型變量推至棧頂
                 3: idiv                //  相除
                 4: istore_1            //  除的結果值壓入本地變量
                 5: goto          11    //  跳轉到指令:11
                 8: astore_2            //  將引用類型值壓入本地變量
                 9: bipush        100   //  將單字節(jié)常量推送棧頂<這里與數(shù)值100有關,可以嘗試修改100后的編譯結果:iconst、bipush、ldc> 
                10: istore_1            //  將int類型值壓入本地變量
                11: iload_1             //  int 型變量推棧頂
                12: ireturn             //  返回
              // 注意看 from 和 to 以及 targer,然后對照著去看上述指令
              Exception table:
                 from    to  target type
                     0     5     8   Class java/lang/Exception
              LineNumberTable:
                line 60
                line 95
                line 78
                line 89
                line 1011
              StackMapTable: number_of_entries = 2
                frame_type = 72 /* same_locals_1_stack_item */
                  stack = [ class java/lang/Exception ]
                frame_type 
          2 /* same */

          再來看 Exception table:
          from=0, to=5。 指令 0~5 對應的就是 try 語句包含的內容,而targer = 8 正好對應 catch 語句塊內部操作。
          個人理解,from 和 to 相當于劃分區(qū)間,只要在這個區(qū)間內拋出了type 所對應的,“java/lang/Exception” 異常(主動athrow 或者 由jvm運行時檢測到異常自動拋出),那么就跳轉到target 所代表的第八行。
          若執(zhí)行過程中,沒有異常,直接從第5條指令跳轉到第11條指令后返回,由此可見未發(fā)生異常時,所謂的性能損耗幾乎不存在;
          如果硬是要說的話,用了try catch 編譯后指令篇幅變長了;goto 語句跳轉會耗費性能,當你寫個數(shù)百行代碼的方法的時候,編譯出來成百上千條指令,這時候這句goto的帶來的影響顯得微乎其微。
          如圖所示為去掉try catch 后的指令篇幅,幾乎等同上述指令的前五條。
          綜上所述:“Java中使用try catch 會嚴重影響性能” 是民間說法,它并不成立。如果不信,接著看下面的測試吧。

          二、關于JVM的編譯優(yōu)化

          其實寫出測試用例并不是很難,這里我們需要重點考慮的是編譯器的自動優(yōu)化,是否會因此得到不同的測試結果?
          本節(jié)會粗略的介紹一些jvm編譯器相關的概念,講它只為更精確的測試結果,通過它我們可以窺探 try catch 是否會影響JVM的編譯優(yōu)化。
          • 前端編譯與優(yōu)化: 我們最常見的前端編譯器是 javac,它的優(yōu)化更偏向于代碼結構上的優(yōu)化,它主要是為了提高程序員的編碼效率,不怎么關注執(zhí)行效率優(yōu)化;例如,數(shù)據流和控制流分析、解語法糖等等。
          • 后端編譯與優(yōu)化: 后端編譯包括 “即時編譯[JIT]” 和 “提前編譯[AOT]”,區(qū)別于前端編譯器,它們最終作用體現(xiàn)于運行期,致力于優(yōu)化從字節(jié)碼生成本地機器碼的過程(它們優(yōu)化的是代碼的執(zhí)行效率)。

          1. 分層編譯

          PS * JVM 自己根據宿主機決定自己的運行模式, “JVM 運行模式”;[客戶端模式-Client、服務端模式-Server],它們代表的是兩個不同的即時編譯器,C1(Client Compiler) 和 C2 (Server Compiler)。
          PS * 分層編譯分為:“解釋模式”、“編譯模式”、“混合模式”;
          • 解釋模式下運行時,編譯器不介入工作;
          • 編譯模式模式下運行,會使用即時編譯器優(yōu)化熱點代碼,有可選的即時編譯器[C1 或 C2];
          • 混合模式為:解釋模式和編譯模式搭配使用。
          如圖,我的環(huán)境里JVM 運行于 Server 模式,如果使用即時編譯,那么就是使用的:C2 即時編譯器。

          2. 即時編譯器

          了解如下的幾個 概念:

          1. 解釋模式

          它不使用即時編譯器進行后端優(yōu)化
          • 強制虛擬機運行于 “解釋模式” -Xint
          • 禁用后臺編譯 -XX:-BackgroundCompilation

          2. 編譯模式

          即時編譯器會在運行時,對生成的本地機器碼進行優(yōu)化,其中重點關照熱點代碼。

          # 強制虛擬機運行于 "編譯模式"
          -Xcomp
          # 方法調用次數(shù)計數(shù)器閾值,它是基于計數(shù)器熱點代碼探測依據[Client模式=1500,Server模式=10000]
          -XX:CompileThreshold=10
          # 關閉方法調用次數(shù)熱度衰減,使用方法調用計數(shù)的絕對值,它搭配上一配置項使用
          -XX:-UseCounterDecay
          # 除了熱點方法,還有熱點回邊代碼[循環(huán)],熱點回邊代碼的閾值計算參考如下:
          -XX:BackEdgeThreshold  = 方法計數(shù)器閾值[-XX:CompileThreshold] * OSR比率[-XX:OnStackReplacePercentage]
          # OSR比率默認值:Client模式=933,Server模式=140
          -XX:OnStackReplacePercentag=100

          所謂 “即時”,它是在運行過程中發(fā)生的,所以它的缺點也也明顯:在運行期間需要耗費資源去做性能分析,也不太適合在運行期間去大刀闊斧的去做一些耗費資源的重負載優(yōu)化操作。

          3. 提前編譯器:jaotc

          它是后端編譯的另一個主角,它有兩個發(fā)展路線,基于Graal [新時代的主角] 編譯器開發(fā),因為本文用的是 C2 編譯器,所以只對它做一個了解;
          第一條路線:與傳統(tǒng)的C、C++編譯做的事情類似,在程序運行之前就把程序代碼編譯成機器碼;好處是夠快,不占用運行時系統(tǒng)資源,缺點是"啟動過程" 會很緩慢;
          第二條路線:已知即時編譯運行時做性能統(tǒng)計分析占用資源,那么,我們可以把其中一些耗費資源的編譯工作,放到提前編譯階段來完成啊,最后在運行時即時編譯器再去使用,那么可以大大節(jié)省即時編譯的開銷;這個分支可以把它看作是即時編譯緩存;
          遺憾的是它只支持 G1 或者 Parallel 垃圾收集器,且只存在JDK 9 以后的版本,暫不需要去關注它;JDK 9 以后的版本可以使用這個參數(shù)打印相關信息:[-XX:PrintAOT]。

          三、關于測試的約束

          執(zhí)行用時統(tǒng)計

          System.naoTime() 輸出的是過了多少時間[微秒:10的負9次方秒],并不是完全精確的方法執(zhí)行用時的合計,為了保證結果準確性,測試的運算次數(shù)將拉長到百萬甚至千萬次。

          編譯器優(yōu)化的因素

          上一節(jié)花了一定的篇幅介紹編譯器優(yōu)化,這里我要做的是:對比完全不使用任何編譯優(yōu)化,與使用即時編譯時,try catch 對的性能影響。
          • 通過指令禁用 JVM 的編譯優(yōu)化,讓它以最原始的狀態(tài)運行,然后看有無 try catch 的影響。
          • 通過指令使用即時編譯,盡量做到把后端優(yōu)化拉滿,看看 try catch 十有會影響到 jvm的編譯優(yōu)化。

          關于指令重排序

          目前尚未可知 try catch 的使用影響指令重排序;
          我們這里的討論有一個前提,當 try catch 的使用無法避免時,我們應該如何使用 try catch 以應對它可能存在的對指令重排序的影響。
          • 指令重排序發(fā)生在多線程并發(fā)場景,這么做是為了更好的利用CPU資源,在單線程測試時不需要考慮。不論如何指令重排序,都會保證最終執(zhí)行結果,與單線程下的執(zhí)行結果相同;
          • 雖然我們不去測試它,但是也可以進行一些推斷,參考 volatile 關鍵字禁止指令重排序的做法:插入內存屏障;
          • 假定 try catch 存在屏障,導致前后的代碼分割;那么最少的try catch代表最少的分割。
          • 所以,是不是會有這樣的結論呢:我們把方法體內的 多個 try catch 合并為一個 try catch 是不是反而能減少屏障呢?這么做勢必造成 try catch 的范圍變大。
          當然,上述關于指令重排序討論內容都是基于個人的猜想,猶未可知 try catch 是否影響指令重排序;本文重點討論的也只是單線程環(huán)境下的 try catch 使用影響性能。

          四、測試代碼

          循環(huán)次數(shù)為100W ,循環(huán)內10次預算[給編譯器優(yōu)化預留優(yōu)化的可能,這些指令可能被合并]
          每個方法都會到達千萬次浮點計算。
          同樣每個方法外層再循環(huán)跑多次,最后取其中的眾數(shù)更有說服力。

          public class ExecuteTryCatch {

              // 100W 
              private static final int TIMES = 1000000;
              private static final float STEP_NUM = 1f;
              private static final float START_NUM = Float.MIN_VALUE;


              public static void main(String[] args){
                  int times = 50;
                  ExecuteTryCatch executeTryCatch = new ExecuteTryCatch();
                  // 每個方法執(zhí)行 50 次
                  while (--times >= 0){
                      System.out.println("times=".concat(String.valueOf(times)));
                      executeTryCatch.executeMillionsEveryTryWithFinally();
                      executeTryCatch.executeMillionsEveryTry();
                      executeTryCatch.executeMillionsOneTry();
                      executeTryCatch.executeMillionsNoneTry();
                      executeTryCatch.executeMillionsTestReOrder();
                  }
              }

              /**
               * 千萬次浮點運算不使用 try catch
               * */

              public void executeMillionsNoneTry(){
                  float num = START_NUM;
                  long start = System.nanoTime();
                  for (int i = 0; i < TIMES; ++i){
                      num = num + STEP_NUM + 1f;
                      num = num + STEP_NUM + 2f;
                      num = num + STEP_NUM + 3f;
                      num = num + STEP_NUM + 4f;
                      num = num + STEP_NUM + 5f;
                      num = num + STEP_NUM + 1f;
                      num = num + STEP_NUM + 2f;
                      num = num + STEP_NUM + 3f;
                      num = num + STEP_NUM + 4f;
                      num = num + STEP_NUM + 5f;
                  }
                  long nao = System.nanoTime() - start;
                  long million = nao / 1000000;
                  System.out.println("noneTry   sum:" + num + "  million:" + million + "  nao: " + nao);
              }

              /**
               * 千萬次浮點運算最外層使用 try catch
               * */

              public void executeMillionsOneTry(){
                  float num = START_NUM;
                  long start = System.nanoTime();
                  try {
                      for (int i = 0; i < TIMES; ++i){
                          num = num + STEP_NUM + 1f;
                          num = num + STEP_NUM + 2f;
                          num = num + STEP_NUM + 3f;
                          num = num + STEP_NUM + 4f;
                          num = num + STEP_NUM + 5f;
                          num = num + STEP_NUM + 1f;
                          num = num + STEP_NUM + 2f;
                          num = num + STEP_NUM + 3f;
                          num = num + STEP_NUM + 4f;
                          num = num + STEP_NUM + 5f;
                      }
                  } catch (Exception e){

                  }
                  long nao = System.nanoTime() - start;
                  long million = nao / 1000000;
                  System.out.println("oneTry    sum:" + num + "  million:" + million + "  nao: " + nao);
              }

              /**
               * 千萬次浮點運算循環(huán)內使用 try catch
               * */

              public void executeMillionsEveryTry(){
                  float num = START_NUM;
                  long start = System.nanoTime();
                  for (int i = 0; i < TIMES; ++i){
                      try {
                          num = num + STEP_NUM + 1f;
                          num = num + STEP_NUM + 2f;
                          num = num + STEP_NUM + 3f;
                          num = num + STEP_NUM + 4f;
                          num = num + STEP_NUM + 5f;
                          num = num + STEP_NUM + 1f;
                          num = num + STEP_NUM + 2f;
                          num = num + STEP_NUM + 3f;
                          num = num + STEP_NUM + 4f;
                          num = num + STEP_NUM + 5f;
                      } catch (Exception e) {

                      }
                  }
                  long nao = System.nanoTime() - start;
                  long million = nao / 1000000;
                  System.out.println("evertTry  sum:" + num + "  million:" + million + "  nao: " + nao);
              }


              /**
               * 千萬次浮點運算循環(huán)內使用 try catch,并使用 finally
               * */

              public void executeMillionsEveryTryWithFinally(){
                  float num = START_NUM;
                  long start = System.nanoTime();
                  for (int i = 0; i < TIMES; ++i){
                      try {
                          num = num + STEP_NUM + 1f;
                          num = num + STEP_NUM + 2f;
                          num = num + STEP_NUM + 3f;
                          num = num + STEP_NUM + 4f;
                          num = num + STEP_NUM + 5f;
                      } catch (Exception e) {

                      } finally {
                          num = num + STEP_NUM + 1f;
                          num = num + STEP_NUM + 2f;
                          num = num + STEP_NUM + 3f;
                          num = num + STEP_NUM + 4f;
                          num = num + STEP_NUM + 5f;
                      }
                  }
                  long nao = System.nanoTime() - start;
                  long million = nao / 1000000;
                  System.out.println("finalTry  sum:" + num + "  million:" + million + "  nao: " + nao);
              }

              /**
               * 千萬次浮點運算,循環(huán)內使用多個 try catch
               * */

              public void executeMillionsTestReOrder(){
                  float num = START_NUM;
                  long start = System.nanoTime();
                  for (int i = 0; i < TIMES; ++i){
                      try {
                          num = num + STEP_NUM + 1f;
                          num = num + STEP_NUM + 2f;
                      } catch (Exception e) { }

                      try {
                          num = num + STEP_NUM + 3f;
                          num = num + STEP_NUM + 4f;
                          num = num + STEP_NUM + 5f;
                      } catch (Exception e){}

                      try {
                          num = num + STEP_NUM + 1f;
                          num = num + STEP_NUM + 2f;
                      } catch (Exception e) { }
                      try {

                          num = num + STEP_NUM + 3f;
                          num = num + STEP_NUM + 4f;
                          num = num + STEP_NUM + 5f;
                      } catch (Exception e) {}
                  }
                  long nao = System.nanoTime() - start;
                  long million = nao / 1000000;
                  System.out.println("orderTry  sum:" + num + "  million:" + million + "  nao: " + nao);
              }

          }

          五、解釋模式下執(zhí)行測試

          設置如下JVM參數(shù),禁用編譯優(yōu)化

            -Xint
            -XX:-BackgroundCompilation

          結合測試代碼發(fā)現(xiàn),即使百萬次循環(huán)計算,每個循環(huán)內都使用了 try catch 也并沒用對造成很大的影響。
          唯一發(fā)現(xiàn)了一個問題,每個循環(huán)內都是使用 try catch 且使用多次。發(fā)現(xiàn)性能下降,千萬次計算差值為:5~7 毫秒;4個 try 那么執(zhí)行的指令最少4條goto ,前邊闡述過,這里造成這個差異的主要原因是 goto 指令占比過大,放大了問題;當我們在幾百行代碼里使用少量try catch 時,goto所占比重就會很低,測試結果會更趨于合理。

          六、編譯模式測試

          設置如下測試參數(shù),執(zhí)行10 次即為熱點代碼

             -Xcomp
             -XX:CompileThreshold=10
             -XX:-UseCounterDecay
             -XX:OnStackReplacePercentage=100
             -XX:InterpreterProfilePercentage=33        

          執(zhí)行結果如下圖,難分勝負,波動只在微秒級別,執(zhí)行速度也快了很多,編譯效果拔群啊,甚至連 “解釋模式” 運行時多個try catch 導致的,多個goto跳轉帶來的問題都給順帶優(yōu)化了;由此也可以得到 try catch 并不會影響即時編譯的結論。
          我們可以再上升到億級計算,依舊難分勝負,波動在毫秒級。

          七、結論

          try catch 不會造成巨大的性能影響,換句話說,我們平時寫代碼最優(yōu)先考慮的是程序的健壯性,當然大佬們肯定都知道了怎么合理使用try catch了,但是對萌新來說,你如果不確定,那么你可以使用 try catch;
          在未發(fā)生異常時,給代碼外部包上 try catch,并不會造成影響。
          舉個栗子吧,我的代碼中使用了:URLDecoder.decode,所以必須得捕獲異常。

          private int getThenAddNoJudge(JSONObject json, String key){
                  if (Objects.isNull(json))
                      throw new IllegalArgumentException("參數(shù)異常");
                  int num;
                  try {
                      // 不校驗 key 是否未空值,直接調用 toString 每次觸發(fā)空指針異常并被捕獲
                      num = 100 + Integer.parseInt(URLDecoder.decode(json.get(key).toString(), "UTF-8"));
                  } catch (Exception e){
                      num = 100;
                  }
                  return num;
              }

              private int getThenAddWithJudge(JSONObject json, String key){
                  if (Objects.isNull(json))
                      throw new IllegalArgumentException("參數(shù)異常");
                  int num;
                  try {
                      // 校驗 key 是否未空值
                      num = 100 + Integer.parseInt(URLDecoder.decode(Objects.toString(json.get(key), "0"), "UTF-8"));
                  } catch (Exception e){
                      num = 100;
                  }
                  return num;
              }

              public static void main(String[] args){
                  int times = 1000000;// 百萬次

                  long nao1 = System.nanoTime();
                  ExecuteTryCatch executeTryCatch = new ExecuteTryCatch();
                  for (int i = 0; i < times; i++){
                      executeTryCatch.getThenAddWithJudge(new JSONObject(), "anyKey");
                  }
                  long end1 = System.nanoTime();
                  System.out.println("未拋出異常耗時: millions=" + (end1 - nao1) / 1000000 + "毫秒  nao=" + (end1 - nao1) + "微秒");


                  long nao2 = System.nanoTime();
                  for (int i = 0; i < times; i++){
                      executeTryCatch.getThenAddNoJudge(new JSONObject(), "anyKey");
                  }
                  long end2 = System.nanoTime();
                  System.out.println("每次必拋出異常: millions=" + (end2 - nao2) / 1000000 + "毫秒  nao=" + (end2 - nao2) + "微秒");
              }

          調用方法百萬次,執(zhí)行結果如下:
          經過這個例子,我想你知道你該如何 編寫你的代碼了吧?可怕的不是 try catch 而是 搬磚業(yè)務不熟練啊。
          <END>

          推薦閱讀:

          告別卡頓困擾:IDEA 性能優(yōu)化設置

          被問懵了:MySQL 自增主鍵一定是連續(xù)的嗎?

          互聯(lián)網初中高級大廠面試題(9個G)

          內容包含Java基礎、JavaWeb、MySQL性能優(yōu)化、JVM、鎖、百萬并發(fā)、消息隊列、高性能緩存、反射、Spring全家桶原理、微服務、Zookeeper......等技術棧!

          ?戳閱讀原文領取!                                  朕已閱 

          瀏覽 68
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  美女扒开粉嫩尿囗桶爽免费网站 | 国产操逼电影大全 | 爱草视频 | 日韩精品人妻 | 天天干女人 |