抛出Exception的哪个部分是昂贵的?

在Java中,当实际上没有错误时使用throw / catch作为逻辑的一部分通常是一个坏主意(部分),因为抛出和捕获exception是很昂贵的,而且在循环中多次执行通常比其他控制结构不涉及抛出exception。

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

换句话说,如果我这样做

Exception e = new Exception(); 

但是不要扔它,是抛出的大部分费用,还是抛出+ catch处理什么是昂贵的?

我不问是否将代码放在try / catch块中,是否会增加执行代码的代价,我想知道捕获Exception是否是昂贵的部分,或者创build(调用构造函数)Exception是昂贵的部分。

另一个问这个问题的方法是,如果我做了Exception的一个实例,并且一遍又一遍地抛出并捕获它,那么每次抛出这个exception会比创build一个新的exception快得多吗?

创build一个exception对象并不比创build其他常规对象更昂贵。 主要成本隐藏在本地fillInStackTrace方法中,该方法遍历调用堆栈并收集所有需要的信息以构build堆栈跟踪:类,方法名称,行号等

关于高exception成本的神话来自Throwable函数的大部分隐式调用fillInStackTrace的事实。 但是,有一个构造函数创build一个没有堆栈跟踪的Throwable 。 它可以让你创build非常快的throwables来实例化。 另一种创build轻量级exception的方法是覆盖fillInStackTrace


现在抛出exception呢?
实际上,这取决于抛出exception的位置。

如果它被捕获到相同的方法(或者更确切地说,在相同的上下文中,因为上下文可以包含几种内联方法),所以throwgoto一样快(当然,在JIT编译之后)。

然而,如果一个catch块在堆栈中的某处更深,那么JVM需要展开堆栈帧,这可能需要更长的时间。 如果涉及到synchronized块或方法,则需要更长的时间,因为展开意味着释放已移除的堆栈帧拥有的监视器。


我可以通过适当的基准来确认上述说法,但幸运的是,我并不需要这样做,因为HotSpot性能工程师AlexeyShipilëv的后续工作已经完全涵盖了各个方面: Lil例外的卓越性能 。

大多数Throwable函数中的第一个操作是填充堆栈跟踪,这是大部分花费的地方。

但是,有一个带有标志的受保护的构造函数来禁用堆栈跟踪。 这个构造函数在扩展Exception时也是可访问的。 如果您创build自定义的exceptiontypes,则可以避免创build堆栈跟踪,并以较less的信息为代价获得更好的性能。

如果用普通方法创build任何types的单个exception,则可以多次重新抛出它,而无需填充堆栈跟踪的开销。 但是,它的堆栈跟踪将反映它的构build位置,而不是它在特定实例中的位置。

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

如果消除堆栈跟踪生成,另一个主要成本就是在抛出和捕获之间展开堆栈。 发现exception之前遇到的介入帧数越less,就越快。

devise你的程序,以便只在真正例外的情况下抛出exception,像这样的优化是很难certificate的。

这里有一个很好的例外。

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

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

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

 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,那么创build对象的时间大约是40%,实际的堆栈跟踪占绝大多数。 实例化JavaException对象的〜93%是由于采取了堆栈跟踪。 这意味着,在这种情况下退卷是另外50%的时间。

当我们closures堆栈跟踪对象创build帐户的一个小得多的部分,即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]; }; } 

这个例子中的堆栈框架与通常find的相比是微小的。

您可以使用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 

使用null堆栈跟踪创buildException需要的时间与throwtry-catch块的时间大致相同。 但是, 填充堆栈跟踪平均需要5倍的时间

我创build了以下基准来演示对性能的影响。 我在运行configuration中添加了-Djava.compiler=NONE来禁用编译器优化。 为了衡量构build堆栈跟踪的影响,我扩展了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 

这意味着创build一个NoStackException几乎和重复抛出相同的Exception一样昂贵。 它还显示创build一个Exception并填充堆栈跟踪需要大约4倍的时间。

这部分的问题…

另一个问这个问题的方法是,如果我做了Exception的一个实例,并且一遍又一遍地抛出并捕获它,那么每次抛出这个exception会比创build一个新的exception快得多吗?

似乎问是否创build一个exception,并caching在某处提高性能。 是的,它确实。 这与closures在对象创build中写入的堆栈相同,因为它已经完成。

这些是我得到的时间,请在此之后注意警告…

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

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

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

除了添加一个Exception实例重复抛出的情况之外,我还closures了编译器优化,以便我们可以获得准确的性能结果。 根据这个答案 ,我添加了-Djava.compiler=NONE到VM参数。 (在eclipse中,编辑运行configuration→参数来设置这个VM参数)

结果:

 new Exception + throw/catch = 643.5 new Exception only = 510.7 throw/catch only = 115.2 new String (benchmark) = 669.8 

所以创造这个例外的成本大约是投掷+捕获的5倍。 假设编译器不会优化大部分的成本。

为了比较,这里是相同的testing运行,而不禁用优化:

 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; } }