在Java中,使用throw / catch作为逻辑的一部分,当实际上并不是错误通常是一个坏主意(部分),因为抛出和捕获异常是昂贵的,并且在循环通常比其他不涉及抛出异常的控制结构慢得多。
我的问题是,抛出/捕获本身或创建Exception对象时产生的成本(因为它获取了大量运行时信息,包括执行堆栈)?
换句话说,如果我这样做
Exception e = new Exception();
但是不要扔它,是投掷的大部分成本,还是投掷+捕获处理的代价是什么?
我没有问是否将代码放入try / catch块会增加执行该代码的成本,我问是否捕获Exception是昂贵的部分,还是创建(调用构造函数为)例外是昂贵的部分。
另一种问这个问题的方法是,如果我创建了一个Exception实例并且一遍又一遍地抛出它,那么这会比每次抛出时创建一个新的Exception快得多吗?
答案 0 :(得分:251)
创建异常对象并不比创建其他常规对象更昂贵。主要成本隐藏在本地fillInStackTrace
方法中,该方法遍历调用堆栈并收集构建堆栈跟踪所需的所有信息:类,方法名称,行号等。
关于高异常成本的神话来自于大多数Throwable
构造函数隐含地调用fillInStackTrace
这一事实。但是,有一个constructor来创建没有堆栈跟踪的Throwable
。它允许您制作非常快速实例化的throwable。创建轻量级异常的另一种方法是覆盖fillInStackTrace
。
那么抛出异常呢? 实际上,它取决于抛出的异常被捕获的位置。
如果它被捕获在相同的方法中(或者更确切地说,在相同的上下文中,由于上下文可以包括由于内联而导致的多种方法),那么throw
与goto
一样快速和简单(当然,在JIT编译之后)。
但是如果catch
块位于堆栈的更深处,那么JVM需要展开堆栈帧,这可能需要更长的时间。如果涉及synchronized
块或方法,则需要更长的时间,因为展开意味着释放由移除的堆栈帧拥有的监视器。
我可以通过适当的基准来确认上述陈述,但幸运的是我不需要这样做,因为HotSpot的绩效工程师Alexey Shipilev的帖子已经完全涵盖了所有方面:{{ 3}}
答案 1 :(得分:72)
大多数Throwable
构造函数中的第一个操作是fill in the stack trace,,这是大部分费用的所在。
但是,有一个受保护的构造函数,其中包含一个禁用堆栈跟踪的标志。在扩展Exception
时也可以访问This constructor。如果创建自定义异常类型,则可以避免创建堆栈跟踪并以更少的信息为代价获得更好的性能。
如果通过常规方法创建任何类型的单个异常,则可以多次重新抛出它,而无需填充堆栈跟踪的开销。但是,它的堆栈跟踪将反映它的构造位置,而不是它在特定实例中抛出的位置。
当前版本的Java尝试优化堆栈跟踪创建。调用本机代码以填充堆栈跟踪,该跟踪以较轻的本机结构记录跟踪。仅当getStackTrace()
,printStackTrace()
或其他需要跟踪的方法被调用时,才会从此记录中延迟创建相应的Java StackTraceElement
对象。
如果消除堆栈跟踪生成,另一个主要成本是在throw和catch之间展开堆栈。捕获异常之前遇到的中间帧越少,这种情况就越快。
设计您的程序,以便仅在真正特殊的情况下抛出异常,并且这些优化很难证明是合理的。
答案 2 :(得分:25)
这里有一篇关于例外的好文章。
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%的时间,实际的堆栈跟踪占据了绝大多数。实例化JavaException对象的~93%是由于采用了堆栈跟踪。这意味着在这种情况下展开堆栈会占用另外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
答案 3 :(得分:12)
使用Exception
堆栈跟踪创建null
所需的时间与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
这意味着创建NoStackException
的费用与重复投放Exception
的费用差不多。它还表明,创建Exception
并填充其堆栈跟踪大约需要 4x 。
答案 4 :(得分:4)
这部分问题......
另一种问这个问题的方法是,如果我创建了一个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 (%)|
当然,问题是你的堆栈跟踪现在指向你实例化对象的位置而不是它被抛出的位置。
答案 5 :(得分:3)
以@ AustinD的答案为出发点,我做了一些调整。代码在底部。
除了添加重复抛出一个Exception实例的情况之外,我还关闭了编译器优化,以便我们可以获得准确的性能结果。我根据this answer向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;
}
}