Java中异常对性能有何影响?

560

问题:Java中的异常处理实际上很慢吗?

传统智慧以及许多谷歌搜索结果都表明,在Java中,异常逻辑不应用于正常程序流程。通常给出两个原因:

  1. 它非常缓慢——甚至比常规代码慢一个数量级(给出的原因有所不同),

  1. 这很混乱,因为人们只期望在异常代码中处理错误。

这个问题涉及到第一个原因。

例如,this page将Java异常处理描述为“非常缓慢”,并将其缓慢性与异常消息字符串的创建相关联——“然后使用该字符串创建抛出的异常对象。这不快。”文章Effective Exception Handling in Java表示,“原因是由于异常处理的对象创建方面,从而使抛出异常本质上变慢”。还有另外一个原因是堆栈跟踪生成会导致其变慢。

使用Java 1.6.0_07,Java HotSpot 10.0,在32位Linux上进行测试表明,异常处理不比常规代码慢。我尝试在循环中运行一个执行某些代码的方法。在方法结束时,我使用布尔值来指示是要返回还是抛出。这样实际处理是相同的。我尝试以不同的顺序运行方法并平均测试时间,认为可能是JVM在热身。在所有测试中,如果不是更快(高达3.1%),则抛出至少与返回一样快。我完全开放我的测试可能是错误的可能性,但是在过去一两年中,我没有看到任何关于Java异常处理实际上会变慢的代码示例,测试比较或结果。

导致我走这条路的是我需要使用的API将异常作为正常控制逻辑的一部分抛出。我想在使用中更正它们,但现在我可能无法这样做。我是否需要称赞他们的前瞻性?

在论文Efficient Java exception handling in just-in-time compilation中,作者建议即使没有抛出异常,仅异常处理程序的存在就足以防止JIT编译器正确优化代码,从而减慢速度。我尚未测试此理论。


13
我知道你并没有询问关于第二点,但你应该认识到,使用异常来控制程序流程并不比使用GOTO更好。一些人会为GOTO进行辩护,一些人会为你所说的做法进行辩护,但如果你询问那些实施和维护过这两种方法的人,他们会告诉你这两种方法都是糟糕且难以维护的设计实践(并且可能会咒骂那个认为自己足够聪明能够决定使用它们的人)。 - Bill K
97
声称使用异常处理程序流程不比使用GOTO好不如声称使用条件和循环处理程序流程不比使用GOTO好,这是一个转移注意力的话题。解释一下,其他编程语言可以有效地使用异常处理程序流程。惯用的Python代码经常使用异常处理程序,例如。我可以并且确实维护过使用此方式的代码(不包括Java),我认为这种方式本质上没有任何问题。 - mmalone
23
在Java中,使用异常来进行正常控制流是不好的,因为这种范式选择是这样做的。请阅读Bloch EJ2——他明确指出,引用(Item 57),“异常正如其名称所示,只应用于异常情况;它们永远不应该用于普通的控制流”——并详细解释了原因。而且,他是编写Java库的人。因此,他是定义类API契约的人。/同意Bill K 的观点。 - user719662
8
如果某个框架在非异常情况下使用异常,那么它的设计存在缺陷并违反了语言中异常类的约定。仅仅因为某些人编写了糟糕的代码,并不会使其变得更好。 - user719662
11
就异常处理而言,stackoverflow.com的创始人也有错误。软件开发的黄金准则是不要让简单变得复杂和难以控制。 他写道:“确实,当你添加良好的错误检查时,本应只需三行代码的程序往往会变成48行代码,但这就是生活...” 这是对纯粹性的追求,而非简洁性。 - sf_jeff
显示剩余7条评论
18个回答

3

关于异常性能的优秀文章:

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

实例化与重用现有对象,带有堆栈跟踪和不带堆栈跟踪等:

Benchmark                            Mode   Samples         Mean   Mean error  Units

dynamicException                     avgt        25     1901.196       14.572  ns/op
dynamicException_NoStack             avgt        25       67.029        0.212  ns/op
dynamicException_NoStack_UsedData    avgt        25       68.952        0.441  ns/op
dynamicException_NoStack_UsedStack   avgt        25      137.329        1.039  ns/op
dynamicException_UsedData            avgt        25     1900.770        9.359  ns/op
dynamicException_UsedStack           avgt        25    20033.658      118.600  ns/op

plain                                avgt        25        1.259        0.002  ns/op
staticException                      avgt        25        1.510        0.001  ns/op
staticException_NoStack              avgt        25        1.514        0.003  ns/op
staticException_NoStack_UsedData     avgt        25        4.185        0.015  ns/op
staticException_NoStack_UsedStack    avgt        25       19.110        0.051  ns/op
staticException_UsedData             avgt        25        4.159        0.007  ns/op
staticException_UsedStack            avgt        25       25.144        0.186  ns/op

根据堆栈跟踪的深度:

Benchmark        Mode   Samples         Mean   Mean error  Units

exception_0000   avgt        25     1959.068       30.783  ns/op
exception_0001   avgt        25     1945.958       12.104  ns/op
exception_0002   avgt        25     2063.575       47.708  ns/op
exception_0004   avgt        25     2211.882       29.417  ns/op
exception_0008   avgt        25     2472.729       57.336  ns/op
exception_0016   avgt        25     2950.847       29.863  ns/op
exception_0032   avgt        25     4416.548       50.340  ns/op
exception_0064   avgt        25     6845.140       40.114  ns/op
exception_0128   avgt        25    11774.758       54.299  ns/op
exception_0256   avgt        25    21617.526      101.379  ns/op
exception_0512   avgt        25    42780.434      144.594  ns/op
exception_1024   avgt        25    82839.358      291.434  ns/op

要了解其他细节(包括JIT的x64汇编),请阅读原始博客文章。

这意味着Hibernate/Spring等EE框架因为异常而变慢(xD)。

通过重写应用程序控制流以避免异常(将错误作为return返回),可以提高应用程序的性能10倍至100倍,具体取决于您抛出异常的频率。))


4
这篇文章很棒,但你提到的Hibernate/Spring/EE因为异常而变慢的结论在这里没有任何依据。如果你的Hibernate/Spring应用程序的CPU利用率达到上限,则可能是这个原因。但更有可能的是其他原因导致性能不佳,比如完全不理解Hibernate在底层执行什么操作,使用ORM并不意味着你会自动获得良好的性能,必须仔细检查它正在执行哪些SQL语句(以及数量)是否非常低效。 - john16384

3

HotSpot可以很好地删除系统生成的异常代码,只要它被内联。然而,显式创建的异常和其他未被删除的异常花费了大量时间来创建堆栈跟踪。重写fillInStackTrace方法可以看到这如何影响性能。


2
即使抛出异常不慢,但将其用于正常程序流程仍然是一个坏主意。这种用法类似于GOTO...。
我猜这并没有真正回答问题。我想在早期的Java版本(<1.4)中,抛出异常被认为是慢的“常规”智慧。创建异常需要VM创建整个堆栈跟踪。自那时以来,VM发生了很多变化以加速事情,并且这可能是已经改进的一个领域。

1
定义“正常程序流程”会很有帮助。已经有很多关于使用已检查异常作为业务流程失败和未检查异常用于不可恢复性故障的文章,因此从某种意义上说,业务逻辑中的故障仍然可以被视为正常流程。 - Spencer Kormos
2
@Spencer K:异常,顾名思义,意味着发现了一个异常情况(文件消失了,网络突然关闭了...)。这意味着这种情况是意外的。如果预计到这种情况会发生,我不会使用异常来处理它。 - Mecki
2
@Mecki:没错。最近我和某人讨论了这个问题……他们正在编写一个验证框架,并在验证失败时抛出异常。我认为这是一个不好的想法,因为这种情况很常见。我宁愿看到该方法返回一个 ValidationResult。 - user38051
1
@Mecki:嗯,ThrowableException的超类,而名称Throwable并不意味着发生了什么异常情况。 - KajMagnus
2
在控制流方面,异常类似于breakreturn,而不是goto - Hot Licks
3
有大量的编程范式。无法有单一的“正常流程”,不管你指的是什么。基本上,异常机制只是一种快速离开当前框架并展开堆栈直到某个点的方式。“异常”这个词并没有暗示它的“意外”本质。一个快速的例子:当路由过程中出现某些情况时,从Web应用程序“抛出”404非常自然。为什么不能使用异常来实现这种逻辑呢?什么是反模式? - BorisOkunskiy

2

比较一下Integer.parseInt和以下方法。如果数据无法解析,该方法仅返回默认值,而不会抛出异常:

  public static int parseUnsignedInt(String s, int defaultValue) {
    final int strLength = s.length();
    if (strLength == 0)
      return defaultValue;
    int value = 0;
    for (int i=strLength-1; i>=0; i--) {
      int c = s.charAt(i);
      if (c > 47 && c < 58) {
        c -= 48;
        for (int j=strLength-i; j!=1; j--)
          c *= 10;
        value += c;
      } else {
        return defaultValue;
      }
    }
    return value < 0 ? /* übergebener wert > Integer.MAX_VALUE? */ defaultValue : value;
  }

只要你对“有效”数据应用两种方法,它们的工作速度大约相同(即使Integer.parseInt可以处理更复杂的数据)。但是,一旦您尝试解析无效数据(例如1,000,000次解析“abc”),性能差异就应该是重要的。

1

使用附加的代码,在JDK 15上,@Mecki测试案例的结果完全不同。这基本上会在5个循环中运行代码,第一个循环会稍微短一些,以便给虚拟机一些时间来预热。

结果如下:

Loop 1 10000 cycles
method1 took 1 ms, result was 2
method2 took 0 ms, result was 2
method3 took 22 ms, result was 2
method4 took 22 ms, result was 2
method5 took 24 ms, result was 2
Loop 2 10000000 cycles
method1 took 39 ms, result was 2
method2 took 39 ms, result was 2
method3 took 1558 ms, result was 2
method4 took 1640 ms, result was 2
method5 took 1717 ms, result was 2
Loop 3 10000000 cycles
method1 took 49 ms, result was 2
method2 took 48 ms, result was 2
method3 took 126 ms, result was 2
method4 took 88 ms, result was 2
method5 took 87 ms, result was 2
Loop 4 10000000 cycles
method1 took 34 ms, result was 2
method2 took 34 ms, result was 2
method3 took 33 ms, result was 2
method4 took 98 ms, result was 2
method5 took 58 ms, result was 2
Loop 5 10000000 cycles
method1 took 34 ms, result was 2
method2 took 33 ms, result was 2
method3 took 33 ms, result was 2
method4 took 48 ms, result was 2
method5 took 49 ms, result was 2

package hs.jfx.eventstream.api;

public class Snippet {
  int value;


  public int getValue() {
      return value;
  }

  public void reset() {
      value = 0;
  }

  // Calculates without exception
  public void method1(int i) {
      value = ((value + i) / i) << 1;
      // Will never be true
      if ((i & 0xFFFFFFF) == 1000000000) {
          System.out.println("You'll never see this!");
      }
  }

  // Could in theory throw one, but never will
  public void method2(int i) throws Exception {
      value = ((value + i) / i) << 1;
      // Will never be true
      if ((i & 0xFFFFFFF) == 1000000000) {
          throw new Exception();
      }
  }

  private static final NoStackTraceRuntimeException E = new NoStackTraceRuntimeException();

  // This one will regularly throw one
  public void method3(int i) throws NoStackTraceRuntimeException {
      value = ((value + i) / i) << 1;
      // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
      // an AND operation between two integers. The size of the number plays
      // no role. AND on 32 BIT always ANDs all 32 bits
      if ((i & 0x1) == 1) {
          throw E;
      }
  }

  // This one will regularly throw one
  public void method4(int i) throws NoStackTraceThrowable {
      value = ((value + i) / i) << 1;
      // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
      // an AND operation between two integers. The size of the number plays
      // no role. AND on 32 BIT always ANDs all 32 bits
      if ((i & 0x1) == 1) {
          throw new NoStackTraceThrowable();
      }
  }

  // This one will regularly throw one
  public void method5(int i) throws NoStackTraceRuntimeException {
      value = ((value + i) / i) << 1;
      // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
      // an AND operation between two integers. The size of the number plays
      // no role. AND on 32 BIT always ANDs all 32 bits
      if ((i & 0x1) == 1) {
          throw new NoStackTraceRuntimeException();
      }
  }

  public static void main(String[] args) {
    for(int k = 0; k < 5; k++) {
      int cycles = 10000000;
      if(k == 0) {
        cycles = 10000;
        try {
          Thread.sleep(500);
        }
        catch(InterruptedException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
      }
      System.out.println("Loop " + (k + 1) + " " + cycles + " cycles");
      int i;
      long l;
      Snippet t = new Snippet();

      l = System.currentTimeMillis();
      t.reset();
      for (i = 1; i < cycles; i++) {
          t.method1(i);
      }
      l = System.currentTimeMillis() - l;
      System.out.println(
          "method1 took " + l + " ms, result was " + t.getValue()
      );

      l = System.currentTimeMillis();
      t.reset();
      for (i = 1; i < cycles; i++) {
          try {
              t.method2(i);
          } catch (Exception e) {
              System.out.println("You'll never see this!");
          }
      }
      l = System.currentTimeMillis() - l;
      System.out.println(
          "method2 took " + l + " ms, result was " + t.getValue()
      );

      l = System.currentTimeMillis();
      t.reset();
      for (i = 1; i < cycles; i++) {
          try {
              t.method3(i);
          } catch (NoStackTraceRuntimeException e) {
            // always comes here
          }
      }
      l = System.currentTimeMillis() - l;
      System.out.println(
          "method3 took " + l + " ms, result was " + t.getValue()
      );


      l = System.currentTimeMillis();
      t.reset();
      for (i = 1; i < cycles; i++) {
          try {
              t.method4(i);
          } catch (NoStackTraceThrowable e) {
            // always comes here
          }
      }
      l = System.currentTimeMillis() - l;
      System.out.println( "method4 took " + l + " ms, result was " + t.getValue() );


      l = System.currentTimeMillis();
      t.reset();
      for (i = 1; i < cycles; i++) {
          try {
              t.method5(i);
          } catch (RuntimeException e) {
            // always comes here
          }
      }
      l = System.currentTimeMillis() - l;
      System.out.println( "method5 took " + l + " ms, result was " + t.getValue() );
    }
  }

  public static class NoStackTraceRuntimeException extends RuntimeException {
    public NoStackTraceRuntimeException() {
        super("my special throwable", null, false, false);
    }
  }

  public static class NoStackTraceThrowable extends Throwable {
    public NoStackTraceThrowable() {
        super("my special throwable", null, false, false);
    }
  }
}


但这与什么相关呢?我的基准测试并不是为了证明异常很慢,只是为了测试它们是否慢。我只是分享了我的结果作为样本输出,而不是为了证明任何观点。它们现在可能以不同的方式实现,正如我所解释的,你可以用许多方法来实现它们。这怎么能证明我在答案中写的任何内容都是错误的呢?当我说它取决于时,我的回答的第一句话哪部分不清楚呢?它还取决于系统,JVM代码并不是所有系统都相同的,我只是说“不要指望它们很快”。所以我不明白你的意思。 - Mecki
我来到这里寻找问题的答案。当我发现当前回答中的某些内容在最新硬件/版本上不再准确时,我倾向于留下评论,以便其他人获得更好的信息。 - john16384

0

我修改了@Mecki上面的答案,让method1返回一个布尔值,并在调用方法中进行检查,因为你不能只是用空值替换异常。经过两次运行,method1仍然是最快的或者和method2一样快。

以下是代码的快照:

// Calculates without exception
public boolean method1(int i) {
    value = ((value + i) / i) << 1;
    // Will never be true
    return ((i & 0xFFFFFFF) == 1000000000);

}
....
   for (i = 1; i < 100000000; i++) {
            if (t.method1(i)) {
                System.out.println("Will never be true!");
            }
    }

并且结果为:

运行1

method1 took 841 ms, result was 2
method2 took 841 ms, result was 2
method3 took 85058 ms, result was 2

运行 2

method1 took 821 ms, result was 2
method2 took 838 ms, result was 2
method3 took 85929 ms, result was 2

-3

关于异常速度与编程检查数据的观点。

许多类都有字符串转值转换器(扫描器/解析器),还有受人尊敬和知名的库;)

通常具有形式

class Example {
public static Example Parse(String input) throws AnyRuntimeParsigException
...
}

异常名称仅为示例,通常是未经检查的(运行时),因此抛出声明仅是我的图示。

有时存在第二种形式:

public static Example Parse(String input, Example defaultValue)

永不抛出异常

当第二个选项不可用(或程序员阅读的文档太少,只使用第一个选项)时,可以使用正则表达式编写此类代码。正则表达式很酷,政治正确等等:

Xxxxx.regex(".....pattern", src);
if(ImTotallySure)
{
  Example v = Example.Parse(src);
}

使用这段代码,程序员不需要承担异常的成本。但是与有时需要承担的小成本的异常相比,正则表达式始终具有非常高的成本。

在这种情况下,我几乎总是使用异常。

try { parse } catch(ParsingException ) // concrete exception from javadoc
{
}

不需要分析堆栈跟踪等,我相信在您的讲座后会变得更快。

不要害怕异常


-6
为什么异常会比普通返回慢呢?
只要您不将堆栈跟踪打印到终端并保存到文件中或类似的操作,catch块并不会比其他代码块执行更多的工作。因此,我无法想象为什么“throw new my_cool_error()”会这么慢。
好问题,我期待进一步了解这个话题!

18
即使异常并没有被使用,它也必须捕获有关堆栈跟踪的信息。 - Jon Skeet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接