抛出异常的哪一部分是昂贵的?

在Java中,在没有实际错误的情况下使用throw/catch作为逻辑的一部分通常是一个坏主意(在某种程度上),因为抛出和捕获异常的代价很高,并且在循环中多次执行这个操作通常比其他不涉及抛出异常的控制结构慢得多。

我的问题是,成本是在throw/catch本身产生的,还是在创建Exception对象时产生的(因为它获得了很多运行时信息,包括执行堆栈)?

换句话说,如果我

Exception e = new Exception();

但不要扔,这是扔的大部分成本,还是扔+接的处理成本高?

我不是在问把代码放在try/catch块中是否会增加执行该代码的成本,我是在问捕获异常是否是昂贵的部分,或者创建(调用的构造函数)异常是否是昂贵的部分。

另一种问法是,如果我创建一个异常实例,并反复抛出并捕获它,这是否比每次抛出都创建一个新异常要快得多?

22745 次浏览

创建带有null堆栈跟踪的Exception所需的时间与创建throwtry-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

这意味着创建NoStackException的代价与重复抛出相同的Exception差不多。它还表明,创建Exception并填充其堆栈跟踪所需的时间大约要长4 x

大多数Throwable构造函数的第一个操作是填写堆栈跟踪,,这是大部分开销所在。

但是,有一个受保护的构造函数,它带有一个禁用堆栈跟踪的标志。扩展Exception时也可以访问这个构造函数。如果创建自定义异常类型,则可以避免创建堆栈跟踪,以减少信息为代价获得更好的性能。

如果您通过正常方式创建任何类型的单个异常,则可以多次重新抛出它,而无需填充堆栈跟踪的开销。但是,它的堆栈跟踪将反映它的构造位置,而不是在特定实例中抛出的位置。

当前版本的Java尝试优化堆栈跟踪的创建。调用本机代码来填充堆栈跟踪,堆栈跟踪以较轻的本机结构记录跟踪。只有在调用getStackTrace()printStackTrace()或其他需要跟踪的方法时,才会从此记录惰性地创建相应的Java StackTraceElement对象。

如果消除堆栈跟踪生成,另一个主要成本是在throw和catch之间展开堆栈。在异常被捕获之前遇到的插入帧越少,这个过程就越快。

在设计程序时,要保证只有在真正异常的情况下才会抛出异常,这样的优化很难证明是正确的。

创建异常对象并不比创建其他常规对象更昂贵。主要开销隐藏在原生fillInStackTrace方法中,该方法遍历调用堆栈并收集所有必要的信息来构建堆栈跟踪:类、方法名、行号等。

大多数Throwable构造函数隐式调用fillInStackTrace。这就是创建异常很慢的想法的来源。然而,有一个构造函数可以创建一个不带堆栈跟踪的Throwable。它允许你制作可以快速实例化的可抛出对象。创建轻量级异常的另一种方法是覆盖fillInStackTrace


现在异常怎么办?
事实上,它取决于抛出的异常位于抓住了的位置

如果在相同的方法中捕获它(或者更准确地说,在相同的上下文中,因为由于内联,上下文可以包括多个方法),那么throw就像goto一样快速和简单(当然,在JIT编译之后)。

然而,如果catch块在堆栈中较深的地方,那么JVM需要展开堆栈帧,这可能会花费更长的时间。如果涉及到synchronized块或方法,则需要更长的时间,因为展开意味着释放被删除的堆栈帧所拥有的监视器。


我可以通过适当的基准测试来确认上面的陈述,但幸运的是,我不需要这样做,因为HotSpot的性能工程师Alexey Shipilev的帖子已经完美地涵盖了所有方面:Lil' Exception的出色表现

以@AustinD的回答为出发点,我做了一些调整。代码在底部。

除了添加重复抛出一个Exception实例的情况外,我还关闭了编译器优化,以便获得准确的性能结果。我将-Djava.compiler=NONE添加到VM参数中,就像这个答案一样。(在eclipse中,编辑Run Configuration →参数来设置这个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;
}
}

这里有一篇关于异常的好文章。

http://shipilev.net/blog/2014/exceptional-performance/

结论是堆栈跟踪构造和堆栈展开是昂贵的部分。下面的代码利用了1.7中的一个特性,在这个特性中我们可以打开和关闭堆栈跟踪。然后我们可以用它来看看不同的场景有什么样的成本

下面是单独创建对象的计时。我在这里添加了String,所以你可以看到,在没有写入堆栈的情况下,创建JavaException对象和String几乎没有区别。随着堆栈写入打开,差异是戏剧性的,即至少一个数量级变慢。

Time to create million String objects: 41.41 (ms)
Time to create million JavaException objects with    stack: 608.89 (ms)
Time to create million JavaException objects without stack: 43.50 (ms)

下面显示了它从特定深度投掷一百万次所花费的时间。

|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)|
|   16|           1428|             243| 588 (%)|
|   15|           1763|             393| 449 (%)|
|   14|           1746|             390| 448 (%)|
|   13|           1703|             384| 443 (%)|
|   12|           1697|             391| 434 (%)|
|   11|           1707|             410| 416 (%)|
|   10|           1226|             197| 622 (%)|
|    9|           1242|             206| 603 (%)|
|    8|           1251|             207| 604 (%)|
|    7|           1213|             208| 583 (%)|
|    6|           1164|             206| 565 (%)|
|    5|           1134|             205| 553 (%)|
|    4|           1106|             203| 545 (%)|
|    3|           1043|             192| 543 (%)|

以下几乎肯定是一个严重的过度简化…

如果我们将写入堆栈的深度设置为16,那么对象创建大约需要40%的时间,实际的堆栈跟踪占据了其中的绝大部分。93%的JavaException对象实例化是由于正在进行堆栈跟踪。这意味着在这种情况下展开堆栈占用了另外50%的时间。

当我们关闭堆栈时,跟踪对象创建所占的比例要小得多 分数即20%和堆栈展开现在占80%的时间

在这两种情况下,堆栈展开占用了总时间的很大一部分。

public class JavaException extends Exception {
JavaException(String reason, int mode) {
super(reason, null, false, false);
}
JavaException(String reason) {
super(reason);
}


public static void main(String[] args) {
int iterations = 1000000;
long create_time_with    = 0;
long create_time_without = 0;
long create_string = 0;
for (int i = 0; i < iterations; i++) {
long start = System.nanoTime();
JavaException jex = new JavaException("testing");
long stop  =  System.nanoTime();
create_time_with += stop - start;


start = System.nanoTime();
JavaException jex2 = new JavaException("testing", 1);
stop = System.nanoTime();
create_time_without += stop - start;


start = System.nanoTime();
String str = new String("testing");
stop = System.nanoTime();
create_string += stop - start;


}
double interval_with    = ((double)create_time_with)/1000000;
double interval_without = ((double)create_time_without)/1000000;
double interval_string  = ((double)create_string)/1000000;


System.out.printf("Time to create %d String objects: %.2f (ms)\n", iterations, interval_string);
System.out.printf("Time to create %d JavaException objects with    stack: %.2f (ms)\n", iterations, interval_with);
System.out.printf("Time to create %d JavaException objects without stack: %.2f (ms)\n", iterations, interval_without);


JavaException jex = new JavaException("testing");
int depth = 14;
int i = depth;
double[] with_stack    = new double[20];
double[] without_stack = new double[20];


for(; i > 0 ; --i) {
without_stack[i] = jex.timerLoop(i, iterations, 0)/1000000;
with_stack[i]    = jex.timerLoop(i, iterations, 1)/1000000;
}
i = depth;
System.out.printf("|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%%)|\n");
for(; i > 0 ; --i) {
double ratio = (with_stack[i] / (double) without_stack[i]) * 100;
System.out.printf("|%5d| %14.0f| %15.0f| %2.0f (%%)| \n", i + 2, with_stack[i] , without_stack[i], ratio);
//System.out.printf("%d\t%.2f (ms)\n", i, ratio);
}
}
private int thrower(int i, int mode) throws JavaException {
ExArg.time_start[i] = System.nanoTime();
if(mode == 0) { throw new JavaException("without stack", 1); }
throw new JavaException("with stack");
}
private int catcher1(int i, int mode) throws JavaException{
return this.stack_of_calls(i, mode);
}
private long timerLoop(int depth, int iterations, int mode) {
for (int i = 0; i < iterations; i++) {
try {
this.catcher1(depth, mode);
} catch (JavaException e) {
ExArg.time_accum[depth] += (System.nanoTime() - ExArg.time_start[depth]);
}
}
//long stop = System.nanoTime();
return ExArg.time_accum[depth];
}


private int bad_method14(int i, int mode) throws JavaException  {
if(i > 0) { this.thrower(i, mode); }
return i;
}
private int bad_method13(int i, int mode) throws JavaException  {
if(i == 13) { this.thrower(i, mode); }
return bad_method14(i,mode);
}
private int bad_method12(int i, int mode) throws JavaException{
if(i == 12) { this.thrower(i, mode); }
return bad_method13(i,mode);
}
private int bad_method11(int i, int mode) throws JavaException{
if(i == 11) { this.thrower(i, mode); }
return bad_method12(i,mode);
}
private int bad_method10(int i, int mode) throws JavaException{
if(i == 10) { this.thrower(i, mode); }
return bad_method11(i,mode);
}
private int bad_method9(int i, int mode) throws JavaException{
if(i == 9) { this.thrower(i, mode); }
return bad_method10(i,mode);
}
private int bad_method8(int i, int mode) throws JavaException{
if(i == 8) { this.thrower(i, mode); }
return bad_method9(i,mode);
}
private int bad_method7(int i, int mode) throws JavaException{
if(i == 7) { this.thrower(i, mode); }
return bad_method8(i,mode);
}
private int bad_method6(int i, int mode) throws JavaException{
if(i == 6) { this.thrower(i, mode); }
return bad_method7(i,mode);
}
private int bad_method5(int i, int mode) throws JavaException{
if(i == 5) { this.thrower(i, mode); }
return bad_method6(i,mode);
}
private int bad_method4(int i, int mode) throws JavaException{
if(i == 4) { this.thrower(i, mode); }
return bad_method5(i,mode);
}
protected int bad_method3(int i, int mode) throws JavaException{
if(i == 3) { this.thrower(i, mode); }
return bad_method4(i,mode);
}
private int bad_method2(int i, int mode) throws JavaException{
if(i == 2) { this.thrower(i, mode); }
return bad_method3(i,mode);
}
private int bad_method1(int i, int mode) throws JavaException{
if(i == 1) { this.thrower(i, mode); }
return bad_method2(i,mode);
}
private int stack_of_calls(int i, int mode) throws JavaException{
if(i == 0) { this.thrower(i, mode); }
return bad_method1(i,mode);
}
}


class ExArg {
public static long[] time_start;
public static long[] time_accum;
static {
time_start = new long[20];
time_accum = new long[20];
};
}

这个例子中的堆栈帧与您通常发现的相比非常小。

您可以使用javap查看字节码

javap -c -v -constants JavaException.class

这是方法4…

   protected int bad_method3(int, int) throws JavaException;
flags: ACC_PROTECTED
Code:
stack=3, locals=3, args_size=3
0: iload_1
1: iconst_3
2: if_icmpne     12
5: aload_0
6: iload_1
7: iload_2
8: invokespecial #6                  // Method thrower:(II)I
11: pop
12: aload_0
13: iload_1
14: iload_2
15: invokespecial #17                 // Method bad_method4:(II)I
18: ireturn
LineNumberTable:
line 63: 0
line 64: 12
StackMapTable: number_of_entries = 1
frame_type = 12 /* same */


Exceptions:
throws JavaException

问题的这一部分…

另一种问这个问题的方法是,如果我创建了一个Exception实例和 一遍又一遍地抛接,这样会快很多吗 比每次抛出时创建一个新的异常好吗?< / p >

似乎是在问是否创建一个异常并将其缓存到某个地方可以提高性能。是的。这与在对象创建时关闭正在写入的堆栈是一样的,因为它已经完成了。

这些是我得到的时间,请在这之后阅读警告…

|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 (%)|

当然,这样做的问题是你的堆栈跟踪现在指向你实例化对象的地方,而不是它被抛出的地方。