使用 try-catch 捕獲異常會影響性能嗎?
閱讀本文大概需要 12 分鐘。
來自:blog.csdn.net/bokerr/article/details/122655795
前言
一、JVM 異常處理邏輯
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;
}
}
# 編譯
javac TestClass.java
# 使用javap 查看 add 方法被編譯后的機器指令
javap -verbose TestClass.class
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 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 */

個人理解,from 和 to 相當于劃分區(qū)間,只要在這個區(qū)間內拋出了type 所對應的,“java/lang/Exception” 異常(主動athrow 或者 由jvm運行時檢測到異常自動拋出),那么就跳轉到target 所代表的第八行。
如果硬是要說的話,用了try catch 編譯后指令篇幅變長了;goto 語句跳轉會耗費性能,當你寫個數(shù)百行代碼的方法的時候,編譯出來成百上千條指令,這時候這句goto的帶來的影響顯得微乎其微。

二、關于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. 分層編譯
[客戶端模式-Client、服務端模式-Server],它們代表的是兩個不同的即時編譯器,C1(Client Compiler) 和 C2 (Server Compiler)。解釋模式下運行時,編譯器不介入工作; 編譯模式模式下運行,會使用即時編譯器優(yōu)化熱點代碼,有可選的即時編譯器[C1 或 C2]; 混合模式為:解釋模式和編譯模式搭配使用。

2. 即時編譯器
1. 解釋模式
強制虛擬機運行于 “解釋模式” -Xint 禁用后臺編譯 -XX:-BackgroundCompilation
2. 編譯模式
# 強制虛擬機運行于 "編譯模式"
-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
3. 提前編譯器:jaotc
Graal [新時代的主角] 編譯器開發(fā),因為本文用的是 C2 編譯器,所以只對它做一個了解;[-XX:PrintAOT]。三、關于測試的約束
執(zhí)行用時統(tǒng)計
System.naoTime() 輸出的是過了多少時間[微秒:10的負9次方秒],并不是完全精確的方法執(zhí)行用時的合計,為了保證結果準確性,測試的運算次數(shù)將拉長到百萬甚至千萬次。編譯器優(yōu)化的因素
通過指令禁用 JVM 的編譯優(yōu)化,讓它以最原始的狀態(tài)運行,然后看有無 try catch 的影響。 通過指令使用即時編譯,盡量做到把后端優(yōu)化拉滿,看看 try catch 十有會影響到 jvm的編譯優(yōu)化。
關于指令重排序
指令重排序發(fā)生在多線程并發(fā)場景,這么做是為了更好的利用CPU資源,在單線程測試時不需要考慮。不論如何指令重排序,都會保證最終執(zhí)行結果,與單線程下的執(zhí)行結果相同; 雖然我們不去測試它,但是也可以進行一些推斷,參考 volatile 關鍵字禁止指令重排序的做法:插入內存屏障; 假定 try catch 存在屏障,導致前后的代碼分割;那么最少的try catch代表最少的分割。 所以,是不是會有這樣的結論呢:我們把方法體內的 多個 try catch 合并為一個 try catch 是不是反而能減少屏障呢?這么做勢必造成 try catch 的范圍變大。
四、測試代碼
[給編譯器優(yōu)化預留優(yōu)化的可能,這些指令可能被合并];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í)行測試
-Xint
-XX:-BackgroundCompilation

六、編譯模式測試
-Xcomp
-XX:CompileThreshold=10
-XX:-UseCounterDecay
-XX:OnStackReplacePercentage=100
-XX:InterpreterProfilePercentage=33


七、結論
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) + "微秒");
}

推薦閱讀:
互聯(lián)網初中高級大廠面試題(9個G) 內容包含Java基礎、JavaWeb、MySQL性能優(yōu)化、JVM、鎖、百萬并發(fā)、消息隊列、高性能緩存、反射、Spring全家桶原理、微服務、Zookeeper......等技術棧!
?戳閱讀原文領取! 朕已閱


