Java性能权威指南(txt+pdf+epub+mobi电子书下载)


发布时间:2020-07-07 18:21:01

点击下载

作者:(美) 奥克斯(S. Oaks)

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Java性能权威指南

Java性能权威指南试读:

前言

起初 O'Reilly 公司让我写一本关于 Java 性能调优的书时,我还不确定是否值得写。我在想,难道 Java 性能调优我们做得还不够吗?事实上,虽然我日常的基本工作是 Java(和其他)应用程序的性能调优,但我宁愿将大多数时间都花在提高应用程序的算法效率以及处理外部系统的性能瓶颈上,而不是直接进行 Java 自身性能的调优。

但转念一想,我不禁哑然失笑(像往常一样)。的确,我的大量时间都花在了端到端的系统性能调优上,有时会发现那些原本可以用却用了算法的代码。不过这也说明,我每天考虑的都是 GC 调优、JVM 编译器的性能调优,或者是如何使 Java EE API 的性能发挥到极致。

说这些并不是想要抹杀过去15年里 Java 和 JVM 在性能上取得的巨大进步。1990年代晚期,我在 Sun 公司担当 Java 布道师,当时仅有的真正意义上的“基准测试”工具来自 Pendragon 软件的 CaffeineMark 2.0。由于种种原因,该基准测试工具设计上的不足很快就限制了它的价值;然而在那个年代,我们总喜欢告诉所有人,依据这个基准测试,Java 1.1.8的性能比 Java 1.0快八倍。这并非耸人听闻——Java 1.1.8已经有了真正的即时编译器,而 Java 1.0差不多完全是解释型的。

之后,Java 标准委员会开始制定更严谨的基准测试,Java 的性能测试开始围绕这些基准测试展开。最终,JVM 的所有领域——垃圾收集、编译器和 API 都获得了长足的进步。这个过程一直延续到今天,而关于性能的一个重要事实是,调优正变得越来越艰难。引入即时编译器后所获得的八倍性能提升,只是一个简单的工程问题,即使编译器持续改进,我们也无法再次看到如此巨大的改进了。并行化垃圾收集也曾极大地提升过性能,但最近的变化则是渐进式的。

这是典型的应用发展过程(JVM 本身也是另外一个应用):在项目初期,很容易找到架构上的改进点(或代码缺陷),一旦找到就能极大改善性能。而在成熟应用中,要找到这样的性能改进点则很罕见。

起初我觉得,从很大程度上说,Java 性能调优都已经工程化了,但有几件事情让我相信我错了。首先,我每天在特定环境下运行 JVM 时,都会遇到许多这样或那样的问题。新工程师源源不断地进入 Java 领域。在特定的领域,JVM 的行为仍然相当复杂,因此有份描述它如何运作的指南很有必要。其次,现在的计算环境发生了变化,已经影响到了工程师们所面临的性能问题。

在过去几年中,性能关注点有了分歧。一方面,有大量内存堆可运行 JVM 的大机器现在已经很普遍了。为了应对这样的变化,JVM 也有了新的垃圾收集器(G1)——这项新技术相比传统的收集器更需要手工调优。同时,云计算又提升了单 CPU 的小机器的重要性:你可以从 Oracle、Amazon 或其他公司以非常便宜的价格租用单 CPU 机器,运行小的应用服务器。(你获得的并不是真的单 CPU 机器,而是一台巨大机器上的一个虚拟 OS 镜像,但虚拟 OS 被限制为使用单个 CPU。从 Java 角度看,它和单 CPU 的机器相同。)在这些环境中,正确管理小量内存变得非常重要。

Java 平台也在持续演变。Java 的每个新版本都会提供新的语言特性和新的 API,这些特性和 API 并不总是为了提高应用性能,也是为了改善开发人员的生产率。语言特性运用得好,应用的运行就会变得轻快,反之则缓慢笨重。另外,平台的演化也带来了一些重要的性能课题:毫无疑问,程序间用 JSON 交换信息要比用高度优化的私有协议更容易。节约开发人员的时间就是巨大的收益——但真正的目的是确保生产率提升的同时,性能也能提升(至少是两者之间取得平衡)。读者对象

本书适合那些渴望深入了解 JVM 和 Java API 性能各个方面的性能调优工程师和开发者。

假如你想快速修复性能问题,比如网站周一早上要上线,而现在已经是周日深夜了,那么本书可能不适合你。

如果你是性能分析的新手,正要开始进行 Java 的性能分析,那么本书会对你有所帮助。我的目的主要是为新工程师提供足够多的信息和上下文,以便使他们明白如何将基本的性能调优原则运用到 Java 应用中去。然而,系统分析的范畴非常广阔,已经有大量的优秀资源(那些原则当然也适用于 Java),从这个意义上来说,希望本书也能成为它们的有益补充。

不过从根本上说,想让 Java 运行得飞快,就得深入理解 JVM(以及 Java API)的实际工作原理。有数以百计的 Java 性能调优参数,而 JVM 调优并不像瞎猫碰死耗子那样,调一下再看看是否奏效。与之相反,我的目的是提供更为详尽的 JVM 和 API 工作原理,以期在你理解它们如何工作的原理之后,能理解应用的某些行为为何糟糕。理解了这些之后,排除那些性能低下、令人不快的行为就会变成简单(至少是比以前更简单)的任务。

Java 性能调优工作还有一个有趣的方面,就是开发人员的背景和性能调优或 QA 组人员的背景常常有很大差别。我认识一些开发人员,他们可以记住成千上万个令人费解的很少使用的 Java API 方法签名,但他们对 -Xmn 的含义却没有什么概念。我也认识一些测试工程师,他们可以通过设置垃圾收集器的各种标志来榨取最后一滴性能,但他们却很少有人能用 Java 写出像样的“Hello, World”。

Java 性能调优覆盖这两个领域:编译器和垃圾收集器等的调优参数,以及 API 的最佳实践。所以,我假定你对如何编写 Java 程序有很好的理解。即便你主要的兴趣不是在 Java 编程,我仍然会花一点时间讨论编程,包括例子中包含大量数据的示例程序。

然而,如果你的主要兴趣是 JVM 自身的性能调优——意思是不用更改任何代码而改变 JVM 的行为,那么本书的大量章节都对你有用。可以随意跳过代码部分,而关注你所感兴趣的领域。也许你会顺便为 Java 应用如何影响 JVM 性能提出一些真知灼见,并向开发人员提出更改建议,以便让你的性能调优测试工作更加如鱼得水。排版约定本书使用的排版约定如下。● 楷体

表示新术语。● 等宽字体(constant width)

表示程序片段,以及正文中出现的变量、函数名、数据库、数据

类型、环境变量、语句和关键字等。● 等宽粗体(constant width bold)

表示应该由用户输入的命令或其他文本。● 等宽斜体(constant width italic)

表示应该由用户输入的值或根据上下文确定的值替换的文本。 这个图标代表提示或建议。 这个图标代表重要说明。 这个图标代表警告或提醒。使用代码示例

可以在这里下载本书随附的资料(代码示例、练习题等):https://github.com/ScottOaks/JavaPerformanceTuning。

让本书助你一臂之力。也许你需要在自己的程序或文档中用到本书中的代码。除非大段大段地使用,否则不必与我们联系取得授权。例如,无需请求许可,就可以用本书中的几段代码写成一个程序。但是销售或者发布 O'Reilly 图书中代码的光盘则必须事先获得授权。引用书中的代码来回答问题也无需授权。将大段的示例代码整合到你自己的产品文档中则必须经过许可。

使用我们的代码时,希望你能标明它的出处,但不强求。出处一般包括书名、作者、出版商和 ISBN,例如:Java Performance: The Definitive Guide by Scott Oaks (O'Reilly, 2014). Copyright by Scott Oaks, 978-1-449-35845-7。

如果还有关于使用代码的未尽事宜,可以随时与我们联系:permissions@oreilly.com。®Safari Books Online

Safari Books Online(http://www.safaribooksonline.com)是应需而变的数字图书馆。它同时以图书和视频的形式出版世界顶级技术和商务作家的专业作品。

Safari Books Online 是技术专家、软件开发人员、Web 设计师、商务人士和创意人士开展调研、解决问题、学习和认证培训的第一手资料。

对于组织团体、政府机构和个人,Safari Books Online 提供各种产品组合和灵活的定价策略。用户可通过一个功能完备的数据库检索系统访问 O'Reilly Media、Prentice Hall Professional、Addison-Wesley Professional、Microsoft Press、Sams、Que、Peachpit Press、Focal Press、Cisco Press、John Wiley & Sons、Syngress、Morgan Kaufmann、IBM Redbooks、Packt、Adobe Press、FT Press、Apress、Manning、New Riders、McGraw-Hill、Jones & Bartlett、Course Technology 以及其他几十家出版社的上千种图书、培训视频和正式出版之前的书稿。要了解 Safari Books Online 的更多信息,我们网上见。联系我们

请把对本书的评价和问题发给出版社。

美国:

  O'Reilly Media, Inc.

  1005 Gravenstein Highway North

  Sebastopol, CA 95472

中国:

  北京市西城区西直门南大街2号成铭大厦 C 座807室(100035)

  奥莱利技术咨询(北京)有限公司

O'Reilly 的每一本书都有专属网页,你可以在那儿找到本书的相关信息,包括勘误表、示例代码以及其他信息。本书的网站地址是:

http://oreil.ly/java-performance-tdg。

对于本书的评论和技术性问题,请发送电子邮件到:bookquestions@oreilly.com

要了解更多 O'Reilly 图书、培训课程、会议和新闻的信息,请访问以下网站:http://www.oreilly.com

我们在 Facebook 的地址如下:http://facebook.com/oreilly

请关注我们的 Twitter 动态:http://twitter.com/oreillymedia

我们的 YouTube 视频地址如下:http://www.youtube.com/oreillymedia致谢

感谢在我写这本书时所有帮助过我的人。从各方面来看,这本书汇集了我过去15年来在 Sun 公司和 Oracle 公司的 Java 性能调优小组中的所学所知。如果将为这本书提出真知灼见的人都列出来的话,会是一份长长的列表。感谢在那段时间与我一同工作的工程师们,特别是在那些年里耐心回答我铺天盖地的问题的人们。

我要特别感谢 Stanley Guan、Azeem Jiva、Kim LiChong、Deep Singh、Martijn Verburg 和 Edward Yue Shung Wong,感谢他们拨冗审阅本书的初稿,感谢他们提供的宝贵意见。虽然本书因为他们的建议而完善了许多,但我相信错误仍然在所难免。

O'Reilly 的工作人员总是那么乐于助人,感谢我的编辑 Meg Blanchette,感谢你在整个写作过程中的鼓励。最后,衷心感谢我的丈夫 James 给予我在为书而抓狂的漫漫长夜里的悉心陪伴,以及周末晚餐。第1章导论

这是一本关于 Java 性能调优科学和艺术的书。

说性能调优是门科学,这并不令人意外;性能调优涉及大量数字、检测和分析工作。性能调优工程师大多具有科学背景,只有基于严谨的科学理论才能将性能发挥到极致。

那它的艺术性呢?其实性能调优是科学与艺术的结合体这一说法并不新鲜,但我们探讨性能时却很少能清楚地意识到这一点。从某种程度上说,这可能是因为我们所受的教育训练并不容易产生“艺术”的思想火花。

说它是艺术的部分原因是,对一些人来说,艺术从根本上就是建立在知识和经验的基础上的。据说,足够先进的技术与魔术无异,例如对于圆桌骑士而言,使用手机毫无疑问就是一种魔法。与此类似,优秀性能调优工程师的工作就像是艺术,而这艺术正是源于深厚的知识、丰富的经验和敏锐的直觉。

本书的侧重点不在于三者中的经验和直觉,而是在拓展知识的深度。日积月累,这些知识将有助于提升你的技能,有助于你成为一名优秀的 Java 性能调优工程师。本书还有助于你深入理解 Java 平台性能的各个方面。

本书涉及的知识主要分两大类。首先是如何对 Java 虚拟机(Java Virtual Machine,JVM)自身的性能进行调优,即如何通过 JVM 的配置来影响程序的各种性能指标。JVM 性能调优的过程实际上与 C++ 程序员在编译时通过测试选择编译参数,以及 PHP 码农在 php.ini 文件中选择适当变量等过程非常类似,但对于那些即便有其他语言经验的 Java 开发者来说,调优过程仍然不那么令人愉快。

其次是理解 Java 平台的特性对性能的影响。注意,此处的平台既指 Java 语言(例如线程和同步),也指 Java 标准 API(例如 XML 解析性能)。虽然 Java 语言和 Java API 完全不是一回事,但本书并不作严格区分,这两方面的内容都会涵盖。

JVM 自身的性能很大程度上取决于调优标志,而 Java 平台的性能则更多由在应用代码中采用最佳实践决定。在一个开发团队中,开发人员编写代码,性能组负责性能测试。编码和调优常常被认为是两个不同的专业领域:性能调优工程师只是竭力将 JVM 的性能发挥到极致,而开发人员只关心他们的代码逻辑是否正确。这种区分没有什么意义。任何从事 Java 相关工作的人都应该熟谙代码在 JVM 中的行为,以及如何调优才能提升性能。对专业知识的全面掌握能让你的工作更具艺术气息。1.1 概述

首先是通览:第2章讲述了测试 Java 应用的通用方法,还包括 Java 基准测试中的陷阱。通过可视化的性能分析,我们可以了解应用正在做什么,所以第3章概要介绍了监控 Java 应用的一些工具。

接下来就是深入性能调优了,首先关注的是常见的调优主题:JIT 编译(第4章)和垃圾收集(第5章和第6章)。剩余各章关注的是 Java 平台各方面的最佳实践:Java 堆内存的使用(第7章),本地内存的使用(第8章),线程性能的调优(第9章),Java 企业版 API(第10章),JPA 和 JDBC(第11章),以及一些通用的 Java SE API 技巧(第12章)。附录 A 列出了本书中所有的调优标志,并给出了解释这些标志的交叉引用章节。1.2 平台版本约定

本书基于 Oracle HotSpot JVM 和 Java Standard Edition(Java SE)7和8。在发布版之间,Oracle 更新会定期发布更新版本。多数时候,更新版本只修订 bug,不会加入语言新特性或者变更关键功能,但会更改调优标志的默认值。Oracle 很可能将在本书出版之后提供更新版本,当前版本为 Java 7 update 40和 Java 8(迄今为止,还没1有 Java 8的更新版本)。如果更新版本对 JVM 的行为做了重大修订,则会命名如下:7u6(Java 7 update 6)。1现在为 Java 7 update 55,Java 8 update 5。——译者注

本书关于 Java 企业版(Java EE)的内容是基于 Java EE 7。

虽然当前版本的 Java 构建于之前的发布版,但本书并不涉及老版本 Java 的性能调优。对这本讲述性能调优的书而言,Java 7是一个很好的起始点,因为 Java 7引入了大量的性能新特性和优化。其中最主要的是称为 G1的垃圾收集(GC)算法(老版本的 Java 包含了 G1的试验版,但直至 Java 7u4,G1才真正可用于生产环境)。Java 7也包括许多与性能相关的新特性和增强,可以让我们更清楚地了解 Java 应用的运转。Java 8再接再厉,平台得到了进一步增强(例如引入了 lambda 表达式)。Java 8自身的性能得到了巨大提升,在好几个关键性领域都大大超过了 Java 7。

JVM 还有其他实现。Oracle 自己的 JRockit JVM(支持 Java SE 6)。IBM 提供了自己的Java 兼容实现(包括 Java 7版本)。还有许多其他公司得到许可从而可以改进 Oracle 的 Java 技术。Oracle 的商业版 JVMJava 和 JVM 都是开源的,任何想参与 Java 开发的人都可以加入项目:http://openjdk.java.net。即便你不打算参与开发,也可以从上述网站免费下载源代码。本书讨论的所有内容基本上都是基于开源版的 Java。Oracle 也有商业版的 Java,可以通过支持合同获得。它基于标准开源版本的 Java 平台,但包括了一些开源版中所没有的特性。商业版 JVM 中和性能密切相关的一个特性就是 Java 飞行记录器(Java Flight Recorder,JFR,参见3.4.1节)。除非特别说明,本书所有的内容都适用于开源版的 Java。

虽然只有通过兼容性测试的平台才能使用 Java 的名称,但本书不会总是围绕兼容性展开讨论。那些调优标志尤其如此。所有的 JVM 实现都有一个或多个垃圾收集器,但每个供应商所提供的 GC 实现,其调优标志都是产品特定的。所以本书所讲的概念可适用于所有 Java 实现,但具体的调优标志和建议仅适用于 Oracle 的标准(基于 HotSpot 的)JVM。

用较早版本的 HotSpot JVM 时要注意,标志及其默认值在发布版之间可能会发生变化。本书只涵盖 Java 7(直到7u40)和 Java 8(仅首个版本),而不是试图穷尽迄今为止的各个版本。以后的版本(例如,假定是7u60)可能会对这些信息做少许改动。重要的变更请查阅发布说明。

从 API 层面来看,不同 JVM 实现之间的兼容性很高,即便特定类在 Oracle HotSpot Java SE(或 EE)和其他平台上的实现方式也有细微的不同。类的功能必须等价,但具体实现可以变更。所幸这些并不多见,不会对性能产生重大影响。

对于本书的剩余部分,术语 Java 和 JVM 应理解为特指 Oracle HotSpot 的实现。严格来说,“JVM 首次执行时不会进行代码编译”的说法并不正确,因为有些 Java 的实现在首次执行时会编译代码。但这种略写比到处写(和读)“Oracle HotSpot JVM……”要简便得多。JVM调优标志

除了少数例外,JVM 主要接受两类标志:布尔标志和附带参数的标志。

布尔标志采用以下语法:-XX:+FlagName 表示开启,-XX:-FlagName 表示关闭。

附带参数的标志采用以下语法:-XX:FlagName=something,表示将标志 flagName 的值设置为 something。其中 something 通常可以为任意值。例如 -XX:NewRatio=N,表示 NewRatio 可以设置为任意值 N(N 是我们讨论所关注的重点)。

介绍每个标志时,我们会讨论它的默认值。默认值的选取通常综合考虑了不同因素:运行 JVM 的物理平台,以及其他传给 JVM 的命令行参数。如有疑问,可以参考3.2.1节所介绍的方法,在给定的命令行上,添加 -XX:+Printflagsfinal(默认为 false,即“关闭”)就能获得具体运行环境中特定标志的默认值。基于环境对标志进行自动调优的过程称为自动优化(Ergonomics)。Client 和 Server 类虚拟机Java 的自动优化前提是机器被分为“Client”和“Server”。这两个术语直接与特定平台(参见第4章)上的默认 JVM 编译器相关,它们也设定了默认的调优标志。例如,机器类别决定了平台默认的垃圾收集器(参见第5章)。Microsoft Windows 上运行的任何32位 JVM(无论机器上 CPU 的个数是多少),以及单 CPU 机器(不论是什么操作系统)上运行的任何32位 JVM,都是 Client 类机器。所有其他机器(包括所有64位 JVM)都被认为是 Server 类。

从 Oracle 和 OpenJDK 站点下载的 JVM,被称为“产品”JVM。需要生成不同版本的 JVM 时,可以从源代码构建:调试版、开发版等。这些版本通常有更多的特性。特别是开发版,包含了大量的调优标志,开发人员可以尝试与 JVM 各种算法的细节打交道。不过本书基本上不会考虑这些标志。1.3 全面的性能调优

本书关注于如何以最佳方式利用 JVM 和 Java 平台 API,让程序运行得更快。但除了这两点,还有许多外在的因素影响性能。书中这些因素时不时会出现,但因为它们不只影响 Java,所以不会深入讨论。JVM 和 Java 平台的性能只是高性能主题中的一小部分。

本书会覆盖一些外部因素,这些因素的重要性不亚于 Java 的性能调优。本书中基于 Java 的调优方法可以和这些因素相互补充,但这些因素多数已经超过了本书讨论的范围。1.3.1 编写更好的算法

Java 的许多细节和性能标志都可以影响应用的性能,只不过从来都没有一个叫 -XX:+RunReallyFast 的神奇标志。

归根结底,应用的性能取决于它的代码如何编写。例如,如果程序循环遍历数组中的所有元素,JVM 就可以优化数组的边界检查,使循环更快,展开循环能提供额外的加速。但如果循环是为了找到特定元素,那目前还没有什么优化的办法,使得遍历数组和采用 HashMap 的版本一样快。

需要更高性能时,算法是否优秀就是重中之重了。1.3.2 编写更少的代码

有些人写代码是为钱,有些是为乐趣,还有些人将代码回馈社区,但不管怎样,大家都是码农(或者在写程序的团队里工作)。很难想象,我们对项目的贡献是少写代码,因为仍然有管理者通过所写的代码量来评估开发人员的绩效。

我能理解这种想法,不过这种想法与现实并不吻合。同样是正确的程序,小程序运行起来要比大程序快。对所有的计算机程序来说都是如此,Java 程序自然也不例外。要编译的代码越多,等待程序启动所耗费的时间就越长;要创建和销毁的对象越多,垃圾收集的工作

量就越大;要分配和持有的对象越多,GC 的周期就越长;要从磁盘装载进 JVM 的类越多,程序启动所花费的时间就越长;要执行的代码越多,机器硬件缓存的效率就越低;而执行的代码越多,花费的时间就越长。无法取胜的战争与直觉相反(和令人沮丧)的是,所有应用的性能都会随着时间,即应用新版本的发布而降低。但由于硬件的改善使得新程序的运行速度可以被接受,所以通常都不会有人注意到性能上的差异。想象一下,在曾经运行 Windows 95的机器上运行 Windows Aero 界面,会是什么样子?我以前喜欢 Mac Quadra 950,但它无法运行 Mac OS X(如果真这么做了,它将比 Mac OS 7.5慢许多许多)。从更小的层次上看,Firefox 23.0比 Firefox 22.0快,但它们之间的版本差别很小。具有按 tab 页浏览、同步滚动和安全特性的 Firefox 要比之前的 Mosaic 强大,但 Mosaic 从我硬盘里装载基本 HTML 文件的速度比 Firefox 23.0快50%。当 然,Mosaic 几乎不能从任何的热门网站上装载实际的URL,所以不太可能把 Mosaic 作为主要的浏览器。一般来说,特别是在两个小版本之间,代码会进行优化,从而运行得更快。性能优化工程师应该注意到这点。如果我们擅长这份工作,那就能赢得这场战斗。这是美好而有意义的事。我认为我们应该改善现有应用的性能。但铁一般的事实是:随着新特性的添加和新要求的采纳(为了与对手竞争),程序会越来越大,越来越慢。

我把这总结为“积少成多”原则。开发人员总争辩说,只是增加了很小的功能,压根就不会有什么时间损耗(特别是不使用该功能的时候)。接着项目中的其他开发人员也同样拍着胸脯保证,结果却发现性能突然下降了好几个百分点。下次发布的时候又重复出现这样的情景,而此时程序性能已经下降了10%,反复几次这样的过程之后,性能测试就会检测到资源瓶颈——内存使用达到临界点、代码缓存溢出等情况。对于这些情形,常规的性能测试可以捕获发生状况的原因,性能调优小组也可以修正主要的性能衰减。但随着时间的推移,小衰减积少成多,会越来越难以修复。

我并不是在鼓吹永远不要为产品增加新特性或者新代码,很显然增强程序是有利可图的。但你得小心权衡,尽可能提高效能。1.3.3 老调重弹的过早优化“过早优化”一词公认是由高德纳发明的,开发人员常常据此宣称:只有在运行时才能知道代码的性能有多要紧。但你可能从来没注意到,完整的原话是“我们不应该把大量时间都耗费在那些小的性能改进上;过早考虑优化是所有噩梦的根源”。

这句名言的重点是,最终你应该编写清晰、直接、易读和易理解的代码。这里的“优化”应该理解为虽然算法和设计改变了复杂程序的结构,但是提供了更好的性能。那些真正的优化最好留到以后,等到性能分析表明这些措施有巨大收益的时候才进行。

而这里所指的过早优化,并不包括避免那些已经知道对性能不好的代码结构。每行代码,如果有两种简单、直接的编程方式,那就应该选择性能更好的那种。

在某种程度上,有经验的 Java 开发人员都能很好地领会到这点(这也是一个例证,说明他们日积月累而掌握了调优艺术)。思考以下代码:log.log(Level.FINE, "I am here, and the value of X is" + calcX() + " and Y is " + calcY());

代码包含了一个看起来不太必要的字符串连接。因为除非日志级别很高,否则字符串的信息并不会记录到日志中,如果不打印日志消息,那就没必要调用 calcX() 和 calcY()。有经验的 Java 开发人员会下意识地避免这种写法。有些 IDE(例如 NetBeans)会在代码上打标记并建议更改。(然而没有完美的工具:NetBeans 会在字符串连接操作上打标记,却不会建议去掉不必要的方法调用。)

像这样的日志代码会更好:if (log.isLoggable(Level.FINE)) { log.log(Level.FINE, "I am here, and the value of X is {} and Y is {}", new Object[]{calcX(), calcY()});}

除非启用了日志功能,否则就可以在避免字符串连接(消息体中有格式化字符,不会提高性能,但使代码更清晰)的同时,避免方法调用或者对象分配。

这样写出来的代码仍然清晰易读,与原来的代码相比,没有太多额外工作。好吧,我们还是需要多敲几下键盘,多加一行逻辑。不过这仍然不属于应该避免的过早优化,它是好码农所熟悉的选择。在你思考如何写代码的时候,请不要生搬硬套前辈们的教条。

本书中我们还会看到其他例子,例如第9章讨论了处理 Vector 前先进行循环的性能。1.3.4 其他:数据库很可能就是瓶颈

如果你开发的是独立运行不使用外部资源的 Java 应用,性能就(几乎)只与应用本身相关。一旦添加了外部资源(例如数据库),那这两者的性能就都很重要了。在分布式环境中,比如 Java EE 应用服务器、负载均衡器、数据库和后台企业信息系统,Java 应用服务器的性能问题可能只是其中很小的部分。

本书并不关注整体系统的性能。对于整体系统,我们需要采取结构化方法针对系统的所有方面分析性能。CPU 使用率、I/O 延迟、系统整体的吞吐量都必须测量和分析。只有到那时,我们才能判定到底是哪个组件导致了性能瓶颈。关于这个主题有大量优秀的资源,相关的方法和工具也不只针对 Java。假定你已经完成了分析,并且判断出是运行环境中 Java 组件的性能需要改善。不只 JVM 有 bug 和性能问题这节以数据库的性能为例,但运行环境的任何部分都可能会引起性能问题。我曾经遇到过一个问题,客户正在安装新版本的应用服务器,而测试显示请求发送到服务器上的时间变得越来越长。于是我根据奥卡姆剃刀原则(参见下一条贴士),考察应用服务器中所有可能产生问题的部分。逐一排除之后,性能问题依旧,而且我也没发现后台数据库有问题。因此最可能的原因是测试框架,通过性能分析判定负载发生器——Apache JMeter——才是性能衰退的原因。它将每个响应保留在列表中,每次有新响应到来时,它都要遍历整个列表,以便找到响应时间90% 的请求(如果不熟悉这些词,请参见第2章)。部署应用的系统,它的任何部分都可能会引起性能问题。常规案例分析建议应该首先考虑系统最新变动的部分(通常是 JVM 中的应用),但仍然要准备检查环境的每一个可能出现问题的组件。

另一方面,不要忽视初步分析。如果数据库是瓶颈(提示:的确是的话),那么无论怎么优化访问数据库的 Java 应用,都无助于整体性能;实际上可能适得其反。作为一般性原则,系统负载增加越大,系统性能就会越糟糕。如果更改了 Java 应用使得它更有效,这只会增加已经过载的数据库的负载,整体性能实际反而会下降。导致的风险是,可能会得出错误结论,即认为不应该改进 JVM。

增加系统某个组件的负载从而导致整个系统性能变慢,这项原则不仅限于数据库。CPU 密集型的应用服务器增加负载,或者越来越多线程试图获取已经有线程等待的锁,还有许多其他场景,也都适用这项原则。第9章展示了一个仅涉及 JVM 的极端例子。1.3.5 常见的优化

如果所有的性能问题同等重要,从而“积少成多”地改进性能,那是多么吸引人。但常见的用例场景才是真正应该关注的重点。

我们可以从以下几方面阐述这条原则。● 借助性能分析来优化代码,重点关注性能分析中最耗时的操作。

然而请注意,这并不意味着只看性能分析中的叶子方法(参见第

3章)。● 利用奥卡姆剃刀原则诊断性能问题。性能问题最可能的原因应该

是最容易解释的:新代码比机器配置更可能引入性能问题,而机

器配置比 JVM 或者操作系统的 bug 更容易引入性能问题。隐藏

的 bug 确实存在,但不应该把最可能引起性能问题的原因首先

归咎于它,而只在测试用例通过某种方式触发了隐藏的 bug 时

才关注。但不应该一上来就跳到这种不太可能的场景。● 为应用中最常用的操作编写简单算法。以估算数学公式的程序为

例,用户可以决定他所期望的最大容许误差为10% 或1%。如果

10% 的误差适合多数用户,那么优化代码就意味着即便误差范

围缩小为1%,但是速度变慢了。1.4 小结

Java 7和 Java 8引入了大量新特性和工具,使得 Java 应用的性能更容易发挥到极致。本书有助于你理解如何有效地利用所有的 JVM 特性,最终使程序如虎添翼。

不过请记住,许多情况下,JVM 只占整体性能的一小部分。你需要对 Java 所在的环境进行整体系统调优,数据库和其他后台运行系统性能的重要性不亚于 JVM。不过整体的性能分析不是本书的关注重点,本书假设我们已经做过详细的调查,确定环境中的 Java 组件是系统的重要瓶颈。

此外,JVM 与系统其他部分的交互对性能的影响也同样重要,无论是直接交互(例如以最佳方式使用JDBC),还是间接交互(例如优化应用所使用的本地内存,这类应用与大型系统的各种组件共享机器)。本书也有助于解决这类性能问题。第2章性能测试方法

本章讨论了性能测试的4项原则。这些原则是后续章节的基础,也涵盖了性能工程科学的各个方面。

后续章节中的许多示例均取材于一个普通应用,本章也对此做了概要介绍。2.1 原则1:测试真实应用

第1条原则就是,应该在产品实际使用的环境中进行性能测试。性能测试大体上可以分为3种,每种都有其优点和不足,只有适用于实际应用的才能取得最好的效果。2.1.1 微基准测试

第1种是微基准测试。微基准测试用来测量微小代码单元的性能,包括调用同步方法的用时与非同步方法的用时比较,创建线程的代价与使用线程池的代价,执行某种算法的耗时与其替代实现的耗时,等等。

微基准测试看起来很好,但要写对却很困难。考虑以下代码,被测的方法是计算出第50个斐波那契数,这段代码试图用微基准测试来测试不同实现的性能:public void doTest() { // 主循环 double l; long then = System.currentTimeMillis(); for (int i = 0; i < nLoops; i++) { l = fibImpl1(50); } long now = System.currentTimeMillis(); System.out.println("Elapsed time: " + (now - then));}...private double fibImpl1(int n) { if (n < 0) throw new IllegalArgumentException("Must be > 0"); if (n == 0) return 0d; if (n == 1) return 1d; double d = fibImpl1(n - 2) + fibImpl(n - 1); if (Double.isInfinite(d)) throw new ArithmeticException("Overflow"); return d;}

代码看起来简单,却存在很多问题。

1. 必须使用被测的结果

这段代码的最大问题是,实际上它永远都不会改变程序的任何状态。因为斐波那契的计算结果从来没有被使用,所以编译器可以很放心地去除计算结果。智能的编译器(包括当前的 Java 7和 Java 8)最终执行的是以下代码:long then = System.currentTimeMillis();long now = System.currentTimeMillis();System.out.println("Elapsed time: " + (now - then));

结果是,无论计算斐波那契的方法如何实现,循环执行了多少次,实际的流逝时间其实只有几毫秒。循环如何被消除的细节请参见第4章。

有个方法可以解决这个问题,即确保读取被测结果,而不只是简单地写。实际上,将局部变量 l 的定义改为实例变量(并用关键字 volatile 声明)就能测试这个方法的性能了。(实例变量 l 必需声明为 volatile 的原因请参见第9章。)多线程微基准测试即便本示例是单线程微基准测试,也必需使用 volatile 变量。编写多线程微基准测试时务必深思熟虑。当若干个线程同时执行小段代码时,极有可能会产生同步瓶颈(以及其他线程问题)。所以,如果我们过多依赖多线程基准测试的结果,就常常会将大量时间花费在优化那些真实场景中很少出现的同步瓶颈上,而不是性能需求更迫切的地方。考虑这样的微基准测试,即有两个线程同时调用同步方法。由于基准测试的代码量相对于被测方法来说比较少,所以多数时间都是在执行同步方法。假设执行同步方法的时间只占整个微基准测试的50%,即便少到只有两个线程,同时执行同步代码的概率仍然很高。因此基准测试运行得很慢,并且随着线程数的增加,竞争所导致的性能问题将愈演愈烈。最终结果就是,测试衡量的是 JVM 如何处理竞争,而不是微基准测试的本来目的。

2. 不要包括无关的操作

即便使用了被测结果,依然还有隐患。上述代码只有一个操作:计算第50个斐波那契数。可想而知,其中有些迭代操作是多余的。如果编译器足够智能的话,就能发现这个问题,从而只执行一遍循环——至少可以少几次迭代,因为那些迭代是多余的。

另外,fibImpl(1000) 的性能可能与 fibImpl(1) 相差很大。如果目的是为了比较不同实现的性能,测试的输入就应该考虑用一系列数据。

也就是说,解决这个问题,需要给 fibImpl1() 传入不同的参数。可以使用随机值,但仍然必须小心。

下面是种简单方法,即在循环中使用随机数生成器:for (int i = 0; i < nLoops; i++) { l = fibImpl1(random.nextInteger());}

可以看到,循环中包括了计算随机数,所以测试的总时间是计算斐波那契数列的时间,加上生成一组随机数的时间。这可不是我们的目的。

微基准测试中的输入值必须事先计算好,比如:int[] input = new int[nLoops];for (int i = 0; i < nLoops; i++) { input[i] = random.nextInt();}long then = System.currentTimeMillis();for (int i = 0; i < nLoops; i++) { try { l = fibImpl1(input[i]); } catch (IllegalArgumentException iae) { }}long now = System.currentTimeMillis();

3. 必须输入合理的参数

此处还有第3个隐患,就是测试的输入值范围:任意选择的随机输入值对于这段被测代码的用法来说并不具有代表性。在这个测试例子中,有一半的方法调用会立即抛出异常(即所有的负数)。输入参数大于1476时,也都会抛出异常,因为此时计算出来的是 double 类型所能表示的最大的斐波那契数。

如果计算斐波那契数的速度大幅度提升,但例外情况直到计算结束时才被监测到时,在实现中会发生什么?考虑下面这种替代实现:public double fibImplSlow(int n) { if (n < 0) throw new IllegalArgumentException("Must be > 0"); if (n > 1476) throw new ArithmeticException("Must be < 1476"); return verySlowImpl(n);}

虽然很难想象会有比原先用递归更慢的实现,但我们不妨假定有这么个实现并用在了这段代码里。通过大量输入值比较这两种实现,我们会发现,新的实现竟然比原先的实现快得多——仅仅是因为在方法开始时进行了范围检查。

如果在真实场景中,用户只会传入小于100的值,那这个比较就是不正确的。通常情况下 fibImpl() 会更快,正如第1章所说,我们应该为常见的场景进行优化。(显然这是个精心构造的例子。不管怎样,仅仅在原先的实现上添加了边界测试就使得性能变好,通常这是不可能的。)热身期Java 的一个特点就是代码执行的越多性能越好,第4章将会覆盖这个主题。基于这点,微基准测试应该包括热身期,使得编译器能生成优化的代码。本章后续将深入讨论热身期的优缺点。微基准测试需要热身期,否则测量的是编译而不是被测代码的性能了。

综合所有因素,正确的微基准测试代码看起来应该是这样:package net.sdo;import java.util.Random;public class FibonacciTest { private volatile double l; private int nLoops; private int[] input; public static void main(String[] args) { FibonacciTest ft = new FibonacciTest(Integer.parseInt(args[0])); ft.doTest(true); ft.doTest(false); } private FibonacciTest(int n) { nLoops = n; input = new int[nLoops]; Random r = new Random(); for (int i = 0; i < nLoops; i++) { input[i] = r.nextInt(100); } } private void doTest(boolean isWarmup) { long then = System.currentTimeMillis(); for (int i = 0; i < nLoops; i++) { l = fibImpl1(input[i]); } if (!isWarmup) { long now = System.currentTimeMillis(); System.out.println("Elapsed time:" + (now - then)); } } private double fibImpl1(int n) { if (n < 0) throw new IllegalArgumentException("Must be > 0"); if (n == 0) return 0d; if (n == 1) return 1d; double d = fibImpl1(n - 2) + fibImpl(n - 1); if (Double.isInfinite(d)) throw new ArithmeticException("Overflow"); return d; }}

甚至这个微基准测试的测量结果中也仍然有一些与计算斐波那契数没有太大关系:调用 fibImpl1() 的循环和方法开销,将每个结果都写到 volatile 变量中也会有额外开销。

此外还需要留意编译效应。编译器编译方法时,会依据代码的性能分析反馈来决定所使用的最佳优化策略。性能分析反馈基于以下因素:频繁调用的方法、调用时的栈深度、方法参数的实际类型(包括子类)等,它还依赖于代码实际运行的环境。编译器对于相同代码的优化在微基准测试中和实际应用中经常有所不同。如果用相同的测试衡量斐波那契方法的其他实现,就能看到各种编译效应,特别是当这个实现与当前的实现处在不同的类中时。

最终,还要探讨微基准测试实际意味着什么。比如这里讨论的基准测试,它有大量的循环,整体时间以秒计,但每轮循环迭代通常是纳秒级。没错,纳秒累计起来,“积少成多”就会成为频繁出现的性能问题。特别是在做回归测试的时候,追踪级别设为纳秒很有意义。如果集合操作每次都节约几纳秒,日积月累下来意义就很重大了(示例参见第12章)。对于那些不频繁的操作来说,例如那种同时只需处理一个请求的 servlet,修复微基准测试所发现的纳秒级性能衰减就是浪费时间,这些时间用在优化其他操作上可能会更有价值。

微基准测试难于编写,真正管用的又很有限。所以,应该了解这些相关的隐患后再做出决定,是微基准测试合情合理值得做,还是关注宏观的测试更好。2.1.2 宏基准测试

衡量应用性能最好的事物就是应用自身,以及它所用到的外部资源。如果正常情况下应用需要调用 LDAP 来检验用户凭证,那应用就应该在这种模式下测试。虽然删空 LDAP 调用在模块测试中有一定意义,但应用本身必须在完整真实配置的环境中测试。

随着应用规模的增长,上述准则愈加重要也更难达到。复杂系统并不是各个部分的简单加和,装配之后,各部分的行为会有很大不同。所以,比如你伪装数据库调用,那就意味着你并不担心数据库的性能——对了,你是 Java 人,为什么要处理其他人的性能问题呢?数据库连接会因为缓存而消耗大量堆内存,网络也会因为发送大量数据而饱和,代码调用简单方法(与调用 JDBC 驱动程序的代码相比)时的不同优化,短代码路径因为 CPU 管线和缓存而比长代码路径更为有效,等等。

需要测试整体应用的另外一个原因是资源的分配。在完美世界中,我们有足够的时间去优化应用的每一行代码。但现实是,截止日期迫在眉睫,只对复杂系统进行部分优化也无法立即奏效。

考虑图2-1中的数据流。用户发起数据请求,然后系统进行业务处理,并基于结果从数据库装载数据,再进行处理,最后将更改后的数据存入数据库,并将结果发还给用户。方框中的数字(例如200 RPS)是每秒的请求数,是模块单独测试时所能承载的处理量。图2-1:典型的程序流程

从商业角度看,业务处理是最重要的,是程序存在的理由,也是有人愿意付钱给我们的原因。不过在这个例子中,即便业务处理速度提高100% 也完全没什么好处。任何应用(包含独立运行的 JVM)都

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载