在Java中,在没有实际错误的情况下使用throw/catch作为逻辑的一部分通常是一个坏主意(在某种程度上),因为抛出和捕获异常的代价很高,并且在循环中多次执行这个操作通常比其他不涉及抛出异常的控制结构慢得多。
我的问题是,成本是在throw/catch本身产生的,还是在创建Exception对象时产生的(因为它获得了很多运行时信息,包括执行堆栈)?
换句话说,如果我
Exception e = new Exception();
但不要扔,这是扔的大部分成本,还是扔+接的处理成本高?
我不是在问把代码放在try/catch块中是否会增加执行该代码的成本,我是在问捕获异常是否是昂贵的部分,或者创建(调用的构造函数)异常是否是昂贵的部分。
另一种问法是,如果我创建一个异常实例,并反复抛出并捕获它,这是否比每次抛出都创建一个新异常要快得多?
创建异常对象并不一定比创建其他常规对象更昂贵。主要成本隐藏在本地fillInStackTrace方法中,该方法遍历调用堆栈并收集构建堆栈跟踪所需的所有信息:类、方法名、行号等。
大多数Throwable构造函数隐式调用fillInStackTrace。这就是创建异常很慢的想法的来源。但是,有一个构造函数可以创建一个没有堆栈跟踪的Throwable。它允许你制作可以快速实例化的可抛出对象。创建轻量级异常的另一种方法是重写fillInStackTrace。
现在抛出一个异常呢?
事实上,这取决于在哪里捕获抛出的异常。
如果在相同的方法中捕获(或者更准确地说,在相同的上下文中捕获,因为由于内联,上下文中可以包含多个方法),那么throw与goto一样快速和简单(当然,在JIT编译之后)。
但是,如果catch块位于堆栈的较深位置,则JVM需要展开堆栈帧,这可能会花费更长的时间。如果涉及到同步块或方法,则需要更长的时间,因为展开意味着释放被删除的堆栈框架所拥有的监视器。
我可以通过适当的基准测试来证实上述陈述,但幸运的是,我不需要这样做,因为HotSpot的性能工程师Alexey Shipilev的帖子已经完美地涵盖了所有方面:Lil' Exception的出色性能。
创建异常对象并不一定比创建其他常规对象更昂贵。主要成本隐藏在本地fillInStackTrace方法中,该方法遍历调用堆栈并收集构建堆栈跟踪所需的所有信息:类、方法名、行号等。
大多数Throwable构造函数隐式调用fillInStackTrace。这就是创建异常很慢的想法的来源。但是,有一个构造函数可以创建一个没有堆栈跟踪的Throwable。它允许你制作可以快速实例化的可抛出对象。创建轻量级异常的另一种方法是重写fillInStackTrace。
现在抛出一个异常呢?
事实上,这取决于在哪里捕获抛出的异常。
如果在相同的方法中捕获(或者更准确地说,在相同的上下文中捕获,因为由于内联,上下文中可以包含多个方法),那么throw与goto一样快速和简单(当然,在JIT编译之后)。
但是,如果catch块位于堆栈的较深位置,则JVM需要展开堆栈帧,这可能会花费更长的时间。如果涉及到同步块或方法,则需要更长的时间,因为展开意味着释放被删除的堆栈框架所拥有的监视器。
我可以通过适当的基准测试来证实上述陈述,但幸运的是,我不需要这样做,因为HotSpot的性能工程师Alexey Shipilev的帖子已经完美地涵盖了所有方面:Lil' Exception的出色性能。
问题的这一部分…
另一种问法是,如果我创建了一个Exception实例
一遍又一遍地抛接,这样会快很多吗
比每次抛出时创建一个新的异常好吗?
似乎是在问是否创建一个异常并将其缓存到某个地方可以提高性能。是的。这与在对象创建时关闭正在写入的堆栈是一样的,因为它已经完成了。
这些是我得到的时间,请在这之后阅读警告…
|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)|
| 16| 193| 251| 77 (%)|
| 15| 390| 406| 96 (%)|
| 14| 394| 401| 98 (%)|
| 13| 381| 385| 99 (%)|
| 12| 387| 370| 105 (%)|
| 11| 368| 376| 98 (%)|
| 10| 188| 192| 98 (%)|
| 9| 193| 195| 99 (%)|
| 8| 200| 188| 106 (%)|
| 7| 187| 184| 102 (%)|
| 6| 196| 200| 98 (%)|
| 5| 197| 193| 102 (%)|
| 4| 198| 190| 104 (%)|
| 3| 193| 183| 105 (%)|
当然,这样做的问题是你的堆栈跟踪现在指向你实例化对象的地方,而不是它被抛出的地方。
大多数Throwable构造函数中的第一个操作是填充堆栈跟踪,这是花费最多的地方。
但是,有一个受保护的构造函数,它带有一个禁用堆栈跟踪的标志。扩展Exception时也可以访问这个构造函数。如果创建自定义异常类型,则可以避免创建堆栈跟踪,以减少信息为代价获得更好的性能。
如果您通过正常方式创建任何类型的单个异常,则可以多次重新抛出它,而无需填充堆栈跟踪的开销。但是,它的堆栈跟踪将反映它的构造位置,而不是在特定实例中抛出的位置。
当前版本的Java尝试优化堆栈跟踪的创建。调用本机代码来填充堆栈跟踪,堆栈跟踪以较轻的本机结构记录跟踪。只有在调用getStackTrace()、printStackTrace()或其他需要跟踪的方法时,才会从该记录惰性地创建相应的Java StackTraceElement对象。
如果消除堆栈跟踪生成,另一个主要成本是在throw和catch之间展开堆栈。在异常被捕获之前遇到的插入帧越少,这个过程就越快。
在设计程序时,要保证只有在真正异常的情况下才会抛出异常,这样的优化很难证明是正确的。
以@AustinD的回答为出发点,我做了一些调整。代码在底部。
除了添加重复抛出一个Exception实例的情况外,我还关闭了编译器优化,以便获得准确的性能结果。我在VM参数中添加了-Djava.compiler=NONE,就像这个答案一样。(在eclipse中,编辑Run Configuration→Arguments来设置VM参数)
结果:
new Exception + throw/catch = 643.5
new Exception only = 510.7
throw/catch only = 115.2
new String (benchmark) = 669.8
因此,创建异常的成本大约是抛出+捕获异常的5倍。假设编译器没有优化掉很多成本。
为了比较,这里是没有禁用优化的相同测试运行:
new Exception + throw/catch = 382.6
new Exception only = 379.5
throw/catch only = 0.3
new String (benchmark) = 15.6
代码:
public class ExceptionPerformanceTest {
private static final int NUM_TRIES = 1000000;
public static void main(String[] args) {
double numIterations = 10;
long exceptionPlusCatchTime = 0, excepTime = 0, strTime = 0, throwTime = 0;
for (int i = 0; i < numIterations; i++) {
exceptionPlusCatchTime += exceptionPlusCatchBlock();
excepTime += createException();
throwTime += catchBlock();
strTime += createString();
}
System.out.println("new Exception + throw/catch = " + exceptionPlusCatchTime / numIterations);
System.out.println("new Exception only = " + excepTime / numIterations);
System.out.println("throw/catch only = " + throwTime / numIterations);
System.out.println("new String (benchmark) = " + strTime / numIterations);
}
private static long exceptionPlusCatchBlock() {
long start = System.currentTimeMillis();
for (int i = 0; i < NUM_TRIES; i++) {
try {
throw new Exception();
} catch (Exception e) {
// do nothing
}
}
long stop = System.currentTimeMillis();
return stop - start;
}
private static long createException() {
long start = System.currentTimeMillis();
for (int i = 0; i < NUM_TRIES; i++) {
Exception e = new Exception();
}
long stop = System.currentTimeMillis();
return stop - start;
}
private static long createString() {
long start = System.currentTimeMillis();
for (int i = 0; i < NUM_TRIES; i++) {
Object o = new String("" + i);
}
long stop = System.currentTimeMillis();
return stop - start;
}
private static long catchBlock() {
Exception ex = new Exception(); //Instantiated here
long start = System.currentTimeMillis();
for (int i = 0; i < NUM_TRIES; i++) {
try {
throw ex; //repeatedly thrown
} catch (Exception e) {
// do nothing
}
}
long stop = System.currentTimeMillis();
return stop - start;
}
}
创建带有空堆栈跟踪的Exception所需的时间与throw和try-catch块一起花费的时间相同。然而,填充堆栈跟踪平均需要5倍的时间。
我创建了以下基准测试来演示对性能的影响。我在运行配置中添加了-Djava.compiler=NONE来禁用编译器优化。为了衡量构建堆栈跟踪的影响,我扩展了Exception类以利用无堆栈构造函数:
class NoStackException extends Exception{
public NoStackException() {
super("",null,false,false);
}
}
基准代码如下:
public class ExceptionBenchmark {
private static final int NUM_TRIES = 100000;
public static void main(String[] args) {
long throwCatchTime = 0, newExceptionTime = 0, newObjectTime = 0, noStackExceptionTime = 0;
for (int i = 0; i < 30; i++) {
throwCatchTime += throwCatchLoop();
newExceptionTime += newExceptionLoop();
newObjectTime += newObjectLoop();
noStackExceptionTime += newNoStackExceptionLoop();
}
System.out.println("throwCatchTime = " + throwCatchTime / 30);
System.out.println("newExceptionTime = " + newExceptionTime / 30);
System.out.println("newStringTime = " + newObjectTime / 30);
System.out.println("noStackExceptionTime = " + noStackExceptionTime / 30);
}
private static long throwCatchLoop() {
Exception ex = new Exception(); //Instantiated here
long start = System.currentTimeMillis();
for (int i = 0; i < NUM_TRIES; i++) {
try {
throw ex; //repeatedly thrown
} catch (Exception e) {
// do nothing
}
}
long stop = System.currentTimeMillis();
return stop - start;
}
private static long newExceptionLoop() {
long start = System.currentTimeMillis();
for (int i = 0; i < NUM_TRIES; i++) {
Exception e = new Exception();
}
long stop = System.currentTimeMillis();
return stop - start;
}
private static long newObjectLoop() {
long start = System.currentTimeMillis();
for (int i = 0; i < NUM_TRIES; i++) {
Object o = new Object();
}
long stop = System.currentTimeMillis();
return stop - start;
}
private static long newNoStackExceptionLoop() {
long start = System.currentTimeMillis();
for (int i = 0; i < NUM_TRIES; i++) {
NoStackException e = new NoStackException();
}
long stop = System.currentTimeMillis();
return stop - start;
}
}
输出:
throwCatchTime = 19
newExceptionTime = 77
newObjectTime = 3
noStackExceptionTime = 15
这意味着创建一个noackexception的开销与重复抛出相同的异常差不多。它还显示,创建一个Exception并填充其堆栈跟踪所需的时间大约长4倍。