啪啪,打脸了!领导说:try-catch必须放在循环体外!

哈喽,亲爱的小伙伴们,技术学磊哥,进步没得说!欢迎来到新一期的性能解读系列,我是磊哥。java

今天给你们带来的是关于 try-catch 应该放在循环体外,仍是放在循环体内的文章,咱们将从性能业务场景分析这两个方面来回答此问题。bash

不少人对 try-catch 有必定的误解,好比咱们常常会把它(try-catch)和“低性能”直接画上等号,但对 try-catch 的本质(是什么)却缺乏着最基础的了解,所以咱们也会在本篇中对 try-catch 的本质进行相关的探索框架

img

小贴士:我会尽可能用代码和评测结果来证实问题,但因为自己认知的局限,若有不当之处,请读者朋友们在评论区指出。性能

性能评测

话很少说,咱们直接来开始今天的测试,本文咱们依旧使用 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)来进行测试。测试

首先在 pom.xml 文件中添加 JMH 框架,配置以下:ui

<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
   <groupId>org.openjdk.jmh</groupId>
   <artifactId>jmh-core</artifactId>
   <version>{version}</version>
</dependency>
复制代码

完整测试代码以下:spa

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; // 循环次数
    public static void main(String[] args) throws RunnerException {
        // 启动基准测试
        Options opt = new OptionsBuilder()
                .include(TryCatchPerformanceTest.class.getSimpleName()) // 要导入的测试类
                .build();
        new Runner(opt).run(); // 执行测试
    }

    @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;
    }
}
复制代码

以上代码的测试结果为:线程

img

从以上结果能够看出,程序在循环 1000 次的状况下,单次平均执行时间为:code

  • 循环内包含 try-catch 的平均执行时间是 635 纳秒 ±75 纳秒,也就是 635 纳秒上下偏差是 75 纳秒;
  • 循环外包含 try-catch 的平均执行时间是 630 纳秒,上下偏差 38 纳秒。

也就是说,在没有发生异常的状况下,除去偏差值,咱们获得的结论是:try-catch 不管是在 for 循环内仍是 for 循环外,它们的性能相同,几乎没有任何差异orm

img

try-catch的本质

要理解 try-catch 的性能问题,必须从它的字节码开始分析,只有这样我能才能知道 try-catch 的本质究竟是什么,以及它是如何执行的。

此时咱们写一个最简单的 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 生成字节码以后,再使用 javap -c AppTest 的命令来查看字节码文件:

➜ 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
}
复制代码

从以上字节码中能够看到有一个异常表:

Exception table:
       from    to  target type
          0    12    12   Class java/lang/Exception
复制代码

参数说明:

  • from:表示 try-catch 的开始地址;
  • to:表示 try-catch 的结束地址;
  • target:表示异常的处理起始位;
  • type:表示异常类名称。

从字节码指令能够看出,当代码运行时出错时,会先判断出错数据是否在 fromto 的范围内,若是是则从 target 标志位往下执行,若是没有出错,直接 gotoreturn。也就是说,若是代码不出错的话,性能几乎是不受影响的,和正常的代码的执行逻辑是同样的。

img

业务状况分析

虽然 try-catch 在循环体内仍是循环体外的性能是相似的,可是它们所代码的业务含义却彻底不一样,例如如下代码:

public class AppTest {
    public static void main(String[] args) {
        System.out.println("循环内的执行结果:" + innerForeach());
        System.out.println("循环外的执行结果:" + 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;
    }
}
复制代码

以上程序的执行结果为:

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)

循环内的执行结果:5

循环外的执行结果:3

能够看出在循环体内的 try-catch 在发生异常以后,能够继续执行循环;而循环外的 try-catch 在发生异常以后会终止循环。

所以咱们在决定 try-catch 到底是应该放在循环内仍是循环外,不取决于性能(由于性能几乎相同),而是应该取决于具体的业务场景

例如咱们须要处理一批数据,而不管这组数据中有哪个数据有问题,都不能影响其余组的正常执行,此时咱们能够把 try-catch 放置在循环体内;而当咱们须要计算一组数据的合计值时,只要有一组数据有误,咱们就须要终止执行,并抛出异常,此时咱们须要将 try-catch 放置在循环体外来执行。

img

总结

本文咱们测试了 try-catch 放在循环体内和循环体外的性能,发现两者在循环不少次的状况下性能几乎是一致的。而后咱们经过字节码分析,发现只有当发生异常时,才会对比异常表进行异常处理,而正常状况下则能够忽略 try-catch 的执行。但在循环体内仍是循环体外使用 try-catch,对于程序的执行结果来讲是彻底不一样的,所以咱们应该从实际的业务出发,来决定到 try-catch 应该存放的位置,而非性能考虑

关注公众号「Java中文社群」回复“干货”,获取 50 篇原创干货 Top 榜

相关文章
相关标签/搜索