小言_互联网的博客

啪啪打脸!领导说:try-catch要放在循环体外!

423人阅读  评论(0)

这是我的第 206 期分享

作者 | 王磊

来源 | Java中文社群(ID:javacn666)

转载请联系授权(微信ID:GG_Stone)

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

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

很多人对 try-catch 有一定的误解,比如我们经常会把它(try-catch)和“低性能”直接画上等号,但对 try-catch 的本质(是什么)却缺少着最基础的了解,因此我们也会在本篇中对 try-catch 的本质进行相关的探索

小贴士:我会尽量用代码和评测结果来证明问题,但由于本身认知的局限,如有不当之处,请读者朋友们在评论区指出。

性能评测

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

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


   
  1. <!-- https: //mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
  2. <dependency>
  3.    <groupId>org.openjdk.jmh</groupId>
  4.    <artifactId>jmh-core</artifactId>
  5.    <version>{version}</version>
  6. </dependency>

完整测试代码如下:


   
  1. import org.openjdk.jmh.annotations.*;
  2. import org.openjdk.jmh.runner.Runner;
  3. import org.openjdk.jmh.runner.RunnerException;
  4. import org.openjdk.jmh.runner.options.Options;
  5. import org.openjdk.jmh.runner.options.OptionsBuilder;
  6. import java.util.concurrent.TimeUnit;
  7. /**
  8.  * try - catch 性能测试
  9.  */
  10. @BenchmarkMode(Mode.AverageTime)  // 测试完成时间
  11. @OutputTimeUnit(TimeUnit.NANOSECONDS)
  12. @Warmup(iterations =  1, time =  1, timeUnit = TimeUnit.SECONDS)  // 预热 1 轮,每次 1s
  13. @Measurement(iterations =  5, time =  5, timeUnit = TimeUnit.SECONDS)  // 测试 5 轮,每次 3s
  14. @Fork( 1// fork 1 个线程
  15. @State(Scope.Benchmark)
  16. @Threads( 100)
  17. public class TryCatchPerformanceTest {
  18.     private static final  int forSize =  1000// 循环次数
  19.     public static void main(String[] args) throws RunnerException {
  20.          // 启动基准测试
  21.         Options opt =  new OptionsBuilder()
  22.                 .include(TryCatchPerformanceTest.class.getSimpleName())  // 要导入的测试类
  23.                 .build();
  24.          new Runner(opt).run();  // 执行测试
  25.     }
  26.     @Benchmark
  27.     public  int innerForeach() {
  28.          int count =  0;
  29.          for ( int i =  0; i < forSize; i++) {
  30.             try {
  31.                  if (i == forSize) {
  32.                     throw  new Exception( "new Exception");
  33.                 }
  34.                 count++;
  35.             } catch (Exception e) {
  36.                 e.printStackTrace();
  37.             }
  38.         }
  39.          return count;
  40.     }
  41.     @Benchmark
  42.     public  int outerForeach() {
  43.          int count =  0;
  44.         try {
  45.              for ( int i =  0; i < forSize; i++) {
  46.                  if (i == forSize) {
  47.                     throw  new Exception( "new Exception");
  48.                 }
  49.                 count++;
  50.             }
  51.         } catch (Exception e) {
  52.             e.printStackTrace();
  53.         }
  54.          return count;
  55.     }
  56. }

以上代码的测试结果为:


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

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

  • 循环外包含 try-catch 的平均执行时间是 630 纳秒,上下误差 38 纳秒。

也就是说,在没有发生异常的情况下,除去误差值,我们得到的结论是:try-catch 无论是在 for 循环内还是  for 循环外,它们的性能相同,几乎没有任何差别

try-catch的本质

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

此时我们写一个最简单的 try-catch 代码:


   
  1. public class AppTest {
  2.     public static void main(String[] args) {
  3.         try {
  4.              int count =  0;
  5.             throw  new Exception( "new Exception");
  6.         } catch (Exception e) {
  7.             e.printStackTrace();
  8.         }
  9.     }
  10. }

然后使用 javac 生成字节码之后,再使用 javap -c AppTest 的命令来查看字节码文件:


   
  1. ➜ javap -c AppTest 
  2. 警告: 二进制文件AppTest包含com.example.AppTest
  3. Compiled from  "AppTest.java"
  4. public class com.example.AppTest {
  5.   public com.example.AppTest();
  6.     Code:
  7.         0: aload_0
  8.         1: invokespecial # 1                   // Method java/lang/Object."<init>":()V
  9.         4return
  10.   public static void main(java.lang.String[]);
  11.     Code:
  12.         0: iconst_0
  13.         1: istore_1
  14.         2new           # 2                   // class java/lang/Exception
  15.         5: dup
  16.         6: ldc           # 3                   // String new Exception
  17.         8: invokespecial # 4                   // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
  18.        11: athrow
  19.        12: astore_1
  20.        13: aload_1
  21.        14: invokevirtual # 5                   // Method java/lang/Exception.printStackTrace:()V
  22.        17return
  23.     Exception table:
  24.        from    to  target  type
  25.             0     12     12   Class java/lang/Exception
  26. }

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


   
  1. Exception table:
  2.        from    to  target  type
  3.            0     12     12   Class java/lang/Exception

参数说明:

  • from:表示 try-catch 的开始地址;

  • to:表示 try-catch 的结束地址;

  • target:表示异常的处理起始位;

  • type:表示异常类名称。

从字节码指令可以看出,当代码运行时出错时,会先判断出错数据是否在 from 到 to 的范围内,如果是则从 target 标志位往下执行,如果没有出错,直接 goto 到 return。也就是说,如果代码不出错的话,性能几乎是不受影响的,和正常的代码的执行逻辑是一样的。

业务情况分析

虽然 try-catch 在循环体内还是循环体外的性能是类似的,但是它们所代码的业务含义却完全不同,例如以下代码:


   
  1. public class AppTest {
  2.     public static void main(String[] args) {
  3.         System.out. println( "循环内的执行结果:" + innerForeach());
  4.         System.out. println( "循环外的执行结果:" + outerForeach());
  5.     }
  6.     
  7.      // 方法一
  8.     public static  int innerForeach() {
  9.          int count =  0;
  10.          for ( int i =  0; i <  6; i++) {
  11.             try {
  12.                  if (i ==  3) {
  13.                     throw  new Exception( "new Exception");
  14.                 }
  15.                 count++;
  16.             } catch (Exception e) {
  17.                 e.printStackTrace();
  18.             }
  19.         }
  20.          return count;
  21.     }
  22.      // 方法二
  23.     public static  int outerForeach() {
  24.          int count =  0;
  25.         try {
  26.              for ( int i =  0; i <  6; i++) {
  27.                  if (i ==  3) {
  28.                     throw  new Exception( "new Exception");
  29.                 }
  30.                 count++;
  31.             }
  32.         } catch (Exception e) {
  33.             e.printStackTrace();
  34.         }
  35.          return count;
  36.     }
  37. }

以上程序的执行结果为:

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 放置在循环体外来执行。

总结

本文我们测试了 try-catch 放在循环体内和循环体外的性能,发现二者在循环很多次的情况下性能几乎是一致的。然后我们通过字节码分析,发现只有当发生异常时,才会对比异常表进行异常处理,而正常情况下则可以忽略 try-catch 的执行。但在循环体内还是循环体外使用 try-catch,对于程序的执行结果来说是完全不同的,因此我们应该从实际的业务出发,来决定到 try-catch 应该存放的位置,而非性能考虑


   
  1. 往期推荐

阿里巴巴为什么让初始化集合时必须指定大小?

局部变量竟然比全局变量快 5 倍?

关注公众号发送”进群“,磊哥拉你进读者群。


转载:https://blog.csdn.net/sufu1065/article/details/106485480
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场