凡事预则立,不预则废,和许多事情一样,Java性能调优的成功,离不开行动计划、方法或策略以及特定的领域背景知识。为了在Java性能调优工作中有所成就,你得超越“花似雾中看”的状态,进入“悠然见南山”或者已然是“一览众山小”的境界。
这三个境界的说法可能让你有些糊涂吧,下面进一步解释。
- 花似雾中看(I don‘t know what I don‘t know)。有时候下达的任务会涉及你所不熟悉的问题域。理解陌生问题域首先面临的困难就是如何竭尽所能地学会它,因为你对它几乎一无所知。对于这类问题域,你有许多东西不了解,或者不知道重点。换句话说,这个问题域有哪些东西需要了解,你还傻傻看不清楚。这个阶段就是“花似雾中看”。
- 悠然见南山(I know what I don‘t know)。刚进入不熟悉的问题域时,你对它知之甚少,随着时间的推移,你对它的许多重要方面都已有所认识,只是对重要的具体细节还缺乏了解。这时,你可以算是刚刚“见南山”。
- 一览众山小(I already know what I need to know)。还有些时候,你对任务的问题域非常熟悉,或者已经具有该领域所必备的技能和知识,是这方面的专家。或者你对问题域足够了解,处理起来得心应手,比如你已经掌握了必要的知识,解决问题游刃有余。如果达到这个境界,那就意味着你已经是“一览众山小”了。
通常认为,传统的软件开发过程主要包括4个阶段:分析、设计、编码和测试,如图1-1所示。
图1-1 传统软件开发过程
替换图字: start:开始; analysis:分析; design:设计; code:编码; test:测试;
quality:质量合格?; yes:是; no:否; deploy:部署
分析是开发过程的第一步,用于评估需求、权衡各种架构的利弊以及构思高层抽象。设计则依据分析阶段的基本架构和高层抽象,进行更精细的抽象并着手考虑具体实现。编码自然就是设计的实现。编码之后是测试,用以验证实现是否合乎应用需求。值得注意的是,测试阶段通常只包括功能测试,即检验应用的执行是否合乎需求规格。一旦测试完成,应用就可以发布给客户了。
遵循这种传统软件开发过程的应用,通常要到测试或即将发布时才会关注性能或扩展性。为了解决这个问题,Wilson和Kesselman对传统软件开发过程做了些补充,在传统开发模型基础上引入了性能测试分析阶段,参见他们的畅销书Java Platform Performance。他们建议在测试阶段之后增加性能测试,并将“性能测试是否通过”设定为产品是否发布的标准。如果达到性能和扩展性标准,应用就可以发布,否则就要转向性能分析,并依据分析结果回到之前的某个或者某些步骤。换句话说,通过性能分析来定位性能问题。Wilson和Kesselman添加的性能测试分析如图1-2所示。
图1-2 Wilson和Kesselman添加性能测试分析之后的软件开发过程
替换图字: start:开始; analysis:分析; design:设计; code:编码; performance test:性能测试; performance acceptable:性能测试是否通过; profile:性能分析; yes:是; no:否; deploy:部署
对分析阶段提炼出来的性能需求,Wilson和Kesselman建议以用例(use case)的方式特别标识出来,这有助于在分析阶段制定性能评估指标。不过应用的需求文档中通常都不会明确描述性能或扩展性需求。如果你正在开发的应用还没有明确定义这些需求,那就应该想办法将它们挖掘出来。拿吞吐量和延迟性需求举例,以下清单列举了挖掘这些需求所要考虑的问题。
- 应用预期的吞吐量是多少?
- 请求和响应之间的延迟预期是多少?
- 应用支持多少并发用户或者并发任务?
- 当并发用户数或并发任务数达到最大时,可接受的吞吐量和延迟是多少?
- 最差情况下的延迟是多少?
- 要使垃圾收集引入的延迟在可容忍范围之内,垃圾收集的频率应该是多少?
需求和对应的用例文档应该回答上述问题,并以此制定基准测试和性能测试,确保应用能够满足性能和扩展性需求。基准测试和性能测试应该在性能测试阶段执行。评估用例时有些用例的风险过高,难以实现,应该在分析阶段后期,通过一些原型、基准测试和微基准测试来降低此类风险。分析结束后再变更决策的代价非常高,这个方法可以让你事先对决策进行评估。软件开发周期中的软件缺陷、低劣设计和糟糕实现发现得越晚,修复的代价就越大,这是一条颠扑不破的金科玉律。降低用例的高风险有助于避免这些代价昂贵的错误。
现在许多应用在开发过程中都会使用自动构建和测试。Wilson和Kesselman建议改进软件开发过程,在自动构建或测试中进一步添加自动性能测试。自动性能测试可以发出通知,比如用电子邮件将性能测试结果(如性能是衰减还是改善,或性能指标的达成度)发送给干系人。这个过程可以将因不满足应用性能指标而失败的测试,以及测试的统计数据自动记录到追踪系统。
将性能测试集成到自动构建过程中后,每次代码变更提交到源代码库时,都能很容易地追踪因变更而导致的性能变化,也就能在软件开发的早期发现性能衰减。
另外,将统计方法和自动统计分析添加到自动性能测试系统中也值得考虑。运用统计方法可以进一步验证性能测试的结果。
自顶向下和自底向上是两种常用的性能分析方法。顾名思义,自顶向下(Top Down)着眼于应用顶层,从上往下寻找软件栈中的优化机会和问题。相反,自底向上(Bottom Up)则从软件栈最底层的CPU统计数据(例如CPU缓存未命中率、CPU指令效率)开始,逐渐上升到应用自身的结构或该应用常见的使用方式。应用开发人员常常使用自顶向下的方法,而性能问题专家则通常采用自底向上的方法,用以辨别因不同硬件架构、操作系统或不同的Java虚拟机实现所导致的性能差异。如你所想,不同方法可以用来查找不同类型的性能问题。自顶向下大概是最常用的性能调优方法。如果需要更改应用软件栈的顶层代码进行调优,这也是最常用的方法。
使用自顶向下的方法时,通常你需要从干系人发现性能问题的负载开始监控应用。应用的配置变化或日常负荷变化可能导致性能降低,这种情况下,需要持续地监控应用。此外,当应用的性能和扩展性需求发生变化时,应用可能无法满足新的要求,这时也需要监控应用程序的性能。
不管何种原因引起的性能调优,自顶向下的第一步总是对运行在特定负载之下的应用进行监控。监控的范围包括操作系统、Java虚拟机、Java EE容器以及应用的性能测量统计指标。基于监控信息所给出的提示再开展下一步工作,例如JVM垃圾收集器调优、JVM命令行参数调优、操作系统调优,或者应用程序性能分析。性能分析可能导致应用程序的更改,或者发现第三方库或Java SE类库在实现上的不足。
在不同平台(指底层的CPU架构和数量不同)上进行应用性能调优时,性能专家常使用自底向上的方法。将应用迁移到其他操作系统上时,也常用这种方法改善性能。在无法更改应用源代码时,例如应用已经部署在生产环境中,或者系统供应商为了在竞争中占得先机而必须将性能发挥到极致,也常常会使用这种方法。
自底向上需要收集和监控最底层CPU的性能统计数据。监控的CPU统计数据包括执行特定任务所需要的CPU指令数(通常称为路径长度,path length),以及应用在一定负载下运行时的CPU缓存未命中率。虽然还有其他重要的CPU统计数据,但这两项是自底向上中最常用的。在一定负载下,应用执行和扩展所需的CPU指令越少,运行得就越快。降低CPU缓存未命中率也能改善应用的性能,因为CPU缓存失效会导致CPU为了等待从内存获取数据而浪费若干个周期,而降低CPU缓存未命中率,意味着CPU可以减少等待内存数据的时间,应用也就能运行得更快。
自底向上关注的通常是在不更改应用的前提下,改善CPU使用率。假如应用可以更改,自底向上也能为如何修改应用提供建议。这些更改包括应用源代码的变动,如将经常使用的数据移到一起,使得访问同一条CPU缓存行(CPU cache line)就能获取这些数据,而不用等待从内存中获取数据。这个改动可以降低CPU缓存未命中率,从而减少CPU等待内存数据的时间。
现代Java虚拟机集成了成熟的JIT编译器,可以在Java应用的执行过程中进行优化,比如依据应用的内存访问模式或应用特定的代码路径,生成更有效的机器码。也可以调整操作系统的设置来改善性能,例如更改CPU调度算法,或者修改操作系统的等待时间(指操作系统在将应用执行线程迁移到其他CPU硬件线程之前所等待的时间)。
如果你觉得可以用自底向上的方法,那应该先从收集操作系统和JVM的统计数据开始。监控这些统计数据可以为下一步应该关注哪些重点提供线索。
本文内容摘自《Java性能优化权威指南》