今天給大家?guī)淼氖顷P于try-catch 應該放在循環(huán)體外柠傍,還是放在循環(huán)體內(nèi)的文章,我們將從性能和業(yè)務場景分析這兩個方面來回答此問題息尺。
很多人對 try-catch 有一定的誤解携兵,比如我們經(jīng)常會把它(try-catch)和“低性能”直接畫上等號,但對 try-catch 的本質(zhì)(是什么)卻缺少著最基礎的了解搂誉,因此我們也會在本篇中對 try-catch 的本質(zhì)進行相關的探索徐紧。
性能評測
話不多說,我們直接來開始今天的測試,本文我們依舊使用 Oracle 官方提供的 JMH(Java Microbenchmark Harness并级,JAVA 微基準測試套件)來進行測試拂檩。
首先在 pom.xml 文件中添加 JMH 框架,配置如下:
?<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
? <groupId>org.openjdk.jmh</groupId>
? <artifactId>jmh-core</artifactId>
? <version>{version}</version>
</dependency>
完整測試代碼如下:
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
/**
* try - catch 性能測試
*/
@BenchmarkMode(Mode.AverageTime) // 測試完成時間
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 1 輪嘲碧,每次 1s
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 測試 5 輪稻励,每次 3s
@Fork(1) // fork 1 個線程
@State(Scope.Benchmark)
@Threads(100)
public class TryCatchPerformanceTest {
? ? private static final int forSize = 1000; // 循環(huán)次數(shù)
? ? public static void main(String[] args) throws RunnerException {
? ? ? ? // 啟動基準測試
? ? ? ? Options opt = new OptionsBuilder()
? ? ? ? ? ? ? ? .include(TryCatchPerformanceTest.class.getSimpleName()) // 要導入的測試類
? ? ? ? ? ? ? ? .build();
? ? ? ? new Runner(opt).run(); // 執(zhí)行測試
? ? }
? ? @Benchmark
? ? public int innerForeach() {
? ? ? ? int count = 0;
? ? ? ? for (int i = 0; i < forSize; i++) {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? if (i == forSize) {
? ? ? ? ? ? ? ? ? ? throw new Exception("new Exception");
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? count++;
? ? ? ? ? ? } catch (Exception e) {
? ? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return count;
? ? }
? ? @Benchmark
? ? public int outerForeach() {
? ? ? ? int count = 0;
? ? ? ? try {
? ? ? ? ? ? for (int i = 0; i < forSize; i++) {
? ? ? ? ? ? ? ? if (i == forSize) {
? ? ? ? ? ? ? ? ? ? throw new Exception("new Exception");
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? count++;
? ? ? ? ? ? }
? ? ? ? } catch (Exception e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? ? ? return count;
? ? }
}
以上代碼的測試結(jié)果為:
從以上結(jié)果可以看出,程序在循環(huán) 1000 次的情況下愈涩,單次平均執(zhí)行時間為:循環(huán)內(nèi)包含 try-catch 的平均執(zhí)行時間是 635 納秒 ±75 納秒望抽,也就是 635 納秒上下誤差是 75 納秒;
循環(huán)外包含 try-catch 的平均執(zhí)行時間是 630 納秒履婉,上下誤差 38 納秒煤篙。
也就是說,在沒有發(fā)生異常的情況下毁腿,除去誤差值辑奈,我們得到的結(jié)論是:try-catch 無論是在for循環(huán)內(nèi)還是for循環(huán)外,它們的性能相同已烤,幾乎沒有任何差別鸠窗。
try-catch的本質(zhì)
要理解 try-catch 的性能問題,必須從它的字節(jié)碼開始分析胯究,只有這樣我能才能知道 try-catch 的本質(zhì)到底是什么稍计,以及它是如何執(zhí)行的。
此時我們寫一個最簡單的 try-catch 代碼:
public class AppTest {
? ? public static void main(String[] args) {
? ? ? ? try {
? ? ? ? ? ? int count = 0;
? ? ? ? ? ? throw new Exception("new Exception");
? ? ? ? } catch (Exception e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? }
}
然后使用javac生成字節(jié)碼之后唐片,再使用javap -c AppTest的命令來查看字節(jié)碼文件:
? javap -c AppTest
警告: 二進制文件AppTest包含com.example.AppTest
Compiled from "AppTest.java"
public class com.example.AppTest {
? public com.example.AppTest();
? ? Code:
? ? ? 0: aload_0
? ? ? 1: invokespecial #1? ? ? ? ? ? ? ? ? // Method java/lang/Object."<init>":()V
? ? ? 4: return
? public static void main(java.lang.String[]);
? ? Code:
? ? ? 0: iconst_0
? ? ? 1: istore_1
? ? ? 2: new? ? ? ? ? #2? ? ? ? ? ? ? ? ? // class java/lang/Exception
? ? ? 5: dup
? ? ? 6: ldc? ? ? ? ? #3? ? ? ? ? ? ? ? ? // String new Exception
? ? ? 8: invokespecial #4? ? ? ? ? ? ? ? ? // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
? ? ? 11: athrow
? ? ? 12: astore_1
? ? ? 13: aload_1
? ? ? 14: invokevirtual #5? ? ? ? ? ? ? ? ? // Method java/lang/Exception.printStackTrace:()V
? ? ? 17: return
? ? Exception table:
? ? ? from? ? to? target type
? ? ? ? ? 0? ? 12? ? 12? Class java/lang/Exception
}
從以上字節(jié)碼中可以看到有一個異常表:
Exception table:
? ? ? from? ? to? target type
? ? ? ? ? 0? ? 12? ? 12? Class java/lang/Exception
參數(shù)說明:
from:表示 try-catch 的開始地址丙猬;
to:表示 try-catch 的結(jié)束地址;
target:表示異常的處理起始位费韭;
type:表示異常類名稱。
從字節(jié)碼指令可以看出庭瑰,當代碼運行時出錯時星持,會先判斷出錯數(shù)據(jù)是否在from到to的范圍內(nèi),如果是則從target標志位往下執(zhí)行弹灭,如果沒有出錯督暂,直接goto到return。也就是說穷吮,如果代碼不出錯的話逻翁,性能幾乎是不受影響的,和正常的代碼的執(zhí)行邏輯是一樣的捡鱼。
業(yè)務情況分析
雖然 try-catch 在循環(huán)體內(nèi)還是循環(huán)體外的性能是類似的八回,但是它們所代碼的業(yè)務含義卻完全不同,例如以下代碼:
public class AppTest {
? ? public static void main(String[] args) {
? ? ? ? System.out.println("循環(huán)內(nèi)的執(zhí)行結(jié)果:" + innerForeach());
? ? ? ? System.out.println("循環(huán)外的執(zhí)行結(jié)果:" + outerForeach());
? ? }
? ? // 方法一
? ? public static int innerForeach() {
? ? ? ? int count = 0;
? ? ? ? for (int i = 0; i < 6; i++) {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? if (i == 3) {
? ? ? ? ? ? ? ? ? ? throw new Exception("new Exception");
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? count++;
? ? ? ? ? ? } catch (Exception e) {
? ? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return count;
? ? }
? ? // 方法二
? ? public static int outerForeach() {
? ? ? ? int count = 0;
? ? ? ? try {
? ? ? ? ? ? for (int i = 0; i < 6; i++) {
? ? ? ? ? ? ? ? if (i == 3) {
? ? ? ? ? ? ? ? ? ? throw new Exception("new Exception");
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? count++;
? ? ? ? ? ? }
? ? ? ? } catch (Exception e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? ? ? return count;
? ? }
}
以上程序的執(zhí)行結(jié)果為:
java.lang.Exception: new Exception
at com.example.AppTest.innerForeach(AppTest.java:15)
at com.example.AppTest.main(AppTest.java:5)
java.lang.Exception: new Exception
at com.example.AppTest.outerForeach(AppTest.java:31)
at com.example.AppTest.main(AppTest.java:6)
循環(huán)內(nèi)的執(zhí)行結(jié)果:5
循環(huán)外的執(zhí)行結(jié)果:3
可以看出在循環(huán)體內(nèi)的 try-catch 在發(fā)生異常之后,可以繼續(xù)執(zhí)行循環(huán)缠诅;而循環(huán)外的 try-catch 在發(fā)生異常之后會終止循環(huán)溶浴。
因此我們在決定 try-catch 究竟是應該放在循環(huán)內(nèi)還是循環(huán)外,不取決于性能(因為性能幾乎相同)管引,而是應該取決于具體的業(yè)務場景士败。
例如我們需要處理一批數(shù)據(jù),而無論這組數(shù)據(jù)中有哪一個數(shù)據(jù)有問題褥伴,都不能影響其他組的正常執(zhí)行谅将,此時我們可以把 try-catch 放置在循環(huán)體內(nèi);而當我們需要計算一組數(shù)據(jù)的合計值時重慢,只要有一組數(shù)據(jù)有誤饥臂,我們就需要終止執(zhí)行,并拋出異常伤锚,此時我們需要將 try-catch 放置在循環(huán)體外來執(zhí)行擅笔。
總結(jié)
本文我們測試了 try-catch 放在循環(huán)體內(nèi)和循環(huán)體外的性能,發(fā)現(xiàn)二者在循環(huán)很多次的情況下性能幾乎是一致的屯援。然后我們通過字節(jié)碼分析猛们,發(fā)現(xiàn)只有當發(fā)生異常時,才會對比異常表進行異常處理狞洋,而正常情況下則可以忽略 try-catch 的執(zhí)行弯淘。但在循環(huán)體內(nèi)還是循環(huán)體外使用 try-catch,對于程序的執(zhí)行結(jié)果來說是完全不同的吉懊,因此我們應該從實際的業(yè)務出發(fā)庐橙,來決定到 try-catch 應該存放的位置,而非性能考慮借嗽。
作者:Java中文社群
鏈接:https://juejin.im/post/5ed5b998f265da76bd1ad012
來源:掘金
著作權(quán)歸作者所有态鳖。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處恶导。