您的位置:首页 > 其它

关注性能: 异常的异常

2007-07-16 21:24 225 查看

关注性能: 异常的异常

理解真正的开销





文档选项


将此页作为电子邮件发送

级别: 初级

Jack Shirazi (jack@JavaPerformanceTuning.com), 董事, JavaPerformanceTuning.com
Kirk Pepperdine (kirk@JavaPerformanceTuning.com), 首席技术官, JavaPerformanceTuning.com

2004 年 2 月 13 日

Java 性能狂热者 Jack Shirazi 和 Kirk Pepperdine 分别是 JavaPerformanceTuning.com 的董事和首席技术官,他们从事全球 Internet 上的性能问题讨论,关注开发者遇到的困难。这个月在JavaRanchh 上的访谈,通过深入分析故事背后的故事,他们反驳了关于异常的营火会故事(译者注:即 campfire stories,是 JavaRanchh 推出的一个专栏,专门以童话故事的形式讲授 Java 技术)。
在这个专栏的 第一期,我们讨论了抛出异常的开销。这个月,我们换一个角度再来看这个主题 ―― 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 {
MySpecialClassmyClass = (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
操作会更好,而不会引起强烈的异常开销。

这种情况给了我们最优方法的最终线索:异常应该为异常的情况保留。在异常的情况下使用异常对性能来说是理想的;在无异常的情况下使用检查来避免抛出异常对性能来说也是理想的。








回页首
结束语

从所有这些我们可以看到,遵循最优方法(这里是指异常应该用于异常的情况下)可以产生更好的性能。有的时候我们需要全面考虑类似于这篇文章里所说的情景来决定最优方法到底是什么,以及最优方法在性能上考虑了什么和它的可选方案。但是现在不用担心过早地优化代码,我们以最好的编码实践结束,它有着适当的独立性能,也提供了优化的性能 ―― 在两方面都是最好的。

参考资料

您可以参阅本文在 developerWorks 全球站点上的 英文原文.

访问 Java Performance Tuning.com,获得大量与性能有关的参考资料。

JavaWorld 有大量与该主题相关的文章:

Exceptional practices”(三部分的系列文章)

Beware the dangers of generic Exceptions

Java Tip 134: When catching exceptions, don't cast your net too wide

更多关于 Using Chained Exceptions in JDK 1.4的建议来自 Keld H. Hansen。

The Java Virtual Machine Specification

JavaRanch Big Moose Saloon 的 Topic: instanceof OR ClassCastException线索中有一些有趣的解释。

在企业应用中异常处理目前是一个特殊的挑战。“ Best practices in EJB exception handling” ( developerWorks,2002 年 5月) 和 “ Build a better exception handling framework”( developerWorks,2003 年 4 月) 提供了一些在 J2EE 应用中组织错误处理和错误恢复的建议。

访问 Developer Bookstore 获得技术书籍的完全列表。可以找到许多 与 Java 相关的图书,包括 Jack Shirazi 的 Java Performance Tuning, 2nd Edition (O'Reilly & Associates,2003 年)和 Ant Developer's Handbook (McMillan,2003 年) (与 Kirk Pepperdine 合著)。

developerWorks Java 技术专区 可以找到数百篇关于 Java 编程各个方面的文章。

作者简介



Jack Shirazi 是 JavaPerformanceTuning.com 的董事,也是 Java Performance Tuning (O'Reilly)一书的作者。 除了关注性能调优之外,Jack 也从事人工智能代理技术的开发。



Kirk Pepperdine 是 Java Performance Tuning.com 的首席技术官,最近15年,他主要致力于对象技术和性能调优。Kirk 还是 ANT Developer's Handbook 一书的合著者之一。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: