这是我的第 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 框架,配置如下:
-
<!-- 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;
// 循环次数
-
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;
-
}
-
}
以上代码的测试结果为:

从以上结果可以看出,程序在循环 1000 次的情况下,单次平均执行时间为:
循环内包含 try-catch 的平均执行时间是 635 纳秒 ±75 纳秒,也就是 635 纳秒上下误差是 75 纳秒;
循环外包含 try-catch 的平均执行时间是 630 纳秒,上下误差 38 纳秒。
也就是说,在没有发生异常的情况下,除去误差值,我们得到的结论是:try-catch 无论是在 for
循环内还是 for
循环外,它们的性能相同,几乎没有任何差别。
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:表示异常类名称。
从字节码指令可以看出,当代码运行时出错时,会先判断出错数据是否在 from
到 to
的范围内,如果是则从 target
标志位往下执行,如果没有出错,直接 goto
到 return
。也就是说,如果代码不出错的话,性能几乎是不受影响的,和正常的代码的执行逻辑是一样的。
业务情况分析
虽然 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 放置在循环体外来执行。
总结
本文我们测试了 try-catch 放在循环体内和循环体外的性能,发现二者在循环很多次的情况下性能几乎是一致的。然后我们通过字节码分析,发现只有当发生异常时,才会对比异常表进行异常处理,而正常情况下则可以忽略 try-catch 的执行。但在循环体内还是循环体外使用 try-catch,对于程序的执行结果来说是完全不同的,因此我们应该从实际的业务出发,来决定到 try-catch 应该存放的位置,而非性能考虑。
-
-
-
往期推荐
关注公众号发送”进群“,磊哥拉你进读者群。
转载:https://blog.csdn.net/sufu1065/article/details/106485480