Java异常有多慢?

  • 回答 (2)
  • 关注 (0)
  • 查看 (110)

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

传统的观点以及大量的结果都表明,特殊的逻辑不应该用于Java中的正常程序流程。通常有两个原因,

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

它杂乱无章,因为人们只希望在特殊代码中处理错误。

作为一个例子,这个页面将Java异常处理描述为“非常慢”,并且将慢速与创建异常消息字符串相关联 - “然后使用该字符串创建抛出的异常对象,这不是很快。Java中的有效异常处理( The Effective Exception Handling)在Java中表示,“其原因是由于异常处理的对象创建方面的原因,从而导致异常抛出异常缓慢”。另一个原因是堆栈跟踪的生成是减慢速度。

我的测试(在32位Linux上使用Java 1.6.0_07,Java HotSpot 10.0)表明异常处理不会比普通代码慢。我试着在执行一些代码的循环中运行一个方法。在该方法的最后,我使用一个布尔值来指示是返回还是抛出。这样的实际处理是一样的。我试着以不同的顺序运行这些方法,并平均测试时间,认为这可能是JVM热身。在我所有的测试中,投掷的速度至少与回归速度一样快,如果不快的话(速度快3.1%)。我对我的测试是错误的可能性完全保持开放,但是在代码示例,测试比较或上一两年的结果中,我没有看到任何东西,它们显示Java中的异常处理实际上是慢的。

导致我走这条路的是我需要使用的API,它将异常作为正常控制逻辑的一部分。我想纠正他们的用法,但现在我可能无法。相反,我会不得不赞扬他们的前瞻性思维?

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

sunny陆小维sunny陆小维提问于
心愿回答于

仅供参考,我扩展了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

前三个和Mecki一样(我的笔记本电脑显然比较慢)。

方法4与方法3相同,除了它创建一个new Integer(1)而不是做throw new Exception()。

方法5就像方法3,只是它创建了new Exception()没有抛出它。

method6就像method3,只不过它抛出了一个预先创建的异常(一个实例变量)而不是创建一个新的异常。

在Java中,引发异常的大部分花费是收集堆栈跟踪所花费的时间,这是在创建异常对象时发生的。抛出异常的实际成本虽然很大,但远低于创建异常的成本。

嗨喽你好摩羯座回答于

实际上,真正要讨论的问题并不是,“相对‘那些不会发生错误的代码’来说,‘那些以异常形式上报的错误’会有多慢?”,因为你可能也认同“已接受的回答”。相反,真正的问题是,“相对‘那些以其他形式上报的错误’来说,‘那些以异常形式上报的错误’会有多慢?”

通常认为,“不要抛出你想要捕获的异常”。所以,抛出一个其他人——如平台或框架API——要捕获的异常是合适的。或者在编写一些工具API时,抛出异常也可以的,如日志记录或消息发送,这些操作需要处理外部虚拟机的错误,例如文件IO或网络IO错误。

这是适合抛出异常的例子,应该没有人会在这些例子上有争议。现在,看一下简单方法中出现错误时会发生什么。假设方法签名如下:

/**
 * Transforms SomeClass into SomeOtherClass.
 * @param input some class instance
 * @return the transformed instance,
 *         or null if the transformation was unsuccessful
 */

public SomeOtherClass transform(SomeClass input)

调用该方法的代码如下所示:

SomeClass input = ... // get the input from somewhere
SomeOtherClass result = transform(input);
if(result == null) {
   // handle the failure
} else {
   // use the result
}

但现在,当方法返回null时,我们想知道哪里出现错误了。简单来说可以这样:

/**
 * Transforms SomeClass into SomeOtherClass.
 * @param input some class instance
 * @return the transformed instance, never null
 * @throws TransformationException on failure
 */
public SomeOtherClass transform(SomeClass input) throws TransformationException

为了实现这个功能,你需要将“return null”改为“return new TransformationException(“reason”);”。调用方法需要做改动么?

SomeClass input = ... // get the input from somewhere
try {
   SomeOtherClass result = transform(input);
   // use the result
} catch(TransformationException e) {
   // handle the failure
}

没有人会去读上面的代码块,没什么意义。所以也没什么可惊讶的。你可能每天都在写类似的代码,但也说不上是“代码异味”。可是,将来你会明白在“已预料到”的错误上使用异常是多么大的错误假设有一天你开始读到在“已预料到”的错误上使用异常是非常不好的。现在,捕获“未预料到”异常是非常可笑的,因为编写catch代码块,并显式的处理异常本身就是预料到会有异常。但没关系,我们还可以修改代码改变这种窘境。于是,我们退回到了C语言时代,返回一个异常值来表示错误。

/**
 * Transforms SomeClass into SomeOtherClass.
 * @param input some class instance
 * @return the SomeOtherClass instance or, 
 *    if the transform fails, a TransformFailure instance 
 */

public Object transform(SomeClass input)

于是,调用部分的代码也不得不奇葩一些:

SomeClass input = ... // get the input from somewhere
Object result = transform(input);
if(result instanceof SomeOtherClass) {
   SomeOtherClass success = (SomeOtherClass)result;
   // use the result
} else {
   TransformFailure failure = (TransformFailure)result;
   // handle the failure
}

所以,如果你觉着使用异常有代码异味,但可以接受打破类型安全,那么你最终要面对的是难以维护,没法使用,仅仅比基于异常的解决方案快两倍的代码。但是你又不能接受类型安全被破坏,因为这2倍的性能提升还未被证明,现在就用实在太鲁莽。所以,你决定使用结果对象,而不是返回异常值。

/**
  * Transforms SomeClass into SomeOtherClass.
  * @param input some class instance
  * @return the TransformResult instance
  */
 public TransformResult transform(SomeClass input)

现在,相应地,调用部分的代码变成了这样:

SomeClass input = ... // get the input from somewhere
TransformResult result = transform(input);
if(result.isSuccess()) {
   SomeOtherClass success = result.getSuccess();
   // use the result
} else {
   TransformFailure failure = result.getFailure();
   // handle the failure
}

现在,我们有了一个隐晦,但可管理的解决方案。可是,它比使用异常的第一个方案慢2倍,即使你将TransformResult和TransformFailure合并也是一样的。再说明一遍,使用结果对象比使用异常慢,即使在调用过程中发生了错误。每次你都需要创建一个新的结果对象,这没什么实际意义,而异常对象只在发生错误的时候才会创建。

对于异常,还有一个要讨论的地方。假设有人在使用方法transform时,没有认真看javadoc。在使用异常的例子中,会有下面的代码:

SomeClass input = ... // get the input from somewhere
SomeOtherClass result = transform(input);
// use the result

在使用异常的例子中,他们知道返回值的类型,以及是否一个“已检查异常”,他们可能会得到一个编译时错误,或者他们会在throws语句中声明相应的异常。即使是“未检查异常”,错误会传递到上层调用。现在,考虑使用异常返回值的例子:

SomeClass input = ... // get the input from somewhere
SomeOtherClass result = (SomeOtherClass)transform(input);
// use the result

这个粗心的用户写的代码看起来挺漂亮,但当运行过程中发生错误时,就满不是那么回事了。那时,你费尽力气提供的错误信息会因为发生了ClassCastException异常为全部丢失。使用结果对象也不会好到哪去。

SomeClass input = ... // get the input from somewhere
SomeOtherClass result = transform(input).getSuccess();
// use the result

再说一遍,上面的代码看来相当正常。如果他们盲目使用本文中给出的第一个方法,那么在程序运行过程中,肯定会出现NullPointerException异常。

这里主要想说的是,处理逻辑错误时,使用异常的例子可以按预想的方式正常工作,报告错误信息。但是其他的解决方案却会产生一些没用的异常,即使你已经正确将软件重新部署了一遍,它仍然会出错,只有这时,你才能得到错误信息。

所以,唯一符合逻辑性的结论是,如果你想上报错误信息,那么就应该使用异常。它比结果对象性能高,比异常返回值安全,而且运行稳定。

扫码关注云+社区

领取腾讯云代金券