问:Java中的异常处理真的很慢吗?

传统观点以及大量谷歌结果表明,不应该将异常逻辑用于Java中的正常程序流。通常会给出两个原因,

它真的很慢——甚至比普通代码慢一个数量级(给出的原因各不相同),

and

它很混乱,因为人们只希望在异常代码中处理错误。

这个问题是关于第一条的。

As an example, this page describes Java exception handling as "very slow" and relates the slowness to the creation of the exception message string - "this string is then used in creating the exception object that is thrown. This is not fast." The article Effective Exception Handling in Java says that "the reason for this is due to the object creation aspect of exception handling, which thereby makes throwing exceptions inherently slow". Another reason out there is that the stack trace generation is what slows it down.

My testing (using Java 1.6.0_07, Java HotSpot 10.0, on 32 bit Linux), indicates that exception handling is no slower than regular code. I tried running a method in a loop that executes some code. At the end of the method, I use a boolean to indicate whether to return or throw. This way the actual processing is the same. I tried running the methods in different orders and averaging my test times, thinking it may have been the JVM warming up. In all my tests, the throw was at least as fast as the return, if not faster (up to 3.1% faster). I am completely open to the possibility that my tests were wrong, but I haven't seen anything out there in the way of the code sample, test comparisons, or results in the last year or two that show exception handling in Java to actually be slow.

引导我走上这条路的是我需要使用的一个API,它将抛出异常作为正常控制逻辑的一部分。我想纠正它们的用法,但现在我可能做不到。我是否应该赞美他们的前瞻思维?

在论文《即时编译中的高效Java异常处理》中,作者建议,即使没有抛出异常,仅异常处理程序的存在就足以阻止JIT编译器正确优化代码,从而降低代码的速度。我还没有测试过这个理论。


当前回答

我认为第一篇文章提到遍历调用堆栈和创建堆栈跟踪是最昂贵的部分,虽然第二篇文章没有这样说,但我认为这是对象创建中最昂贵的部分。John Rose在一篇文章中描述了加速异常的不同技术。(预分配和重用异常,没有堆栈跟踪的异常,等等)

但我仍然认为这应该被认为是一种必要的邪恶,一种最后的手段。John这样做的原因是为了模拟JVM中(还)没有的其他语言的特性。你不应该养成对控制流使用异常的习惯。尤其是因为性能原因!正如您自己在第2条中提到的,这样做可能会掩盖代码中的严重错误,而且对于新程序员来说,维护起来会更加困难。

Java中的微基准测试出奇地难以正确(有人告诉过我),特别是在进入JIT领域时,因此我真的怀疑在现实生活中使用异常是否比“返回”更快。例如,我怀疑您在测试中有2到5个堆栈帧?现在假设您的代码将由JBoss部署的JSF组件调用。现在您可能有一个数页长的堆栈跟踪。

也许您可以发布您的测试代码?

其他回答

关于异常性能的好文章是:

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

实例化vs重用现有的,有堆栈跟踪和没有,等等:

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/etc-EE-shit因为异常(xD)而变慢。

通过重写应用程序控制流,避免异常(返回错误作为返回),提高应用程序的性能10 -100倍,这取决于你抛出它们的频率))

不知道这些主题是否相关,但我曾经想实现一个依赖于当前线程的堆栈跟踪的技巧:我想发现方法的名称,它触发了实例化类中的实例化(是的,这个想法很疯狂,我完全放弃了它)。所以我发现调用Thread.currentThread(). getstacktrace()是非常慢的(由于本机的dumpThreads方法,它在内部使用)。

相应地,Java Throwable有一个本地方法fillInStackTrace。我认为前面描述的kill -catch块以某种方式触发了该方法的执行。

但让我告诉你另一个故事……

在Scala中,一些函数特性是使用ControlThrowable在JVM中编译的,它扩展了Throwable,并以以下方式覆盖了它的fillInStackTrace:

override def fillInStackTrace(): Throwable = this

所以我调整了上面的测试(循环量减少了十,我的机器有点慢:):

class ControlException extends ControlThrowable

class T {
  var value = 0

  def reset = {
    value = 0
  }

  def method1(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      println("You'll never see this!")
    }
  }

  def method2(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      throw new Exception()
    }
  }

  def method3(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new Exception()
    }
  }

  def method4(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new ControlException()
    }
  }
}

class Main {
  var l = System.currentTimeMillis
  val t = new T
  for (i <- 1 to 10000000)
    t.method1(i)
  l = System.currentTimeMillis - l
  println("method1 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method2(i)
  } catch {
    case _ => println("You'll never see this")
  }
  l = System.currentTimeMillis - l
  println("method2 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method4(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method4 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method3(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method3 took " + l + " ms, result was " + t.value)

}

所以,结果是:

method1 took 146 ms, result was 2
method2 took 159 ms, result was 2
method4 took 1551 ms, result was 2
method3 took 42492 ms, result was 2

你看,method3和method4之间唯一的区别是它们会抛出不同类型的异常。是的,method4仍然比method1和method2慢,但是差异是可以接受的。

前段时间,我写了一个类来测试将字符串转换为整数的相对性能,使用两种方法:(1)调用Integer.parseInt()并捕获异常,或者(2)用正则表达式匹配字符串并仅在匹配成功时调用parseInt()。我以最有效的方式使用正则表达式(即,在终止循环之前创建Pattern和Matcher对象),并且我没有打印或保存异常的堆栈跟踪。

对于一个包含10,000个字符串的列表,如果它们都是有效数字,那么parseInt()方法的速度是regex方法的四倍。但如果只有80%的字符串是有效的,则regex的速度是parseInt()的两倍。如果20%是有效的,这意味着异常在80%的时间内被抛出和捕获,则regex的速度大约是parseInt()的20倍。

我对结果感到惊讶,因为regex方法处理了两次有效字符串:一次用于匹配,另一次用于parseInt()。但是抛出和捕获异常完全弥补了这一点。这种情况在现实世界中不太可能经常发生,但如果发生了,您绝对不应该使用异常捕获技术。但如果您只是验证用户输入或类似的东西,务必使用parseInt()方法。

我改变了上面的@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

供你参考,我扩展了Mecki做的实验:

method1 took 1733 ms, result was 2
method2 took 1248 ms, result was 2
method3 took 83997 ms, result was 2
method4 took 1692 ms, result was 2
method5 took 60946 ms, result was 2
method6 took 25746 ms, result was 2

前3个和Mecki的一样(我的笔记本电脑明显慢一些)。

method4和method3是一样的,除了它创建了一个新的Integer(1)而不是抛出一个新的Exception()。

method5类似于method3,除了它创建了新的Exception()而不抛出它。

Method6和method3很像,只是它会抛出一个预先创建的异常(一个实例变量),而不是创建一个新异常。

在Java中,抛出异常的大部分开销是收集堆栈跟踪所花费的时间,这发生在创建异常对象时。抛出异常的实际成本虽然很大,但比创建异常的成本要小得多。