在这个专栏的 第一期,我们讨论了抛出异常的开销。这个月,我们换一个角度再来看这个主题 ―― JVM 如何处理所抛出的异常 ―― 并且我们要考虑,最理想的异常编码应该看成是早期的优化还是最优方法?
编码的艰难决择:应该这样做还是那样做?
性能讨论组中充斥着类似于这样的问题“我应该像大多数人那样编写代码,还是为了得到更好的性能那样编写代码?”一般专家会建议应该避免早期的优化,并且直到性能测试显示需要优化的时候才使用最优方法,但实际情况是我们每写一行代码都在做出会影响到性能的决定。
JavaRanch 上的一项讨论调查了确保类型安全的两种选择,一种是抛出异常,另一种是用 instanceof ,并提出了“哪种方法更好”的问题。清单 1 和 2 显示了这两种方法。
清单 1. 使用 instanceof 来分支
Listing 1: using instanceof to branch
for (int i = 0; i < max; i++)
{
Object obj = myVector.elementAt(i);
if (obj instanceof MySpecialClass)
{
// do this
}
}
清单 2. 抛出异常来分支
for (int i = 0; i < max; i++) {
try {
MySpecialClass myClass = (MySpecialClass)myVector.elementAt(i);
// do this
} catch (ClassCastException cce) {
continue; // for loop
}
}
提这种问题的危险之一是,当您想把问题浓缩成一个简单例子时可能会失去很多上下文。没有充分的上下文通常会把讨论弄得长而混乱,因为每一个阅读了问题的回答者都会用他们自己的上下文来联系问题。所有这种额外的上下文都会增添含义,这会把我们从问题的出发点转移出来。头脑里有了这些东西之后,就让我们来看能否从这一思路中找到的消息线索中筛选出某些真理来。
异常的特征
提起异常大多数开发者首先要说的就是它们很昂贵。如果您继续追问为什么它们很昂贵,最普遍的答案是我们需要捕获异常堆栈的当前状态。尽管这是开销的很大一部分,但通过列出异常的一些特征,我们可以知道这只是故事的开始。下面是异常的一些特征:
* 可以被抛出。
* 可以被捕获。
* 可以被程序化地创建。
* 可以被 JVM 创建。
* 被表示为第一级对象。
* 继承的深度从 3 开始。
* 由 String (和来自 1.4 的 StackTraceElement s)组成。
* 依靠本机方法 fillInStackTrace() 。
异常与其他对象的主要区别是异常可以被抛出和捕获。让我们从调查当异常被抛出时所触发的事件过程来开始我们的研究。
处理异常的开销
为了抛出异常,JVM 发出 athrow 字节码指令。 athrow 指令引起 JVM 将异常对象弹出执行堆栈。然后 JVM 搜索当前执行堆栈帧来寻找第一个 catch 子句,这个子句可以处理该类的一个异常或者其超类的一个异常。如果在当前的堆栈帧里没有找到 catch block ,那么当前堆栈帧就被释放,异常在下一个堆栈帧的上下文中被重新抛出,如此这般,直到找到包含匹配的 catch 子句的堆栈帧,或者是到了执行堆栈的底部。最后,如果没找到适当的 catch 块,所有的堆栈帧都会被释放,线程在 ThreadGroup 对象有了处理异常的机会后被终止(参考 ThreadGroup.uncaughtException )。如果找到了适当的 catch 块,程序计数器会重置到那一块代码的第一行。
从这个描述中我们可以了解到处理一个抛出的异常是一个非常昂贵的主张。再看一看上面的异常特征清单。注意到除了 JVM 可以“本能地”创建一个异常外,其余剩下的开销与在任何其他第一级对象的生命周期中所引起的开销没有什么区别。
异常作为第一级对象的开销
现在回忆一下 清单 2。只有当强制转换失败的时候异常才被抛出。JVM 是如何处理它的呢?当一个应用需要进行强制类型转换的时候,一个 checkcast 操作就会被发出。这项操作只用于保证堆栈顶部的参数类型是预期的。如果不是预期的,它就会抛出一个 ClassCastException 。
类型检查文雅的做法是使用如 清单 1 所示的 instanceof 操作符。 checkcast 和 instanceof 的区别之一是后者会在堆栈的顶部保留 0 或 1 来表失败或成功。
instanceof 操作符遵循非常严格的一组规则来决定成功或失败。无论变量引用是不是 null 、数组、接口或者类,规则都需要重视。一旦确定了变量的类型,就必须搜索另一边的合格操作数的层次,直到找到匹配的类型或者到达层次的结尾。在数组的情况中,基本元素的类型也也必须经过同样的审查。
除了这项开销外,一旦用了 instanceof ,您就要在后面的代码里将对象进行强制类型转换。执行后继类型转换会导致 checkcast 字节码的执行。所以用于确定类型转换是否工作的逻辑可能会被重复(假设 JVM 没有优化额外的检查)。虽然如此,但因为使用 instanceof 操作符不需要创建新的对象,所以从内存资源和执行资源方面考虑,比起创建和处理异常来说它是廉价得多的操作。那么最初问题的答案就很明显了,对吗?
隐藏的风险
捕获 ClassCastException 同样存在隐藏的风险,也就是说我们可能会捕获从取代了“这样做”注释的代码中抛出的 ClassCastException 。如果该代码在一些操作的中间抛出 ClassCastException ,那么我们可以捕获到它,如果它来自对 MySpecialClass 的强制类型转换,并被忽略,就可能使应用程序处于不一致的状态。
动态调优
到目前为止,我们所做的只是评估了所提到的两种编码风格中每一种一次性执行的开销。现在我们需要了解代码将被执行的条件,以便确定应该使用哪种编码风格。
考虑迭代一个集合的情况,以了解实际花费的开销。如果集合包括同质的一组对象,并且将在的每一个对象上执行一个 instanceof 操作,过程会把不必要的开销强加在整个运行时间中。另一方面,如果集合包括异类的一组对象,那么我们使用 instanceof 操作会更好,而不会引起强烈的异常开销。
这种情况给了我们最优方法的最终线索:异常应该为异常的情况保留。在异常的情况下使用异常对性能来说是理想的;在无异常的情况下使用检查来避免抛出异常对性能来说也是理想的。
结束语
从所有这些我们可以看到,遵循最优方法(这里是指异常应该用于异常的情况下)可以产生更好的性能。有的时候我们需要全面考虑类似于这篇文章里所说的情景来决定最优方法到底是什么,以及最优方法在性能上考虑了什么和它的可选方案。但是现在不用担心过早地优化代码,我们以最好的编码实践结束,它有着适当的独立性能,也提供了优化的性能 ―― 在两方面都是最好的。
转自http://www.ibm.com/developerworks/cn/java/j-perf02104/
分享到:
相关推荐
对于每一位C++开发者而言,理解异常处理的内部实现及其带来的性能开销至关重要。本文将深入探讨C++异常机制的实现原理,并分析其对程序性能的影响。 #### 二、C++异常处理机制概述 C++异常处理主要包括以下几个...
在Java编程语言中,了解运算开销是优化...理解这些运算开销有助于编写更加高效、性能优良的Java代码。通过适当的优化策略,如减少不必要的计算、缓存结果、使用适当的数据结构和算法,可以显著提升Java应用程序的性能。
2. **滥用异常**:有些开发者可能倾向于使用异常来控制程序流程,而不是作为真正的异常情况处理,这会使得代码难以理解和维护。 3. **过多的catch块**:如果过度使用catch块,可能导致代码变得冗长且复杂,降低了...
然而,C++的异常处理也存在一些批评,比如它可能导致性能开销,因为编译器必须生成额外的代码来检查异常。此外,不是所有的C++库都支持异常处理,有些库可能选择返回错误码,这就需要开发者在使用时兼顾两种错误处理...
- 性能开销:抛出和捕获异常可能会比常规的控制流转移慢,因此不应滥用异常处理,特别是在性能敏感的代码段中。 - 难以预测:由于异常是非局部的,可能导致代码的执行路径难以预料,增加了调试的复杂性。 在实际...
真正对性能造成显著影响的是异常发生时执行的补充代码,即在catch块中处理异常的逻辑。通常,这包括错误恢复、日志记录、资源清理等活动。如果这些操作复杂或耗时,那么性能损失将更为显著。例如,如果异常导致重试...
在深入探讨C#中异常处理的性能注意事项之前,我们首先需要理解异常处理的基本概念及其在C#中的实现方式。异常处理是一种程序设计技术,用于在程序执行过程中出现非预期情况时,提供一种有序的方式来恢复程序的正常...
1. **性能开销**:频繁的异常抛出和捕获会带来一定的性能损失,尤其是在循环结构中。 2. **过度使用**:如果对每种可能的异常都进行捕获,可能会导致代码过于复杂,反而降低可读性和可维护性。 3. **忽略错误**...
结合`Stopwatch`的使用,不仅可以测量正常执行的代码片段,还可以记录异常情况下的开销,这对于全面理解系统的健康状况至关重要。 ### 总结 C#的`Stopwatch`类为开发者提供了一种简便而高效的方式,用于监控和分析...
- 异常处理可能会引入性能开销,尤其是在异常不常见的情况下。 - 如果过度使用,可能导致代码过于复杂。 - 不适用于错误返回值的简单错误处理场景。 总结,C++的异常处理机制为程序员提供了一种强大的工具来处理...
值得注意的是,异常处理可能导致性能开销,因此应谨慎使用,避免不必要的异常处理,尤其是在性能敏感的代码段中。 当应用程序中出现未处理的异常时,.NET框架会尝试在调用堆栈上找到适当的异常处理程序。如果没有...
如果无法避免,应合理地使用try-catch块,尽量减少不必要的异常处理开销。此外,还可以利用finally块来确保资源的正确释放,或者使用throws关键字将异常向上抛出,由调用者处理。 总之,Java的异常处理是一个平衡...
【Java理论与实践:理解JTS—平衡安全性和性能】这篇文章深入探讨了Java事务服务(JTS)在J2EE环境中的应用,特别是在EJB组件中的事务管理。事务是确保系统数据完整性的重要机制,它提供了异常处理的能力,类似于...
通过记录异常信息,可以更好地理解程序崩溃的原因。 7. **最佳实践**:合理使用SEHHook的最佳实践包括:仅在必要时安装钩子,确保钩子函数的可逆性,避免全局异常处理,以及始终提供适当的清理和资源释放机制。 ...
在C和C++编程中,异常处理是一种处理...通过理解并熟练运用C++的异常处理机制,程序员可以编写出更加健壮和易于维护的代码,提高软件的可靠性。在实际项目中,根据具体需求和性能要求,合理选择错误处理策略至关重要。
虽然异常处理在某些情况下可能会带来性能开销,但在正确使用时,其带来的错误处理能力远胜于损失的效率。 8. **异常传播**: 当一个函数捕获到异常但没有处理时,它会将异常传递给调用它的函数。这个过程一直持续...
Java内存机制是Java虚拟机(JVM)的关键组成部分,它管理着程序运行时的数据存储。在Java中,内存主要分为以下几个区域: ...正确理解和运用Java内存机制以及异常处理机制对于开发健壮、高效的Java应用程序至关重要。
但过度使用异常可能会降低程序性能,因为抛出和捕获异常都有一定的开销。 3. **错误码**:返回一个特定的错误码,如整数或枚举值,可以用来指示异常情况。这种方式需要维护一个错误码与错误信息的对应表,增加了...
3. **轻量级异常对象**:Boost提供了一种轻量级异常对象的概念,它们不包含堆分配的成员,因此在抛出和捕获时的性能开销较小。 4. **异常安全编程**:Boost异常模块还与Boost的资源管理工具(如smart pointers)...
ARM架构的处理器是微处理器领域的重要成员,它广泛应用于嵌入式系统中,其中一个核心特性就是其对...开发者需要深入理解这些机制,以便在编写中断服务程序和异常处理代码时,能够更加高效和准确地控制处理器的行为。