被騙了,try-catch語句真的會(huì)影響性能嗎?
推薦閱讀
1、【地址已更新】8月26日最新免費(fèi)ChatGPT,附視頻號(hào)下載方法 2、【私活必備】35款 SpringBoot/SpringCloud 開源項(xiàng)目,用來接私活掙錢真爽! 3、程序員段子:有哪些話一聽就知道對(duì)方很水!
不知道從何時(shí)起,傳出了這么一句話:Java中使用try catch 會(huì)嚴(yán)重影響性能。然而,事實(shí)真的如此么?我們對(duì)try catch 應(yīng)該畏之如猛虎么?
一、JVM 異常處理邏輯
Java 程序中顯式拋出異常由athrow指令支持,除了通過 throw 主動(dòng)拋出異常外,JVM規(guī)范中還規(guī)定了許多運(yùn)行時(shí)異常會(huì)在檢測到異常狀況時(shí)自動(dòng)拋出(效果等同athrow), 例如除數(shù)為0時(shí)就會(huì)自動(dòng)拋出異常,以及大名鼎鼎的 NullPointerException 。
還需要注意的是,JVM 中 異常處理的catch語句不再由字節(jié)碼指令來實(shí)現(xiàn)(很早之前通過 jsr和 ret指令來完成,它們在很早之前的版本里就被舍棄了),現(xiàn)在的JVM通過異常表(Exception table 方法體中能找到其內(nèi)容)來完成 catch 語句;很多人說try catch 影響性能可能就是因?yàn)檎J(rèn)識(shí)還停留于上古時(shí)代。
1.我們編寫如下的類,add 方法中計(jì)算 ++x; 并捕獲異常。
public class TestClass {
private static int len = 779;
public int add(int x){
try {
// 若運(yùn)行時(shí)檢測到 x = 0,那么 jvm會(huì)自動(dòng)拋出異常,(可以理解成由jvm自己負(fù)責(zé) athrow 指令調(diào)用)
x = 100/x;
} catch (Exception e) {
x = 100;
}
return x;
}
}
2.使用javap 工具查看上述類的編譯后的class文件
# 編譯
javac TestClass.java
# 使用javap 查看 add 方法被編譯后的機(jī)器指令
javap -verbose TestClass.class
忽略常量池等其他信息,下邊貼出add 方法編譯后的 機(jī)器指令集:
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 // 將一個(gè)int型變量推至棧頂
3: idiv // 相除
4: istore_1 // 除的結(jié)果值壓入本地變量
5: goto 11 // 跳轉(zhuǎn)到指令:11
8: astore_2 // 將引用類型值壓入本地變量
9: bipush 100 // 將單字節(jié)常量推送棧頂<這里與數(shù)值100有關(guān),可以嘗試修改100后的編譯結(jié)果:iconst、bipush、ldc>
10: istore_1 // 將int類型值壓入本地變量
11: iload_1 // int 型變量推棧頂
12: ireturn // 返回
// 注意看 from 和 to 以及 targer,然后對(duì)照著去看上述指令
Exception table:
from to target type
0 5 8 Class java/lang/Exception
LineNumberTable:
line 6: 0
line 9: 5
line 7: 8
line 8: 9
line 10: 11
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 對(duì)應(yīng)的就是 try 語句包含的內(nèi)容,而targer = 8 正好對(duì)應(yīng) catch 語句塊內(nèi)部操作。
個(gè)人理解,from 和 to 相當(dāng)于劃分區(qū)間,只要在這個(gè)區(qū)間內(nèi)拋出了type 所對(duì)應(yīng)的,“
java/lang/Exception” 異常(主動(dòng)athrow 或者 由jvm運(yùn)行時(shí)檢測到異常自動(dòng)拋出),那么就跳轉(zhuǎn)到target 所代表的第八行。
若執(zhí)行過程中,沒有異常,直接從第5條指令跳轉(zhuǎn)到第11條指令后返回,由此可見未發(fā)生異常時(shí),所謂的性能損耗幾乎不存在;
如果硬是要說的話,用了
try catch編譯后指令篇幅變長了;goto 語句跳轉(zhuǎn)會(huì)耗費(fèi)性能,當(dāng)你寫個(gè)數(shù)百行代碼的方法的時(shí)候,編譯出來成百上千條指令,這時(shí)候這句goto的帶來的影響顯得微乎其微。
如圖所示為去掉try catch 后的指令篇幅,幾乎等同上述指令的前五條。
綜上所述:“Java中使用try catch 會(huì)嚴(yán)重影響性能” 是民間說法,它并不成立。 如果不信,接著看下面的測試吧。
二、關(guān)于JVM的編譯優(yōu)化
其實(shí)寫出測試用例并不是很難,這里我們需要重點(diǎn)考慮的是編譯器的自動(dòng)優(yōu)化,是否會(huì)因此得到不同的測試結(jié)果?
本節(jié)會(huì)粗略的介紹一些jvm編譯器相關(guān)的概念,講它只為更精確的測試結(jié)果,通過它我們可以窺探 try catch 是否會(huì)影響JVM的編譯優(yōu)化。
前端編譯與優(yōu)化:我們最常見的前端編譯器是 javac,它的優(yōu)化更偏向于代碼結(jié)構(gòu)上的優(yōu)化,它主要是為了提高程序員的編碼效率,不怎么關(guān)注執(zhí)行效率優(yōu)化;例如,數(shù)據(jù)流和控制流分析、解語法糖等等。
后端編譯與優(yōu)化:后端編譯包括 “即時(shí)編譯[JIT]” 和 “提前編譯[AOT]”,區(qū)別于前端編譯器,它們最終作用體現(xiàn)于運(yùn)行期,致力于優(yōu)化從字節(jié)碼生成本地機(jī)器碼的過程(它們優(yōu)化的是代碼的執(zhí)行效率)。
1. 分層編譯
PS * JVM 自己根據(jù)宿主機(jī)決定自己的運(yùn)行模式, “JVM 運(yùn)行模式”;[客戶端模式-Client、服務(wù)端模式-Server],它們代表的是兩個(gè)不同的即時(shí)編譯器,C1(Client Compiler) 和 C2 (Server Compiler)。
PS:分層編譯分為:“解釋模式”、“編譯模式”、“混合模式”;
解釋模式下運(yùn)行時(shí),編譯器不介入工作;
編譯模式模式下運(yùn)行,會(huì)使用即時(shí)編譯器優(yōu)化熱點(diǎn)代碼,有可選的即時(shí)編譯器[C1 或 C2];
混合模式為:解釋模式和編譯模式搭配使用。
如圖,我的環(huán)境里JVM 運(yùn)行于 Server 模式,如果使用即時(shí)編譯,那么就是使用的:C2 即時(shí)編譯器。
2. 即時(shí)編譯器
了解如下的幾個(gè) 概念:
1. 解釋模式
它不使用即時(shí)編譯器進(jìn)行后端優(yōu)化
強(qiáng)制虛擬機(jī)運(yùn)行于 “解釋模式” -Xint
禁用后臺(tái)編譯 -XX:-BackgroundCompilation
2. 編譯模式
即時(shí)編譯器會(huì)在運(yùn)行時(shí),對(duì)生成的本地機(jī)器碼進(jìn)行優(yōu)化,其中重點(diǎn)關(guān)照熱點(diǎn)代碼。
# 強(qiáng)制虛擬機(jī)運(yùn)行于 "編譯模式"
-Xcomp
# 方法調(diào)用次數(shù)計(jì)數(shù)器閾值,它是基于計(jì)數(shù)器熱點(diǎn)代碼探測依據(jù)[Client模式=1500,Server模式=10000]
-XX:CompileThreshold=10
# 關(guān)閉方法調(diào)用次數(shù)熱度衰減,使用方法調(diào)用計(jì)數(shù)的絕對(duì)值,它搭配上一配置項(xiàng)使用
-XX:-UseCounterDecay
# 除了熱點(diǎn)方法,還有熱點(diǎn)回邊代碼[循環(huán)],熱點(diǎn)回邊代碼的閾值計(jì)算參考如下:
-XX:BackEdgeThreshold = 方法計(jì)數(shù)器閾值[-XX:CompileThreshold] * OSR比率[-XX:OnStackReplacePercentage]
# OSR比率默認(rèn)值:Client模式=933,Server模式=140
-XX:OnStackReplacePercentag=100
所謂 “即時(shí)”,它是在運(yùn)行過程中發(fā)生的,所以它的缺點(diǎn)也也明顯:在運(yùn)行期間需要耗費(fèi)資源去做性能分析,也不太適合在運(yùn)行期間去大刀闊斧的去做一些耗費(fèi)資源的重負(fù)載優(yōu)化操作。
3. 提前編譯器:jaotc
它是后端編譯的另一個(gè)主角,它有兩個(gè)發(fā)展路線,基于Graal [新時(shí)代的主角] 編譯器開發(fā),因?yàn)楸疚挠玫氖?C2 編譯器,所以只對(duì)它做一個(gè)了解;
第一條路線:與傳統(tǒng)的C、C++編譯做的事情類似,在程序運(yùn)行之前就把程序代碼編譯成機(jī)器碼;好處是夠快,不占用運(yùn)行時(shí)系統(tǒng)資源,缺點(diǎn)是"啟動(dòng)過程" 會(huì)很緩慢;
第二條路線:已知即時(shí)編譯運(yùn)行時(shí)做性能統(tǒng)計(jì)分析占用資源,那么,我們可以把其中一些耗費(fèi)資源的編譯工作,放到提前編譯階段來完成啊,最后在運(yùn)行時(shí)即時(shí)編譯器再去使用,那么可以大大節(jié)省即時(shí)編譯的開銷;這個(gè)分支可以把它看作是即時(shí)編譯緩存;
遺憾的是它只支持 G1 或者 Parallel 垃圾收集器,且只存在JDK 9 以后的版本,暫不需要去關(guān)注它;JDK 9 以后的版本可以使用這個(gè)參數(shù)打印相關(guān)信息:[-XX:PrintAOT]。
三、關(guān)于測試的約束
執(zhí)行用時(shí)統(tǒng)計(jì)
System.naoTime() 輸出的是過了多少時(shí)間[微秒:10的負(fù)9次方秒],并不是完全精確的方法執(zhí)行用時(shí)的合計(jì),為了保證結(jié)果準(zhǔn)確性,測試的運(yùn)算次數(shù)將拉長到百萬甚至千萬次。
編譯器優(yōu)化的因素
上一節(jié)花了一定的篇幅介紹編譯器優(yōu)化,這里我要做的是:對(duì)比完全不使用任何編譯優(yōu)化,與使用即時(shí)編譯時(shí),try catch 對(duì)的性能影響。
-
通過指令禁用 JVM 的編譯優(yōu)化,讓它以最原始的狀態(tài)運(yùn)行,然后看有無 try catch的影響。 -
通過指令使用即時(shí)編譯,盡量做到把后端優(yōu)化拉滿,看看 try catch十有會(huì)影響到 jvm的編譯優(yōu)化。
關(guān)于指令重排序
目前尚未可知 try catch 的使用影響指令重排序;
我們這里的討論有一個(gè)前提,當(dāng) try catch 的使用無法避免時(shí),我們應(yīng)該如何使用 try catch 以應(yīng)對(duì)它可能存在的對(duì)指令重排序的影響。
-
指令重排序發(fā)生在多線程并發(fā)場景,這么做是為了更好的利用CPU資源,在單線程測試時(shí)不需要考慮。不論如何指令重排序,都會(huì)保證最終執(zhí)行結(jié)果,與單線程下的執(zhí)行結(jié)果相同; -
雖然我們不去測試它,但是也可以進(jìn)行一些推斷,參考 volatile關(guān)鍵字禁止指令重排序的做法:插入內(nèi)存屏障; -
假定 try catch存在屏障,導(dǎo)致前后的代碼分割;那么最少的try catch代表最少的分割。 -
所以,是不是會(huì)有這樣的結(jié)論呢:我們把方法體內(nèi)的 多個(gè) try catch合并為一個(gè)try catch是不是反而能減少屏障呢?這么做勢必造成try catch的范圍變大。
當(dāng)然,上述關(guān)于指令重排序討論內(nèi)容都是基于個(gè)人的猜想,猶未可知 try catch 是否影響指令重排序;本文重點(diǎn)討論的也只是單線程環(huán)境下的 try catch 使用影響性能。
四、測試代碼
循環(huán)次數(shù)為100W ,循環(huán)內(nèi)10次預(yù)算[給編譯器優(yōu)化預(yù)留優(yōu)化的可能,這些指令可能被合并];
每個(gè)方法都會(huì)到達(dá)千萬次浮點(diǎn)計(jì)算。
同樣每個(gè)方法外層再循環(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();
// 每個(gè)方法執(zhí)行 50 次
while (--times >= 0){
System.out.println("times=".concat(String.valueOf(times)));
executeTryCatch.executeMillionsEveryTryWithFinally();
executeTryCatch.executeMillionsEveryTry();
executeTryCatch.executeMillionsOneTry();
executeTryCatch.executeMillionsNoneTry();
executeTryCatch.executeMillionsTestReOrder();
}
}
/**
* 千萬次浮點(diǎn)運(yùn)算不使用 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);
}
/**
* 千萬次浮點(diǎn)運(yùn)算最外層使用 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);
}
/**
* 千萬次浮點(diǎn)運(yùn)算循環(huán)內(nèi)使用 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);
}
/**
* 千萬次浮點(diǎn)運(yùn)算循環(huán)內(nèi)使用 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);
}
/**
* 千萬次浮點(diǎn)運(yùn)算,循環(huán)內(nèi)使用多個(gè) 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í)行測試
設(shè)置如下JVM參數(shù),禁用編譯優(yōu)化
-Xint
-XX:-BackgroundCompilation
結(jié)合測試代碼發(fā)現(xiàn),即使百萬次循環(huán)計(jì)算,每個(gè)循環(huán)內(nèi)都使用了
try catch 也并沒用對(duì)造成很大的影響。
唯一發(fā)現(xiàn)了一個(gè)問題,每個(gè)循環(huán)內(nèi)都是使用 try catch 且使用多次。發(fā)現(xiàn)性能下降,千萬次計(jì)算差值為:5~7 毫秒;4個(gè) try 那么執(zhí)行的指令最少4條goto ,前邊闡述過,這里造成這個(gè)差異的主要原因是 goto 指令占比過大,放大了問題;當(dāng)我們在幾百行代碼里使用少量try catch 時(shí),goto所占比重就會(huì)很低,測試結(jié)果會(huì)更趨于合理。
六、編譯模式測試
設(shè)置如下測試參數(shù),執(zhí)行10 次即為熱點(diǎn)代碼
-Xcomp
-XX:CompileThreshold=10
-XX:-UseCounterDecay
-XX:OnStackReplacePercentage=100
-XX:InterpreterProfilePercentage=33
執(zhí)行結(jié)果如下圖,難分勝負(fù),波動(dòng)只在微秒級(jí)別,執(zhí)行速度也快了很多,編譯效果拔群啊,甚至連 “解釋模式” 運(yùn)行時(shí)多個(gè)try catch 導(dǎo)致的,多個(gè)goto跳轉(zhuǎn)帶來的問題都給順帶優(yōu)化了;由此也可以得到 try catch 并不會(huì)影響即時(shí)編譯的結(jié)論。
我們可以再上升到億級(jí)計(jì)算,依舊難分勝負(fù),波動(dòng)在毫秒級(jí)。
七、結(jié)論
try catch 不會(huì)造成巨大的性能影響,換句話說,我們平時(shí)寫代碼最優(yōu)先考慮的是程序的健壯性,當(dāng)然大佬們肯定都知道了怎么合理使用try catch了,但是對(duì)萌新來說,你如果不確定,那么你可以使用 try catch;
在未發(fā)生異常時(shí),給代碼外部包上 try catch,并不會(huì)造成影響。
舉個(gè)栗子吧,我的代碼中使用了:URLDecoder.decode,所以必須得捕獲異常。
private int getThenAddNoJudge(JSONObject json, String key){
if (Objects.isNull(json))
throw new IllegalArgumentException("參數(shù)異常");
int num;
try {
// 不校驗(yàn) key 是否未空值,直接調(diào)用 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 {
// 校驗(yàn) 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("未拋出異常耗時(shí):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) + "微秒");
}
調(diào)用方法百萬次,執(zhí)行結(jié)果如下:
經(jīng)過這個(gè)例子,我想你知道你該如何 編寫你的代碼了吧?可怕的不是 try catch 而是 搬磚業(yè)務(wù)不熟練啊。
作者:bokerr
來源:blog.csdn.net/bokerr/article/details/122655795

資源,怎么領(lǐng)取?
掃二維碼,加我微信,備注:編程合集
一定要備注:編程合集,不要急哦,工作忙完后就會(huì)通過!
!鏈接發(fā)夸克網(wǎng)盤!
