关于java:抛出异常的哪个部分很贵?

Which part of throwing an Exception is expensive?

在Java中,当不存在错误时,使用抛出/ catch作为逻辑的一部分通常是一个坏主意(部分),因为抛出和捕获异常是昂贵的,并且在一个循环中进行多次操作通常要比不涉及抛出异常的其他控制结构慢得多。

我的问题是,在抛出/捕获本身,或者在创建异常对象时(因为它获得了很多运行时信息,包括执行堆栈),会产生成本吗?

换句话说,如果我这样做的话

1
Exception e = new Exception();

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

我不是在问将代码放入try/catch块是否会增加执行代码的成本,而是在问捕获异常是代价高昂的部分,还是创建(调用构造函数)异常是代价高昂的部分。

另一种问这个问题的方法是,如果我创建了一个异常实例,然后一次又一次地抛出并捕获它,那么这是否比每次抛出新的异常都要快得多呢?


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

关于高异常成本的神话来自这样一个事实,即大多数Throwable构造函数隐式地称为fillInStackTrace。但是,有一个构造函数可以创建一个没有堆栈跟踪的Throwable。它允许您创建快速实例化的一次性文件。创建轻量级异常的另一种方法是重写fillInStackTrace

那么抛出一个异常呢?实际上,这取决于在何处捕获抛出的异常。

如果它被捕获在同一个方法中(或者更准确地说,在同一个上下文中,因为上下文可能由于内联而包含多个方法),那么throwgoto一样快速简单(当然,在JIT编译之后)。

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

我可以通过适当的基准来确认上述声明,但幸运的是,我不需要这样做,因为Hotspot的绩效工程师Alexey Shipilev:LIL’Exception的出色表现已经完全涵盖了所有方面。


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

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

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

Java的当前版本尝试优化堆栈跟踪创建。调用本机代码来填充堆栈跟踪,该跟踪以较轻的本机结构记录跟踪。只有当EDCOX1、12、11、EDCOX1、13或其他需要跟踪的方法被调用时,相应的JavaEDCX1×O.0对象才被懒惰地创建。

如果消除了堆栈跟踪的生成,则另一个主要成本是在抛出和捕获之间展开堆栈。捕获异常之前遇到的中间帧越少,速度越快。

设计您的程序,使异常只在真正的异常情况下抛出,这样的优化是很难证明的。


这里有一篇关于例外情况的好文章。

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

结果表明,叠痕构造和叠放是成本较高的零件。下面的代码利用了1.7中的一个特性,我们可以打开和关闭堆栈跟踪。然后我们可以用这个来看看不同情况下的成本。

以下是单独创建对象的时间。我在这里添加了String,这样您就可以看到,如果不编写堆栈,在创建JavaException对象和String时几乎没有区别。打开堆栈写入时,差异非常显著(至少慢一个数量级)。

1
2
3
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)

下面显示了在某个特定深度上投掷100万次后返回的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|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%的时间。

当我们关闭堆栈跟踪对象创建帐户时分数IE 20%和堆栈展开现在占80%的时间。

在这两种情况下,堆栈展开需要大量的总时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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)
"
, iterations, interval_string);
    System.out.printf("Time to create %d JavaException objects with    stack: %.2f (ms)
"
, iterations, interval_with);
    System.out.printf("Time to create %d JavaException objects without stack: %.2f (ms)
"
, 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(%%)|
"
);
    for(; i > 0 ; --i) {
      double ratio = (with_stack[i] / (double) without_stack[i]) * 100;
      System.out.printf("|%5d| %14.0f| %15.0f| %2.0f (%%)|
"
, i + 2, with_stack[i] , without_stack[i], ratio);
      //System.out.printf("%d\t%.2f (ms)
", 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查看字节码

1
javap -c -v -constants JavaException.class

这是方法4…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
   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堆栈跟踪创建Exception的时间大约与throwtry-catch块的时间相同。但是,填充堆栈跟踪平均需要5倍的时间。

我创建了以下基准来演示对性能的影响。我将-Djava.compiler=NONE添加到运行配置以禁用编译器优化。为了测量构建堆栈跟踪的影响,我扩展了Exception类以利用无堆栈构造函数:

1
2
3
4
5
class NoStackException extends Exception{
    public NoStackException() {
        super("",null,false,false);
    }
}

基准代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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;
    }

}

输出:

1
2
3
4
throwCatchTime = 19
newExceptionTime = 77
newObjectTime = 3
noStackExceptionTime = 15

这意味着创建一个NoStackException的成本与重复地抛出同一个Exception的成本差不多。它还表明,创建一个Exception并填充其堆栈跟踪大约需要4倍的时间。


这部分问题…

Another way of asking this is, if I made one instance of Exception and
threw and caught it over and over, would that be significantly faster
than creating a new Exception every time I throw?

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|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的回答为起点,我做了一些调整。代码在底部。

除了添加重复抛出一个异常实例的情况外,我还关闭了编译器优化,这样我们可以得到准确的性能结果。根据这个答案,我在vm参数中添加了-Djava.compiler=NONE。(在Eclipse中,编辑run configuration&rarr;参数以设置此VM参数)

结果:

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

因此,创建异常的成本大约是抛出+捕获异常的5倍。假设编译器没有优化大部分成本。

为了进行比较,以下是相同的测试运行,但不禁用优化:

1
2
3
4
new Exception + throw/catch = 382.6
new Exception only          = 379.5
throw/catch only            = 0.3
new String (benchmark)      = 15.6

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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;
    }
}