前言
不知道從何時起,傳出了這么一句話:Java中使用try catch 會嚴重影響性能。
然而,事實真的如此么?我們對try catch 應(yīng)該畏之如猛虎么?
基于 Spring Boot + MyBatis Plus + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
一、JVM 異常處理邏輯
Java 程序中顯式拋出異常由athrow指令支持,除了通過 throw 主動拋出異常外,JVM規(guī)范中還規(guī)定了許多運行時異常會在檢測到異常狀況時自動拋出(效果等同athrow), 例如除數(shù)為0時就會自動拋出異常,以及大名鼎鼎的 NullPointerException
。
還需要注意的是,JVM 中 異常處理的catch語句不再由字節(jié)碼指令來實現(xiàn)(很早之前通過 jsr和 ret指令來完成,它們在很早之前的版本里就被舍棄了),現(xiàn)在的JVM通過異常表(Exception table
方法體中能找到其內(nèi)容)來完成 catch 語句;很多人說try catch
影響性能可能就是因為認識還停留于上古時代。
1.我們編寫如下的類,add 方法中計算 ++x;
并捕獲異常。
publicclassTestClass{
privatestaticintlen=779;
publicintadd(intx){
try{
//若運行時檢測到x=0,那么jvm會自動拋出異常,(可以理解成由jvm自己負責(zé)athrow指令調(diào)用)
x=100/x;
}catch(Exceptione){
x=100;
}
returnx;
}
}
2.使用javap
工具查看上述類的編譯后的class文件
#編譯
javacTestClass.java
#使用javap查看add方法被編譯后的機器指令
javap-verboseTestClass.class
忽略常量池等其他信息,下邊貼出add 方法編譯后的 機器指令集:
publicintadd(int);
descriptor:(I)I
flags:ACC_PUBLIC
Code:
stack=2,locals=3,args_size=2
0:bipush100//加載參數(shù)100
2:iload_1//將一個int型變量推至棧頂
3:idiv//相除
4:istore_1//除的結(jié)果值壓入本地變量
5:goto11//跳轉(zhuǎn)到指令:11
8:astore_2//將引用類型值壓入本地變量
9:bipush100//將單字節(jié)常量推送棧頂<這里與數(shù)值100有關(guān),可以嘗試修改100后的編譯結(jié)果:iconst、bipush、ldc>
10:istore_1//將int類型值壓入本地變量
11:iload_1//int型變量推棧頂
12:ireturn//返回
//注意看from和to以及targer,然后對照著去看上述指令
Exceptiontable:
fromtotargettype
058Classjava/lang/Exception
LineNumberTable:
line6:0
line9:5
line7:8
line8:9
line10:11
StackMapTable:number_of_entries=2
frame_type=72/*same_locals_1_stack_item*/
stack=[classjava/lang/Exception]
frame_type=2/*same*/
再來看 Exception table
:
from=0
, to=5
。指令 0~5 對應(yīng)的就是 try 語句包含的內(nèi)容,而targer = 8
正好對應(yīng) catch 語句塊內(nèi)部操作。
個人理解,from 和 to 相當于劃分區(qū)間,只要在這個區(qū)間內(nèi)拋出了type 所對應(yīng)的,“
java/lang/Exception
” 異常(主動athrow 或者 由jvm運行時檢測到異常自動拋出),那么就跳轉(zhuǎn)到target 所代表的第八行。
若執(zhí)行過程中,沒有異常,直接從第5條指令跳轉(zhuǎn)到第11條指令后返回,由此可見未發(fā)生異常時,所謂的性能損耗幾乎不存在;
如果硬是要說的話,用了
try catch
編譯后指令篇幅變長了;goto 語句跳轉(zhuǎn)會耗費性能,當你寫個數(shù)百行代碼的方法的時候,編譯出來成百上千條指令,這時候這句goto的帶來的影響顯得微乎其微。
如圖所示為去掉try catch
后的指令篇幅,幾乎等同上述指令的前五條。
綜上所述:“Java中使用try catch 會嚴重影響性能” 是民間說法,它并不成立。 如果不信,接著看下面的測試吧。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
二、關(guān)于JVM的編譯優(yōu)化
其實寫出測試用例并不是很難,這里我們需要重點考慮的是編譯器的自動優(yōu)化,是否會因此得到不同的測試結(jié)果?
本節(jié)會粗略的介紹一些jvm編譯器相關(guān)的概念,講它只為更精確的測試結(jié)果,通過它我們可以窺探 try catch 是否會影響JVM的編譯優(yōu)化。
前端編譯與優(yōu)化 :我們最常見的前端編譯器是 javac,它的優(yōu)化更偏向于代碼結(jié)構(gòu)上的優(yōu)化,它主要是為了提高程序員的編碼效率,不怎么關(guān)注執(zhí)行效率優(yōu)化;例如,數(shù)據(jù)流和控制流分析、解語法糖等等。
后端編譯與優(yōu)化 :后端編譯包括 “即時編譯[JIT]
” 和 “提前編譯[AOT]
”,區(qū)別于前端編譯器,它們最終作用體現(xiàn)于運行期,致力于優(yōu)化從字節(jié)碼生成本地機器碼的過程(它們優(yōu)化的是代碼的執(zhí)行效率)。
1. 分層編譯
PS * JVM 自己根據(jù)宿主機決定自己的運行模式, “JVM 運行模式”;[客戶端模式-Client、服務(wù)端模式-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)化,其中重點關(guān)照熱點代碼。
#強制虛擬機運行于"編譯模式"
-Xcomp
#方法調(diào)用次數(shù)計數(shù)器閾值,它是基于計數(shù)器熱點代碼探測依據(jù)[Client模式=1500,Server模式=10000]
-XX:CompileThreshold=10
#關(guān)閉方法調(diào)用次數(shù)熱度衰減,使用方法調(diào)用計數(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 以后的版本,暫不需要去關(guān)注它;JDK 9 以后的版本可以使用這個參數(shù)打印相關(guān)信息:[-XX:PrintAOT]
。
三、關(guān)于測試的約束
執(zhí)行用時統(tǒng)計
System.naoTime()
輸出的是過了多少時間[微秒:10的負9次方秒]
,并不是完全精確的方法執(zhí)行用時的合計,為了保證結(jié)果準確性,測試的運算次數(shù)將拉長到百萬甚至千萬次。
編譯器優(yōu)化的因素
上一節(jié)花了一定的篇幅介紹編譯器優(yōu)化,這里我要做的是:對比完全不使用任何編譯優(yōu)化,與使用即時編譯時,try catch 對的性能影響。
-
通過指令禁用 JVM 的編譯優(yōu)化,讓它以最原始的狀態(tài)運行,然后看有無
try catch
的影響。 -
通過指令使用即時編譯,盡量做到把后端優(yōu)化拉滿,看看
try catch
十有會影響到 jvm的編譯優(yōu)化。
關(guān)于指令重排序
目前尚未可知 try catch
的使用影響指令重排序;
我們這里的討論有一個前提,當 try catch
的使用無法避免時,我們應(yīng)該如何使用 try catch
以應(yīng)對它可能存在的對指令重排序的影響。
- 指令重排序發(fā)生在多線程并發(fā)場景,這么做是為了更好的利用CPU資源,在單線程測試時不需要考慮。不論如何指令重排序,都會保證最終執(zhí)行結(jié)果,與單線程下的執(zhí)行結(jié)果相同;
-
雖然我們不去測試它,但是也可以進行一些推斷,參考
volatile
關(guān)鍵字禁止指令重排序的做法:插入內(nèi)存屏障; -
假定
try catch
存在屏障,導(dǎo)致前后的代碼分割;那么最少的try catch
代表最少的分割。 -
所以,是不是會有這樣的結(jié)論呢:我們把方法體內(nèi)的 多個
try catch
合并為一個try catch
是不是反而能減少屏障呢?這么做勢必造成try catch
的范圍變大。
當然,上述關(guān)于指令重排序討論內(nèi)容都是基于個人的猜想,猶未可知 try catch
是否影響指令重排序;本文重點討論的也只是單線程環(huán)境下的 try catch
使用影響性能。
四、測試代碼
循環(huán)次數(shù)為100W ,循環(huán)內(nèi)10次預(yù)算[給編譯器優(yōu)化預(yù)留優(yōu)化的可能,這些指令可能被合并]
;
每個方法都會到達千萬次浮點計算。
同樣每個方法外層再循環(huán)跑多次,最后取其中的眾數(shù)更有說服力。
publicclassExecuteTryCatch{
//100W
privatestaticfinalintTIMES=1000000;
privatestaticfinalfloatSTEP_NUM=1f;
privatestaticfinalfloatSTART_NUM=Float.MIN_VALUE;
publicstaticvoidmain(String[]args){
inttimes=50;
ExecuteTryCatchexecuteTryCatch=newExecuteTryCatch();
//每個方法執(zhí)行50次
while(--times>=0){
System.out.println("times=".concat(String.valueOf(times)));
executeTryCatch.executeMillionsEveryTryWithFinally();
executeTryCatch.executeMillionsEveryTry();
executeTryCatch.executeMillionsOneTry();
executeTryCatch.executeMillionsNoneTry();
executeTryCatch.executeMillionsTestReOrder();
}
}
/**
*千萬次浮點運算不使用trycatch
**/
publicvoidexecuteMillionsNoneTry(){
floatnum=START_NUM;
longstart=System.nanoTime();
for(inti=0;i1f;
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;
}
longnao=System.nanoTime()-start;
longmillion=nao/1000000;
System.out.println("noneTrysum:"+num+"million:"+million+"nao:"+nao);
}
/**
*千萬次浮點運算最外層使用trycatch
**/
publicvoidexecuteMillionsOneTry(){
floatnum=START_NUM;
longstart=System.nanoTime();
try{
for(inti=0;i1f;
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(Exceptione){
}
longnao=System.nanoTime()-start;
longmillion=nao/1000000;
System.out.println("oneTrysum:"+num+"million:"+million+"nao:"+nao);
}
/**
*千萬次浮點運算循環(huán)內(nèi)使用trycatch
**/
publicvoidexecuteMillionsEveryTry(){
floatnum=START_NUM;
longstart=System.nanoTime();
for(inti=0;itry{
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(Exceptione){
}
}
longnao=System.nanoTime()-start;
longmillion=nao/1000000;
System.out.println("evertTrysum:"+num+"million:"+million+"nao:"+nao);
}
/**
*千萬次浮點運算循環(huán)內(nèi)使用trycatch,并使用finally
**/
publicvoidexecuteMillionsEveryTryWithFinally(){
floatnum=START_NUM;
longstart=System.nanoTime();
for(inti=0;itry{
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(Exceptione){
}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;
}
}
longnao=System.nanoTime()-start;
longmillion=nao/1000000;
System.out.println("finalTrysum:"+num+"million:"+million+"nao:"+nao);
}
/**
*千萬次浮點運算,循環(huán)內(nèi)使用多個trycatch
**/
publicvoidexecuteMillionsTestReOrder(){
floatnum=START_NUM;
longstart=System.nanoTime();
for(inti=0;itry{
num=num+STEP_NUM+1f;
num=num+STEP_NUM+2f;
}catch(Exceptione){}
try{
num=num+STEP_NUM+3f;
num=num+STEP_NUM+4f;
num=num+STEP_NUM+5f;
}catch(Exceptione){}
try{
num=num+STEP_NUM+1f;
num=num+STEP_NUM+2f;
}catch(Exceptione){}
try{
num=num+STEP_NUM+3f;
num=num+STEP_NUM+4f;
num=num+STEP_NUM+5f;
}catch(Exceptione){}
}
longnao=System.nanoTime()-start;
longmillion=nao/1000000;
System.out.println("orderTrysum:"+num+"million:"+million+"nao:"+nao);
}
}
五、解釋模式下執(zhí)行測試
設(shè)置如下JVM參數(shù),禁用編譯優(yōu)化
-Xint
-XX:-BackgroundCompilation
結(jié)合測試代碼發(fā)現(xiàn),即使百萬次循環(huán)計算,每個循環(huán)內(nèi)都使用了 try catch
也并沒用對造成很大的影響。
唯一發(fā)現(xiàn)了一個問題,每個循環(huán)內(nèi)都是使用 try catch
且使用多次。發(fā)現(xiàn)性能下降,千萬次計算差值為:5~7 毫秒;4個 try 那么執(zhí)行的指令最少4條goto ,前邊闡述過,這里造成這個差異的主要原因是 goto 指令占比過大,放大了問題;當我們在幾百行代碼里使用少量try catch
時,goto所占比重就會很低,測試結(jié)果會更趨于合理。
六、編譯模式測試
設(shè)置如下測試參數(shù),執(zhí)行10 次即為熱點代碼
-Xcomp
-XX:CompileThreshold=10
-XX:-UseCounterDecay
-XX:OnStackReplacePercentage=100
-XX:InterpreterProfilePercentage=33
執(zhí)行結(jié)果如下圖,難分勝負,波動只在微秒級別,執(zhí)行速度也快了很多,編譯效果拔群啊,甚至連 “解釋模式” 運行時多個try catch
導(dǎo)致的,多個goto跳轉(zhuǎn)帶來的問題都給順帶優(yōu)化了;由此也可以得到 try catch
并不會影響即時編譯的結(jié)論。
我們可以再上升到億級計算,依舊難分勝負,波動在毫秒級。
七、結(jié)論
try catch
不會造成巨大的性能影響,換句話說,我們平時寫代碼最優(yōu)先考慮的是程序的健壯性,當然大佬們肯定都知道了怎么合理使用try catch
了,但是對萌新來說,你如果不確定,那么你可以使用 try catch;
在未發(fā)生異常時,給代碼外部包上 try catch
,并不會造成影響。
舉個栗子吧,我的代碼中使用了:URLDecoder.decode
,所以必須得捕獲異常。
privateintgetThenAddNoJudge(JSONObjectjson,Stringkey){
if(Objects.isNull(json))
thrownewIllegalArgumentException("參數(shù)異常");
intnum;
try{
//不校驗key是否未空值,直接調(diào)用toString每次觸發(fā)空指針異常并被捕獲
num=100+Integer.parseInt(URLDecoder.decode(json.get(key).toString(),"UTF-8"));
}catch(Exceptione){
num=100;
}
returnnum;
}
privateintgetThenAddWithJudge(JSONObjectjson,Stringkey){
if(Objects.isNull(json))
thrownewIllegalArgumentException("參數(shù)異常");
intnum;
try{
//校驗key是否未空值
num=100+Integer.parseInt(URLDecoder.decode(Objects.toString(json.get(key),"0"),"UTF-8"));
}catch(Exceptione){
num=100;
}
returnnum;
}
publicstaticvoidmain(String[]args){
inttimes=1000000;//百萬次
longnao1=System.nanoTime();
ExecuteTryCatchexecuteTryCatch=newExecuteTryCatch();
for(inti=0;inewJSONObject(),"anyKey");
}
longend1=System.nanoTime();
System.out.println("未拋出異常耗時:millions="+(end1-nao1)/1000000+"毫秒nao="+(end1-nao1)+"微秒");
longnao2=System.nanoTime();
for(inti=0;inewJSONObject(),"anyKey");
}
longend2=System.nanoTime();
System.out.println("每次必拋出異常:millions="+(end2-nao2)/1000000+"毫秒nao="+(end2-nao2)+"微秒");
}
調(diào)用方法百萬次,執(zhí)行結(jié)果如下:
經(jīng)過這個例子,我想你知道你該如何 編寫你的代碼了吧?可怕的不是 try catch
而是 搬磚業(yè)務(wù)不熟練啊。
審核編輯 :李倩
-
JAVA
+關(guān)注
關(guān)注
19文章
2959瀏覽量
104553 -
編譯器
+關(guān)注
關(guān)注
1文章
1618瀏覽量
49052 -
JVM
+關(guān)注
關(guān)注
0文章
157瀏覽量
12209
原文標題:別被騙了,try-catch語句真的會影響性能嗎?
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論