问题:Java中的异常处理是否真的很慢?
传统观点以及许多Google的研究结果都表明,不应将异常逻辑用于Java中的常规程序流程。通常有两个原因,
它确实很慢-甚至比常规代码慢一个数量级(给出的原因各不相同), 和
这是混乱的,因为人们期望仅在异常代码中处理错误。 这个问题是关于#1的。
例如,此页面将Java异常处理描述为“非常慢”,并将这种缓慢与异常消息字符串的创建相关联-“然后,此字符串用于创建抛出的异常对象。这不是很快。” Java中的有效异常处理文章说:“其原因是由于异常处理的对象创建方面,因此使固有的异常抛出速度变慢”。另一个原因是堆栈跟踪的生成会降低堆栈跟踪的速度。
我的测试(在32位Linux上使用Java 1.6.0_07,Java HotSpot 10.0)表明异常处理并不比常规代码慢。我尝试在执行一些代码的循环中运行方法。在方法的最后,我使用一个布尔值来指示是返回还是throw。这样,实际处理是相同的。我尝试以不同的顺序运行这些方法并平均测试时间,以为可能是JVM变暖了。在我所有的测试中,投掷速度至少与返回速度一样快,甚至不快(高达3.1%)。我完全可以接受我的测试错误的可能性,但是我并没有看到代码样本,测试比较或最近一两年中显示Java异常处理的结果的任何方式慢。
导致我走这条路的是我需要使用的将异常作为常规控制逻辑的一部分的API。我想更正它们的用法,但现在可能无法。我是否必须赞扬他们的前瞻性思维?
取决于异常的实现方式。最简单的方法是使用setjmp和longjmp。这意味着将CPU的所有寄存器都写入堆栈(这已经花费了一些时间),并且可能需要创建一些其他数据…所有这些都已经在try语句中发生。throw语句需要展开堆栈并恢复所有寄存器的值(以及VM中可能的其他值)。因此try和throw都同样缓慢,而且也相当慢,但是,如果没有抛出异常,则在大多数情况下退出try块几乎不会花费任何时间(因为所有内容都放在堆栈中,如果该方法存在,则堆栈会自动清除)。
Sun和其他公司认识到,这可能不是最佳选择,并且随着时间的流逝,虚拟机当然会越来越快。还有另一种实现异常的方法,它可以使尝试本身快如闪电(实际上一般而言,尝试都不会发生任何事情-当类由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放在同一方法(method3)中并全部抛出,结果将不会那么糟,但这是我不依赖的特殊JIT优化。即使使用此优化,抛出也仍然很慢。所以我不知道你要在这里做什么,但是绝对有比使用try / catch / throw更好的方法。
仅供参考,我扩展了梅基所做的实验:
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相同,只不过它创建了new Integer(1)而不是do throw new Exception()。
new Integer(1)
do throw new Exception()
method5类似于method3,不同之处在于它创建了new Exception()而不抛出它。
new Exception()
method6与method3相似,除了它抛出一个预先创建的异常(实例变量)而不是创建一个新的异常。
在Java中,引发异常的大部分开销是花费在收集堆栈跟踪上的时间,该时间在创建异常对象时发生。引发异常的实际成本虽然很大,但比创建异常的成本要少得多。