1. Overview
1.概述
In Java, exceptions are generally considered expensive and shouldn’t be used for flow control. This tutorial will prove that this perception is correct and pinpoint what causes the performance issue.
在Java中,异常通常被认为是昂贵的,不应该被用于流控制。本教程将证明这种看法是正确的,并准确指出导致性能问题的原因。
2. Setting Up Environment
2.设置环境
Before writing code to evaluate the performance cost, we need to set up a benchmarking environment.
在编写评估性能成本的代码之前,我们需要建立一个基准测试环境。
2.1. Java Microbenchmark Harness
2.1 Java微基准测试工具
Measuring exception overhead isn’t as easy as executing a method in a simple loop and taking note of the total time.
衡量异常开销并不像在一个简单的循环中执行一个方法并记下总时间那么简单。
The reason is that a just-in-time compiler can get in the way and optimize the code. Such optimization may make the code perform better than it would actually do in a production environment. In other words, it might yield falsely positive results.
原因是及时编译器会碍于面子而优化代码。这种优化可能会使代码的性能比它在生产环境中的实际表现更好。换句话说,它可能产生错误的积极结果。
To create a controlled environment that can mitigate JVM optimization, we’ll use Java Microbenchmark Harness, or JMH for short.
为了创建一个可以缓解JVM优化的受控环境,我们将使用Java Microbenchmark Harness,或简称为JMH。
The following subsections will walk through setting up a benchmarking environment without going into the details of JMH. For more information about this tool, please check out our Microbenchmarking with Java tutorial.
下面的小节将介绍如何设置基准测试环境,而不涉及JMH的细节。有关该工具的更多信息,请查看我们的用Java进行微基准测试的教程。
2.2. Obtaining JMH Artifacts
2.2.获得JMH文物
To get JMH artifacts, add these two dependencies to the POM:
为了获得JMH工件,请在POM中添加这两个依赖项。
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>
Please refer to Maven Central for the latest versions of JMH Core and JMH Annotation Processor.
请参考Maven Central,了解JMH Core和JMH注释处理器的最新版本。
2.3. Benchmark Class
2.3.基准类
We’ll need a class to hold benchmarks:
我们需要一个班级来保持基准。
@Fork(1)
@Warmup(iterations = 2)
@Measurement(iterations = 10)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class ExceptionBenchmark {
private static final int LIMIT = 10_000;
// benchmarks go here
}
Let’s go through the JMH annotations shown above:
让我们来看看上面显示的JMH注释。
- @Fork: Specifying the number of times JMH must spawn a new process to run benchmarks. We set its value to 1 to generate only one process, avoiding waiting for too long to see the result
- @Warmup: Carrying warm-up parameters. The iterations element being 2 means the first two runs are ignored when calculating the result
- @Measurement: Carrying measurement parameters. An iterations value of 10 indicates JMH will execute each method 10 times
- @BenchmarkMode: This is how JHM should collect execution results. The value AverageTime requires JMH to count the average time a method needs to complete its operations
- @OutputTimeUnit: Indicating the output time unit, which is the millisecond in this case
Additionally, there’s a static field inside the class body, namely LIMIT. This is the number of iterations in each method body.
此外,在类的主体中还有一个静态字段,即LIMIT。这是每个方法体中的迭代次数。
2.4. Executing Benchmarks
2.4.执行基准
To execute benchmarks, we need a main method:
为了执行基准,我们需要一个main方法。
public class MappingFrameworksPerformance {
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
We can package the project into a JAR file and run it at the command line. Doing so now will, of course, produce an empty output as we haven’t added any benchmarking method.
我们可以把项目打包成一个JAR文件,然后在命令行上运行它。当然,现在这样做会产生一个空的输出,因为我们还没有添加任何基准测试方法。
For convenience, we can add the maven-jar-plugin to the POM. This plugin allows us the execute the main method inside an IDE:
为了方便,我们可以在POM中添加maven-jar-plugin。这个插件允许我们在IDE中执行main方法。
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.baeldung.performancetests.MappingFrameworksPerformance</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
The latest version of maven-jar-plugin can be found here.
最新版本的maven-jar-plugin可以在这里找到。
3. Performance Measurement
3.绩效测量
It’s time to have some benchmarking methods to measure performance. Each of these methods must carry the @Benchmark annotation.
是时候有一些衡量性能的基准测试方法了。这些方法中的每一个都必须带有@Benchmark注解。
3.1. Method Returning Normally
3.1.正常返回的方法
Let’s start with a method returning normally; that is, a method that doesn’t throw an exception:
让我们从一个正常返回的方法开始;也就是说,一个不抛出异常的方法。
@Benchmark
public void doNotThrowException(Blackhole blackhole) {
for (int i = 0; i < LIMIT; i++) {
blackhole.consume(new Object());
}
}
The blackhole parameter references an instance of Blackhole. This is a JMH class that helps prevent dead code elimination, an optimization a just-in-time compiler may perform.
blackhole参数引用了Blackhole的一个实例。这是一个JMH类,有助于防止死代码的消除,这是一个及时编译器可能进行的优化。
The benchmark, in this case, doesn’t throw any exception. In fact, we’ll use it as a reference to evaluate the performance of those that do throw exceptions.
在这种情况下,该基准并没有抛出任何异常。事实上,我们将把它作为一个参考,以评估那些抛出异常的性能。
Executing the main method will give us a report:
执行main方法会给我们一个报告。
Benchmark Mode Cnt Score Error Units
ExceptionBenchmark.doNotThrowException avgt 10 0.049 ± 0.006 ms/op
There’s nothing special in this result. The average execution time of the benchmark is 0.049 milliseconds, which is per se pretty meaningless.
这个结果没有什么特别之处。该基准的平均执行时间是0.049毫秒,这本身就很没有意义。
3.2. Creating and Throwing an Exception
3.2.创建和抛出一个异常
Here’s another benchmark that throws and catches exceptions:
下面是另一个抛出和捕获异常的基准。
@Benchmark
public void throwAndCatchException(Blackhole blackhole) {
for (int i = 0; i < LIMIT; i++) {
try {
throw new Exception();
} catch (Exception e) {
blackhole.consume(e);
}
}
}
Let’s have a look at the output:
让我们看一下输出结果。
Benchmark Mode Cnt Score Error Units
ExceptionBenchmark.doNotThrowException avgt 10 0.048 ± 0.003 ms/op
ExceptionBenchmark.throwAndCatchException avgt 10 17.942 ± 0.846 ms/op
The small change in the execution time of method doNotThrowException isn’t important. It’s just the fluctuation in the state of the underlying OS and the JVM. The key takeaway is that throwing an exception makes a method run hundreds of times slower.
方法doNotThrowException的执行时间的微小变化并不重要。这只是底层操作系统和JVM的状态的波动。关键的启示是,抛出异常使方法的运行速度慢了数百倍。
The next few subsections will find out what exactly leads to such a dramatic difference.
接下来的几个小节将找出究竟是什么导致了如此巨大的差异。
3.3. Creating an Exception Without Throwing It
3.3.创建一个异常而不抛出它
Instead of creating, throwing, and catching an exception, we’ll just create it:
我们不需要创建、抛出和捕捉一个异常,而是直接创建它。
@Benchmark
public void createExceptionWithoutThrowingIt(Blackhole blackhole) {
for (int i = 0; i < LIMIT; i++) {
blackhole.consume(new Exception());
}
}
Now, let’s execute the three benchmarks we’ve declared:
现在,让我们来执行我们所声明的三个基准。
Benchmark Mode Cnt Score Error Units
ExceptionBenchmark.createExceptionWithoutThrowingIt avgt 10 17.601 ± 3.152 ms/op
ExceptionBenchmark.doNotThrowException avgt 10 0.054 ± 0.014 ms/op
ExceptionBenchmark.throwAndCatchException avgt 10 17.174 ± 0.474 ms/op
The result may come as a surprise: the execution time of the first and the third methods are nearly the same, while that of the second is substantially smaller.
结果可能会让人吃惊:第一种和第三种方法的执行时间几乎相同,而第二种方法的执行时间则大大减少。
At this point, it’s clear that the throw and catch statements themselves are fairly cheap. The creation of exceptions, on the other hand, produces high overheads.
在这一点上,很明显,throw和catch语句本身是相当便宜的。另一方面,创建异常会产生很高的开销。。
3.4. Throwing an Exception Without Adding the Stack Trace
3.4.抛出一个异常而不添加堆栈跟踪
Let’s figure out why constructing an exception is much more expensive than doing an ordinary object:
让我们来弄清楚为什么构造一个异常比做一个普通对象要昂贵得多。
@Benchmark
@Fork(value = 1, jvmArgs = "-XX:-StackTraceInThrowable")
public void throwExceptionWithoutAddingStackTrace(Blackhole blackhole) {
for (int i = 0; i < LIMIT; i++) {
try {
throw new Exception();
} catch (Exception e) {
blackhole.consume(e);
}
}
}
The only difference between this method and the one in subsection 3.2 is the jvmArgs element. Its value -XX:-StackTraceInThrowable is a JVM option, keeping the stack trace from being added to the exception.
这个方法和第3.2小节中的方法唯一的区别是jvmArgs元素。它的值-XX:-StackTraceInThrowable是一个JVM选项,保持堆栈跟踪不被添加到异常中。
Let’s run the benchmarks again:
让我们再次运行基准测试。
Benchmark Mode Cnt Score Error Units
ExceptionBenchmark.createExceptionWithoutThrowingIt avgt 10 17.874 ± 3.199 ms/op
ExceptionBenchmark.doNotThrowException avgt 10 0.046 ± 0.003 ms/op
ExceptionBenchmark.throwAndCatchException avgt 10 16.268 ± 0.239 ms/op
ExceptionBenchmark.throwExceptionWithoutAddingStackTrace avgt 10 1.174 ± 0.014 ms/op
By not populating the exception with the stack trace, we reduced execution duration by more than 100 times. Apparently, walking through the stack and adding its frames to the exception bring about the sluggishness we’ve seen.
通过不在异常中加入堆栈跟踪,我们将执行时间缩短了100倍以上。显然,走过堆栈并将其框架添加到异常中,带来了我们所看到的迟缓现象。
3.5. Throwing an Exception and Unwinding Its Stack Trace
3.5.抛出异常并解开其堆栈跟踪
Finally, let’s see what happens if we throw an exception and unwind the stack trace when catching it:
最后,让我们看看如果我们抛出一个异常并在捕捉它的时候解除堆栈跟踪会发生什么。
@Benchmark
public void throwExceptionAndUnwindStackTrace(Blackhole blackhole) {
for (int i = 0; i < LIMIT; i++) {
try {
throw new Exception();
} catch (Exception e) {
blackhole.consume(e.getStackTrace());
}
}
}
Here’s the outcome:
结果是这样的。
Benchmark Mode Cnt Score Error Units
ExceptionBenchmark.createExceptionWithoutThrowingIt avgt 10 16.605 ± 0.988 ms/op
ExceptionBenchmark.doNotThrowException avgt 10 0.047 ± 0.006 ms/op
ExceptionBenchmark.throwAndCatchException avgt 10 16.449 ± 0.304 ms/op
ExceptionBenchmark.throwExceptionAndUnwindStackTrace avgt 10 326.560 ± 4.991 ms/op
ExceptionBenchmark.throwExceptionWithoutAddingStackTrace avgt 10 1.185 ± 0.015 ms/op
Just by unwinding the stack trace, we see a whopping increase of some 20 times in the execution duration. Put another way, the performance is much worse if we extract the stack trace from an exception in addition to throwing it.
仅仅通过解开堆栈跟踪,我们就可以看到执行时间惊人地增加了20倍左右。换句话说,如果我们在抛出异常的同时提取堆栈跟踪,性能会差很多。
4. Conclusion
4.总结
In this tutorial, we analyzed the performance effects of exceptions. Specifically, it found out the performance cost is mostly in the addition of the stack trace to the exception. If this stack trace is unwound afterward, the overhead becomes much larger.
在本教程中,我们分析了异常的性能影响。具体来说,它发现性能成本主要体现在对异常的堆栈跟踪的增加。如果这个堆栈跟踪在之后被解开,那么开销就会变大很多。
Since throwing and handling exceptions is expensive, we shouldn’t use it for normal program flows. Instead, as its name implies, exceptions should only be used for exceptional cases.
由于抛出和处理异常的成本很高,我们不应该在正常的程序流程中使用它。相反,正如其名称所暗示的,异常应该只用于特殊情况。
The complete source code can be found over on GitHub.
完整的源代码可以在GitHub>上找到。