问题:Java中的异常处理实际上是否很慢?
传统观念以及许多谷歌搜索结果表明,不应将特殊逻辑用于Java中的正常程序流程.通常有两个原因,
它确实很慢 - 甚至比常规代码慢一个数量级(给出的原因各不相同),
和
它很混乱,因为人们只希望在特殊代码中处理错误.
这个问题是关于#1.
例如,这个页面将Java异常处理描述为"非常慢",并将缓慢与异常消息字符串的创建联系起来 - "然后将此字符串用于创建抛出的异常对象.这并不快." Java中的有效异常处理这篇文章说"其原因在于异常处理的对象创建方面,从而使异常本身变得缓慢".另一个原因是堆栈跟踪生成减慢了它的速度.
我的测试(使用Java 1.6.0_07,Java HotSpot 10.0,在32位Linux上)表明异常处理并不比常规代码慢.我尝试在循环中运行一个执行一些代码的方法.在方法结束时,我使用布尔值来指示是返回还是抛出.这样实际处理是一样的.我尝试以不同的顺序运行方法并平均我的测试时间,认为它可能是JVM升温.在我的所有测试中,投掷至少与返回一样快,如果不是更快(最多快3.1%).我对我的测试错误的可能性持开放态度,但我没有看到代码示例,测试比较或过去一两年中显示Java中的异常处理的结果慢.
让我沿着这条路走下去的是我需要使用的API,它将异常作为正常控制逻辑的一部分.我想在他们的使用中纠正它们,但现在我可能无法做到.相反,我是否必须赞美他们的前瞻性思维?
在即时编译中的高效Java异常处理文章中,作者建议单独存在异常处理程序,即使没有抛出异常,也足以阻止JIT编译器正确优化代码,从而减慢它的速度.我还没有测试过这个理论.
这取决于如何实施例外.最简单的方法是使用setjmp和longjmp.这意味着CPU的所有寄存器都被写入堆栈(这已经需要一些时间),并且可能需要创建一些其他数据......所有这些都已经发生在try语句中.throw语句需要展开堆栈并恢复所有寄存器的值(以及VM中可能的其他值).所以try和throw同样很慢,而且速度很慢,但是如果没有抛出异常,退出try块在大多数情况下都不会花费任何时间(因为如果方法存在,所有内容都会被放到堆栈中自动清理).
Sun和其他人认识到,这可能不是最理想的,当然VM随着时间的推移变得越来越快.还有另一种方法来实现异常,这使得try本身闪电般快速(实际上没有任何事情发生在一般情况下尝试 - 当VM加载类时,所有需要发生的事情已经完成)并且它使得抛出速度不是很慢.我不知道哪个JVM使用这种新的,更好的技术......
...但是你是用Java编写的,所以稍后你的代码只在一个特定系统上的一个JVM上运行?既然它可能在任何其他平台或任何其他JVM版本(可能是任何其他供应商)上运行,谁说他们也使用快速实现?快速的比复杂的更复杂,并且在所有系统上都不容易实现.你想保持便携?然后不要依赖快速的异常.
它在try块中的作用也有很大的不同.如果你打开一个try块并且从不在这个try块中调用任何方法,那么try块将是超快的,因为JIT可以实际上像一个简单的goto一样处理throw.如果抛出异常,它既不需要保存堆栈状态也不需要展开堆栈(它只需要跳转到catch处理程序).但是,这不是你通常做的.通常你打开一个try块,然后调用一个可能抛出异常的方法,对吧?即使你只是在你的方法中使用try块,这将是什么样的方法,不会调用任何其他方法?它只是计算一个数字吗?那么你需要什么例外呢?有更多优雅的方法来规范程序流程.对于除了简单的数学之外的其他任何事情
请参阅以下测试代码:
public class Test { 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(); } } // This one will regularly throw one public void method3(int i) throws Exception { 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 Exception(); } } public static void main(String[] args) { int i; long l; Test t = new Test(); l = System.currentTimeMillis(); t.reset(); for (i = 1; i < 100000000; 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 < 100000000; 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 < 100000000; i++) { try { t.method3(i); } catch (Exception e) { // Do nothing here, as we will get here } } l = System.currentTimeMillis() - l; System.out.println( "method3 took " + l + " ms, result was " + t.getValue() ); } }
结果:
method1 took 972 ms, result was 2 method2 took 1003 ms, result was 2 method3 took 66716 ms, result was 2
try块的减速太小,无法排除后台进程等混杂因素.但是拦截块杀死了所有东西并使它慢了66倍!
正如我所说,如果你把try/catch和all all放在同一个方法(method3)中,结果就不会那么糟糕,但这是一个我不会依赖的特殊JIT优化.即使使用这种优化,投掷仍然很慢.所以我不知道你在这里要做什么,但肯定有比使用try/catch/throw更好的方法.
仅供参考,我延长了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相同(我的笔记本电脑显然比较慢).
method4与method3相同,只是它创建了一个new Integer(1)
而不是一个throw new Exception()
.
method5类似于method3,除了它创建new Exception()
而不抛出它.
method6与method3类似,不同之处在于它抛出预先创建的异常(实例变量)而不是创建新异常.
在Java中,抛出异常的大部分费用是收集堆栈跟踪所花费的时间,这在创建异常对象时会发生.抛出异常的实际成本虽然很大,但远远低于创建异常的成本.
AlekseyShipilëv进行了非常彻底的分析,他在各种条件组合下对Java异常进行了基准测试:
新创建的异常与预先创建的异常
启用堆栈跟踪与禁用
请求的堆栈跟踪vs从未请求过
处于最高级别,而不是每个级别的rethrown,而不是链接/包裹在每个级别
各种级别的Java调用堆栈深度
没有内联优化与极端内联与默认设置
用户定义的字段读取与未读取
他还将它们与在各种错误频率级别检查错误代码的性能进行了比较.
结论(从他的帖子中逐字引用)是:
真正特殊的例外情况非常出色.如果按照设计使用它们,并且只在常规代码处理的绝大多数非例外情况中传达真正例外情况,那么使用异常就是性能获胜.
异常的性能成本有两个主要组件:实例化异常时的堆栈跟踪构造和异常抛出期间的堆栈展开.
堆栈跟踪构建成本与异常实例化时的堆栈深度成比例.那已经很糟糕了,因为地球上的谁知道这个投掷方法的堆栈深度?即使您关闭堆栈跟踪生成和/或缓存异常,您也只能摆脱这部分性能成本.
堆栈展开成本取决于我们在编译代码中使异常处理程序更接近的程度.仔细构建代码以避免深度异常处理程序查找可能有助于我们更幸运.
如果我们消除这两种影响,异常的性能成本就是本地分支的性能成本.无论它听起来多么美丽,这并不意味着你应该使用Exceptions作为通常的控制流程,因为在这种情况下你可以优化编译器!您应该仅在真正特殊情况下使用它们,其中异常频率会分摊引发实际异常的可能不幸成本.
对于异常,乐观的经验法则似乎是10 ^ -4频率.当然,这取决于异常本身的重量级,异常处理程序中采取的确切操作等.
结果是,当没有抛出异常时,您不需要支付成本,因此当异常情况非常罕见时,异常处理比if
每次使用都要快.完整的帖子非常值得一读.
遗憾的是,我的回答太长了,无法在此发布.因此,请在此总结一下,并向您推荐http://www.fuwjax.com/how-slow-are-java-exceptions/了解详细信息.
这里真正的问题不是"与'永不失败的代码'相比,'故障报告为异常的速度有多慢'?" 因为公认的回应可能让你相信.相反,问题应该是"与其他方式报告的故障相比,故障报告为异常的速度有多慢?" 通常,报告失败的另外两种方法是使用sentinel值或使用结果包装器.
Sentinel值是尝试在成功的情况下返回一个类,而另一个在失败的情况下返回.您可以将其视为返回异常而不是抛出异常.这需要一个带有success对象的共享父类,然后执行"instanceof"检查和一对转换以获取成功或失败信息.
事实证明,在存在类型安全风险的情况下,Sentinel值比异常更快,但只有大约2倍.现在,这可能看起来很多,但是2x只涵盖了实现差异的成本.在实践中,因为我们的方法可能失败的因素比本页其他地方的示例代码中的一些算术运算符更有趣,因此该因子要低得多.
结果另一方面,包装机不会牺牲类型安全性.它们将成功和失败信息包装在一个类中.因此,它们不是"instanceof"而是为成功和失败对象提供"isSuccess()"和getter.但是,结果对象比使用异常大约慢 2 倍.事实证明,每次创建一个新的包装器对象比有时抛出异常要昂贵得多.
最重要的是,异常是指示方法可能失败的语言.没有其他方法可以告诉API,预期哪些方法总是(大部分)工作,哪些方法会报告失败.
例外比哨兵更安全,比结果对象更快,并且比任何一个都更不令人惊讶.我并不是建议使用try/catch替换if/else,但异常是报告失败的正确方法,即使在业务逻辑中也是如此.
也就是说,我想指出,我遇到的两种最常见的影响性能的方法是创建不必要的对象和嵌套循环.如果您可以在创建异常或不创建异常之间进行选择,请不要创建异常.如果您可以选择有时创建异常或始终创建另一个对象,则创建异常.
我扩展了@Mecki和@incarnate给出的答案,没有堆栈填充Java.
使用Java 7+,我们可以使用Throwable(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace)
.但对于Java6,请参阅我对此问题的回答
// 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) { int i; long l; Test t = new Test(); l = System.currentTimeMillis(); t.reset(); for (i = 1; i < 100000000; i++) { try { t.method4(i); } catch (NoStackTraceThrowable e) { // Do nothing here, as we will get 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 < 100000000; i++) { try { t.method5(i); } catch (RuntimeException e) { // Do nothing here, as we will get here } } l = System.currentTimeMillis() - l; System.out.println( "method5 took " + l + " ms, result was " + t.getValue() ); }
在Core i7,8GB RAM上使用Java 1.6.0_45输出:
method1 took 883 ms, result was 2 method2 took 882 ms, result was 2 method3 took 32270 ms, result was 2 // throws Exception method4 took 8114 ms, result was 2 // throws NoStackTraceThrowable method5 took 8086 ms, result was 2 // throws NoStackTraceRuntimeException
因此,与抛出异常的方法相比,返回值的方法仍然更快.恕我直言,我们无法为成功和错误流程使用返回类型设计一个明确的API.抛出异常而没有堆栈跟踪的方法比正常异常快4-5倍.
编辑:NoStackTraceThrowable.java 谢谢@Greg
public class NoStackTraceThrowable extends Throwable { public NoStackTraceThrowable() { super("my special throwable", null, false, false); } }
我认为第一篇文章是指遍历调用堆栈并创建堆栈跟踪作为昂贵部分的行为,虽然第二篇文章没有说明,但我认为这是对象创建中最昂贵的部分.约翰罗斯有一篇文章,他描述了加速异常的不同技术.(预分配和重用异常,没有堆栈跟踪的异常等)
但仍然 - 我认为这应该只是一个必要的邪恶,最后的手段.John这样做的原因是模拟JVM中尚未提供的其他语言的功能.你不应该养成使用控制流异常的习惯.特别是出于性能原因!正如你自己在#2中提到的那样,你冒着以这种方式掩盖代码中的严重错误的风险,并且对新程序员来说维护起来会更困难.
Java中的微型计算机难以理解(我被告知),特别是当你进入JIT领域时,所以我真的怀疑使用异常比现实生活中的"返回"更快.例如,我怀疑你的测试中有2到5个堆栈帧?现在假设您的代码将由JBoss部署的JSF组件调用.现在您可能有几页长的堆栈跟踪.
也许你可以发布你的测试代码?
前段时间我写了一个类来测试使用两种方法将字符串转换为int的相对性能:(1)调用Integer.parseInt()并捕获异常,或(2)将字符串与正则表达式匹配并调用parseInt()只有匹配成功.我以最有效的方式使用正则表达式(即,在循环之前创建Pattern和Matcher对象),并且我没有打印或保存异常中的堆栈跟踪.
对于一万个字符串的列表,如果它们都是有效数字,则parseInt()方法的速度是正则表达式方法的四倍.但如果只有80%的字符串有效,那么正则表达式的速度是parseInt()的两倍.如果20%是有效的,意味着异常被抛出并且80%的时间被捕获,那么正则表达式的速度大约是parseInt()的20倍.
考虑到正则表达式方法两次处理有效字符串,我对结果感到惊讶:一次是匹配,另一次是parseInt().但抛出和捕获异常不仅仅是为了弥补这一点.在现实世界中,这种情况不太可能经常发生,但如果确实如此,你肯定不应该使用异常捕获技术.但是,如果您只是验证用户输入或类似的东西,请务必使用parseInt()方法.
不知道这些主题是否相关,但我曾经想要依靠当前线程的堆栈跟踪实现一个技巧:我想发现方法的名称,它触发了实例化类中的实例化(是的,这个想法很疯狂,我完全放弃了).所以我发现,调用Thread.currentThread().getStackTrace()
是极其缓慢的(由于本机dumpThreads
供内部使用的方法).
因此,Java Throwable
相应地具有本机方法fillInStackTrace
.我认为catch
前面描述的杀手块会以某种方式触发此方法的执行.
但是让我告诉你另一个故事......
在Scala中,一些功能特性是在JVM中使用编译的,它以下列方式ControlThrowable
扩展Throwable
和覆盖它fillInStackTrace
:
override def fillInStackTrace(): Throwable = this
所以我调整了上面的测试(周期数量减少了10,我的机器有点慢:):
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
,但不同的是远更容易接受.
我已经使用JVM 1.5进行了一些性能测试,并且使用异常的速度至少慢了2倍.平均而言:一个简单的小方法的执行时间超过三倍(3x),但有例外.一个非常小的循环必须捕获异常,自我时间增加了2倍.
我在生产代码和微基准测试中看到了类似的数字.
绝对不应该用于经常调用的任何事情.每秒抛出数以千计的异常将导致巨大的瓶颈.
例如,使用"Integer.ParseInt(...)"查找非常大的文本文件中的所有错误值 - 非常糟糕的主意.(我已经看到这个实用程序方法在生产代码上杀死性能)
使用异常来报告用户GUI表单上的错误值,从性能角度来看可能并不那么糟糕.
无论它是否是一个好的设计实践,我都会遵守规则:如果错误是正常的/预期的,那么使用返回值.如果不正常,请使用例外.例如:读取用户输入,错误值是正常的 - 使用错误代码.将值传递给内部实用程序函数时,应通过调用代码来过滤错误值 - 使用异常.