首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》通过类加载自动测量

关灯直达底部

我们在第1章和第5章讨论过如何把类编译成可执行程序。其中一个关键步骤是在加载字节码时进行转换。这个特性非常强大,是很多现代Java平台的核心技术。其中一个简单的例子就是方法的自动测量。

在这种方法中,特殊的类加载器加载methodToBeMeasured所属类,在方法开始和结束的地方加上记录方法进入和退出时间的字节码。这些时间通常会被写入共享的数据结构,由其他线程访问。这些线程一般会将数据写入日志文件,或者通过网络交给负责处理原始数据的服务器。

很多高端的性能监测工具(比如OpTier CoreFirst)都是以这项技术为核心的。但在编写本书时,这个市场上似乎还没有开源工具。

注意 我们会在后面讨论到,Java 方法开始时需要进行解释,然后才切换到编译模式。要得到真正的性能指标结果,你必须去掉解释模式占用的时间,因为它们会严重扭曲真实结果。后面还会给出更多细节,告诉你如何确定方法切换为编译模式的时间。

你可以用这两项技术(其一或全部)找出某一方法执行所需的时长。下一个问题,完成调优之后,你想得到什么样的数值?

6.2.3 知道性能目标是什么

清晰的目标能让人注意力集中,所以了解和传达优化的最终目标(知道要测量什么)至关重要。大多数情况下,这个目标简单而明确,比如:

  • 将10个并发用户的端到端等待时间的第90个百分位数减少20%;
  • handleRequest的平均等待时间减少40%,方差减少25%。

在一些更复杂的情况中,目标可能由几个相关的性能目标共同构成。你要知道,你所测量和想要优化的独立可观测项越多,调优工作就会变得越复杂。优化一个性能目标可能会对其他性能目标产生负面影响。

有时,在设定目标之前你很有必要做些初步分析,比如在确定要让方法运行得更快这一目标之前,应该先确定哪些方法最重要。这很好,但经过初步探索后,你最好停下来再确认一下目标,然后再达成它们。开发人员非常爱犯只顾低头拉车,不顾抬头看路的错误。

6.2.4 知道什么时候停止优化

理论上来说,知道什么时候停止优化并不难——达成目标之时就是任务完成之日。然而实际中人们很容易陷入性能调优的泥淖。如果事情进展顺利,你肯定想要继续前进并做得更好。而如果不太顺利,你为了达成目标就会不断尝试新策略。

要想知道什么时候停止优化,你需要对目标有清醒的认识并理解它们的价值。能达成性能目标的90%通常就足够了,你还可以利用节省下来的时间去做些别的事。

还要考虑一点,你要看看有多少工作投入到了极少用到的代码路径上。通过优化代码来减少程序运行时长的1%(甚至更少)完全是在浪费时间,但奇怪的是做这种事儿的开发人员数量惊人。

至于该优化什么,这里有一组非常简单的指导规则。你可能需要根据自身情况进行调整,但它们的适用范围很广泛:

  • 优化那些重要,而不是最容易的代码。
  • 首先优化那些最重要(通常是调用最频繁)的方法。
  • 在遇到那些唾手可得的优化时,把它办了,但要清楚代码的调用频率。

最后再做一轮测量工作。如果还没达成性能目标,你就需要清查一下,看看离命中目标还有多大差距,以及取得的成绩是不是已经对整体性能产生了你所期望的影响。

6.2.5 知道高性能的成本

所有性能调整都贴着价签。

  • 分析和优化代码要占用的时间(在任何软件项目中,开发人员的时间基本都是最大的开支)。

  • 所做的调整可能会引入额外的技术复杂度(也有简化代码的性能优化,但它们不是主流)。

  • 为了让主处理线程运行得更快,可能会引入额外的线程来执行辅助任务,但这些线程可能会在负载较高时对系统整体产生不可预料的影响。

不管是什么价签,你都要重视,并尽量在完成第一轮优化之前找到它们。

这有助于你了解提高性能的最大可接受成本。这个成本可能是设定开发人员调优的时间限制,额外的类数或代码行数。比如说,开发人员决定花在优化上的时间不能超过一个星期,或者因优化而生的类增长不应该超过100%(即大小变成原来的两倍)。

6.2.6 知道过早优化的危险

关于优化,Donald Knuth有段著名的评论:

程序员浪费了大量时间考虑,或担心程序中无关紧要部分的速度,并且那些尝试改进效率的行为实际上有很强的负面影响……过早优化是万恶之源。 1

1 Donald E. Knuth, “带go to语句的结构化编程”, 计算调查,6,no.4(1974年12月)。 http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf。

这段话在业内引起了广泛争论,而且人们通常只记住了最后一句。这之所以令人感到遗憾,有如下原因。

  • 在评论的前段,Knuth含蓄地提醒我们要测量,没有测量就不能确定程序的关键部分。

  • 我们再次提醒你,可能不是代码导致等待时间过长——环境中的其他部分也会产生等待时间。

  • 在完整的评论中,很容易看出Knuth是在谈论那些有意识的、齐心协力的优化。

  • 这段评论的简短版让它变成了不良设计或糟糕执行选择的相当巧合的借口。

有些优化体现在良好的编码风格上:

  • 不要分配不需要的对象。
  • 如果再也不需要调试日志,就去掉它。

我们在下面的代码中加了一个检查,看日志对象是否处理调试日志。这种检查被称为日志守卫。如果日志子系统被设置为不处理调试日志,这段代码就不会构造日志消息,省掉了为了日志消息而调用currentTimeMillis和构造StringBuilder对象的开销。

if (log.isDebugEnabled) log.debug(/"Useless log at: /"+System.currentTimeMillis);  

但如果调试日志真的没有用,我们可以把这段代码一并去掉,就能再节省两个处理器周期(日志守卫的开销)。

性能调优的工作之一就是从一开始就写出质地优良、高效运行的代码。更好地认识Java平台,知道它的底层运行机制(比如理解在合并两个字符串时隐含的对象分配),并在编码时考虑到性能问题,才能写出更好的代码。

现在我们有了框定性能问题和目标的基本词汇,还有如何解决问题的方法大纲。但我们还没解释为什么这是软件工程师会遇到的问题,以及这种需求来自哪里。要弄懂这个,我们有必要简单了解一下硬件的世界。