异常的代价
最近在dynaTrace上出现了一场关于异常(Exception)的代价的大讨论。在跟一些客户的接触中,我们经常的发现他们的代码里有大量的异常处理,自己都不知道。在移除了这些异常后,程序的运行速度比以前有大幅度的提高。这让我们产生了一种假想,程序中的异常处理语句是否给性能带来了巨大的开销?由此得出的推理会是,应该避免使用异常处理。由于异常处理是一个非常重要的处理错误情况的概念,完全的避免使用异常处理看起来并不是一种好的办法。总的来说,我们有充足的理由来近距离的观察一下异常的成本代价。
试验
我的试验是一段很简单的代码,随机的抛出异常。这并不是一个具有什么重要意义的性能测量,而且我们也不知道HotSpot编译器在程序运行时会对它做什么操作。不管怎样,这多少会给我们提供一些基本的可观察的信息。
public class ExceptionTest { public long maxLevel = 20; public static void main (String ... args){ ExceptionTest test = new ExceptionTest(); long start = System.currentTimeMillis(); int count = 10000; for (int i= 0; i < count; i++){ try { test.doTest(2, 0); }catch (Exception ex){ // ex.getStackTrace(); } } long diff = System.currentTimeMillis() - start; System.out.println(String.format("Average time for invocation: %1$.5f",((double) diff)/count)); } public void doTest (int i, int level){ if (level < maxLevel){ try { doTest (i, ++level); } catch (Exception ex){ // ex.getStackTrace(); throw new RuntimeException ("UUUPS", ex); } } else { if (i > 1) { throw new RuntimeException("Ups".substring(0, 3)); } } } }
结果
结果非常的有趣。抛出和捕捉异常的成本看起来非常的低。在我们这个测试中,每个异常大概用去0.002毫秒。这差不多可以忽略不计,除非你真的抛出了太多的异常——太多是指10万个或更多。
虽然这些结果向我们展示了异常捕捉本身并不会给程序带来性能问题,但它让我们打开了另外一个问题:那究竟是什么使这些异常产生了巨大的性能压力?所以,很显然,我遗漏了某些东西——一些重要的东西。
对这个问题重新思考后,我认识到我遗漏了异常捕捉中的一个重要的部分。我遗漏的这块是当异常发生后人们会做的事情。大多数情况下——希望如此——你并不只是把异常捕捉到,然后就完了。通常你会试图纠正出现的问题,让程序的功能能继续满足最终用户。所以我的遗漏点是捕捉到异常后采用挽救措施的代码。依赖于你的这段代码究竟做了什么,它对性能的影响会有很大的不同。在一些情况下这意味着你需要重新连接某个服务,而另一些情况下可能意味着要调用缺省的预案,可能是一种操作简化的方案。
这对我们在很多场景中见到的现象是一个很好的解释,而我没有做这样的分析。我预感到,应该还有什么东西被我遗漏了。
堆栈跟踪(Stack Traces)
对这个问题好奇不减,我观察了一下当收集这些堆栈跟踪信息时,情况会发生什么变化。这种情况很常见。你会记录一个异常和它的堆栈跟踪信息,用来找出是什么问题。
因此我修改了代码,让它获取异常的堆栈跟踪信息。情况发生了显著的变化。获取异常的堆栈跟踪信息要比只是简单的捕捉、抛出它们能产生10倍大的性能压力。所以,堆栈跟踪信息一方面能帮助我们理解什么地方出了问题甚至为什么会发生这个异常,但同时,它也带来了性能上的惩罚。
这通常对性能的冲击非常的大,因为我们并不是只面对一条堆栈跟踪信息。大多数情况下异常的抛出——捕捉——会发生在多个层级上。让我们看一下一个简单的Web Service连接服务器的例子。首先连接失败的异常会在Java类库基层产生。然后客户端的失败会被框架捕捉到,然后在应用层面上某些业务逻辑调用失败会抛出异常。现在加起来有会收集到3种堆栈跟踪信息。
大多数情况下你会查看这些日志文件和应用输出信息。记录这些很长的堆栈信息也会带来性能上的冲击。如果你有规律的查看你的日志文件,你通常会研究它们,对问题作出反应——这是你要做的事情,不是吗?;-)
有时我还能看到由于不正确的日志写法导致的更严重的性能问题。开发人员不首先调用log.isxxEnabled ()来检查中某个log级别的log行为是否开放,而是直接调用logging方法。当你这样做时,日志代码总是在执行时返回异常的堆栈跟踪信息。但由于日志级别设置的太低,你可能永远看不到这些信息,你可能根本不知道什么事情发生了。首先检查日志记录级别,这应该被当作一个基本习惯,这会让你避免产生不必要的对象。
结论
由于担心造成潜在的性能问题而不使用异常处理是一个不明智的做法。异常提供了一个标准方式来处理运行时的问题,帮助你写出清晰的代码。可是对于这程序中抛出的异常你需要跟踪。尽管异常可以捕捉到,但它们仍然会产生很大的性能压力。在dynaTrace公司,我们——缺省情况下——会跟踪抛出的异常——在很多情况下客户会对程序中的这些代码产生的影响以及解决它们的方法感到很惊讶。
异常处理是个有用的东西,但你应该避免捕捉过多的堆栈跟踪信息。大多数情况下它们对理解问题的发生没有必要的用处——特别是你捕捉一个已经预期到的异常。这时简单异常信息已经足够让你明白问题的原因。看到一个连接被拒绝信息,我已经知道什么问题了,所以我不需要java.net内部调用堆栈跟踪。