前言
你如何定义性能?
当被问及应用程序的性能时,大部分开发人员会假定他们需要测量某些速度值,比如每秒交易数,或者处理了多少吉字节(GB)数据……,要在尽可能短的时间里完成大量工作。
如果你是应用程序架构师,那你可能会测量更广泛的指标。与按直线逻辑执行的程序相比,你或许更关注资源利用率。你可能更重视服务间连接的性能,而不是服务本身的性能。如果你要为公司做出业务决策,应用程序的性能很多时候不是用时间而是用美元来计算的。你可能会与开发人员和架构师争论资源分配,权衡 DevOps 的成本和完成公司工作所需要的时间。
有些时候,甚至是大部分时候,“好的”编码模式盛行:小方法会恰当内联,接口和类型检查成本变低,JIT 编译器生成的原生代码紧凑又高效。但是其他时候,考虑到编译器和 CPU 的限制,我们需要手动调整代码,改变抽象和架构。有些时候,对象几乎是没什么成本的,都不用考虑我们会消耗内存带宽和垃圾收集周期。其他时候,我们要处理 TB甚至更大规模的数据集,这时候即使是最好的垃圾收集器和内存子系统,也要承受很大压力。
而现在,性能问题的答案是了解你的工具。通常这意味着你不但要了解Java 语言是如何工作的,还要知道 JVM 类库、内存、编译器、垃圾收集器和应用程序运行所在的硬件是如何交互的。在我从事 JRuby 项目的工作中,我学到一个有关 JVM 的不变的真理:所有的性能问题都没有单一的解决方案,而是有很多解决方案。技巧就是找到那些方案,并把最能满足要求的拼凑起来。
学习如何平衡应用程序的设计和可用的资源,如何监控和调优JVM,如何利用比老旧的类库和模式更高效的最新 Java 技术,如何让Java 运行如飞!
对Java开发人员而言,这是一个激动人心的时刻,从来没有这么多机会在 Java 平台上构建高效、响应式的应用程序。让我们开始吧。
首先看目录
其次,看主要内容
第1章明确优化与性能;优化 Java 或其他语言代码的性能经常被视作一种暗黑艺术。性能分析有种神秘感,人们常常将其看作孤独的黑客在绞尽脑汁、深思熟虑之后练就的手艺。(孤独的黑客也是好莱坞最喜欢的关于计算机和操作人员的电影桥段之一。)画面是这样的:一个人能够深入了解某个系统,提出神奇的解决方案,使计算机运行得更快。
影像中经常夹杂这种不幸但常见的情况:软件团队没那么重视性能。进而出现的场景是,只有当系统已经陷入麻烦时,团队才会加以分析。所以也就需要性能“英雄”来救场了。不过现实情况有点不同。
事实是,性能分析是坚实的经验主义和软性的人类心理学的奇异组合。重点在于,一方面是可观测指标的绝对数字,另一方面是最终用户和干系人如何看待这些数字。本文其余部分的主题就是如何解决这一明显的悖论。
本章首先讨论了 Java 的性能是什么,不是什么;然后介绍了经验科学和测量的基本主题,以及一个好的性能实践将用到的基本词汇和观测量;最后介绍了性能测试结果中一些常见的案例。接下来我们将开始讨论 JVM 的一些主要内容,并为理解到底是什么导致基于 JVM 的性能优化如此复杂做好准备。
第2章JVM概览;Java 无疑是地球上最大的技术平台之一,根据 Oracle 的数据,该平台拥有大约 900 万到 1000 万的开发人员。按照设计,很多开发人员不需要了解平台底层的复杂机制。这就导致一种情况,只有当客户抱怨性能时,他们才会遇到这些细节问题。
不过对于关心性能的开发人员而言,理解 JVM 技术栈的基础非常重要。了解 JVM 技术可使开发人员编写出更好的软件,并为调查与性能相关的问题提供必要的理论背景。
本章将介绍 JVM 如何执行 Java,为后面章节更深入地探索这些主题打下基础。特别是第 9 章会深入介绍字节码。读者可以选择现在阅读本章,也可以在理解了其他主题之后结合第 9 章一起重读。
本章简要介绍了 JVM 的整体结构。虽然我们只能触及一些最重要的主题,但事实上,这里提到的几乎每个主题背后都有丰富完整的内容,值得进一步研究。
第 3 章将讨论操作系统和硬件工作原理的一些细节。这为 Java 性能分析人员了解观测结果提供了必要的背景。我们还将更详细地研究计时子系统,它将作为一个完整的例子来说明虚拟机和原生子系统是如何交互的。
第3章硬件与操作系统;在过去的 20 年里,处理器设计和现代硬件发生了巨大的变化。在摩尔定律和工程上的限制(特别是内存速度相对较慢)的驱动下,处理器设计的进步已经变得让人有点难以理解。缓存未命中率已经成为衡量一个应用程序性能最明显的领先指标。
在 Java 领域,JVM 的设计允许它使用额外的处理器核心,甚至对于单线程应用程序代码也是如此。这意味着与其他环境相比,Java 应用程序已经从硬件趋势中获得了明显的性能优势。
随着摩尔定律的消逝,人们的注意力再次转向软件的相对性能上。注重性能的工程师至少需要了解现代硬件和操作系统的基本要点,以确保他们能够充分利用硬件,而不是反其道而行之。
下一章将介绍性能测试的核心方法论,并且讨论性能测试的主要类型、需要承担的任务以及性能工作的整个生命周期。我们还将列举一些在性能分析领域常见的最佳实践和反模式。
第4章性能测试模式与反模式;性能测试会因不同的原因而进行。本章将介绍团队可能希望执行的不同类型的测试,并讨论每种类型的最佳实践。
本章的后半部分将概述一些可能会困扰性能测试或团队的常见反模式,并阐释如何重构的解决方案,以防止它们成为团队的问题。
当评估性能结果时,一定要以恰当的方式处理数据,避免陷入不科学、主观的思考中。本章介绍了一些测试类型、测试最佳实践以及性能分析中伴生的反模式。
下一章将研究底层的性能测量手段、微基准测试的陷阱,以及一些用于处理从 JVM 中测得的原始结果的统计技术。
第5章微基准测试与统计;本章将考虑直接测量 Java 性能数字的具体细节。JVM 的动态特性意味着性能数字往往比许多开发人员预期的要更难处理。因此,互联网上出现了许多不准确或带有误导性的性能数字。
本章的一个主要目标是确保你意识到这些可能的陷阱,并且只生成你和其他人可以信赖的性能数字。特别需要注意的是,对小块 Java 代码的测量(微基准测试)非常微妙且难以正确完成,这也是本章将要探究的主要内容,同时我们还会介绍性能工程师应该如何正确使用它。
第6章理解垃圾收集;自平台诞生以来,垃圾收集一直是 Java 社区内讨论的热门话题。本章介绍了性能工程师需要了解的关键概念,以便有效地与 JVM 的垃圾收集子系统一起工作。这些概念包括:
- 标记和清除收集;
- 对象在 HotSpot 内部的运行时表示;
- 弱分代假说;
- HotSpot 的内存子系统实例;
- 并行收集器;
- 分配及其核心作用。
下一章将讨论垃圾收集的调优、监控和分析。有些主题在本章已经出现过,特别是分配,以及过早晋升等特殊效应,这些内容对于接下来的目标和主题也特别重要,经常回来参考本章可能会很有帮助。
第7章垃圾收集高级话题;上一章介绍了 Java 垃圾收集的基本理论。以此为起点,本章将进一步研究现代 Java 垃圾收集器的理论。这个领域有很多不可避免的权衡可以指导工程师如何选择收集器。
首先,本章将介绍并深入了解 HotSpot JVM 提供的其他收集器,其中包括停顿时间超短、通常为并发的收集器(CMS)和现代通用收集器(G1)。
此外,还将考虑一些比较少见的收集器,包括:
- Shenandoah
- C4
- 均衡(balanced)收集器
- 遗留的 HotSpot 收集器
并非所有这些收集器都在 HotSpot 虚拟机中使用,我们还将讨论另外两个虚拟机的收集器: IBM J9(IBM 的一款 JVM,之前是闭源的,目前正在逐步开源)和 Azul Zing(一款专有的 JVM),我们在 2.6 节曾介绍过。
第8章垃圾收集日志、监控、调优及工具;本章介绍了垃圾收集调优艺术的一些皮毛。这里演示的技术大多是针对个别收集器的,但也有一些普遍适用的基本技术。本章还介绍了一些处理垃圾收集日志的基本原则以及一些有用的工具。
下一章将讨论 JVM 的另一个主要子系统:应用程序代码的执行。我们首先将概述解释器,然后在此基础上开始讨论 JIT 编译,包括它与标准编译(或者说 AOT 编译)的关系。
第9章JVM上的代码执行;JVM 的初始代码执行环境是字节码解释器。本章探讨了解释器的基础知识,因为要正确理解 JVM 的代码执行,掌握字节码如何工作的知识是必不可少的。此外,我们还介绍了 JIT 编译的基本理论。
然而,对于大多数性能工作而言,JIT 编译的代码的行为远比解释器的任何方面重要。下一章将在本章介绍的基础上,深入研究 JIT 编译的理论与实践。
对于许多应用程序而言,本章所演示的针对代码缓存的简单调优技术已经足够了。但对于那些对性能特别敏感的应用程序来说,可能还需要对JIT 行为进行更深入地探索。下一章将介绍一些对要求更严格的应用程序进行调优的工具和技术。
第10章理解即时编译;本章将深入介绍 JVM 中 JIT 编译器的内部工作方式。大部分内容直接适用于 HotSpot,不过并不保证和其他 JVM 实现一致。
我们曾经提到过,与 JIT 编译相关的科学研究已经相当深入,不只是JVM,很多现代编程环境也用到了 JIT。因此,很多 JIT 技术也适用于其他 JIT 编译器。
因为这个主题非常抽象,而且技术上也很复杂,所以需要一些工具来帮我们理解 JVM 的内部工作方式,并将其以可视化方式表现出来。本章将使用的主要工具是 JITWatch,我们会先加以介绍。之后会解释具体的JIT 优化和特性,并演示如何通过 JITWatch 观察该技术及其效果。
第11章Java语言性能技术;本章讨论了标准 Java 集合 API 的一些性能问题,以及处理领域对象的关键关注点。
最后我们探讨了另外两个与平台级别关系密切的应用程序性能方面的考虑:终结化和方法句柄。虽然很多开发人员在日常工作中并不会遇到这两个概念,但对于关注性能的工程师来说,了解和认识它们可以充实自己的技术工具箱。
下一章将继续讨论几个重要的开源库,包括那些为标准集合类提供替代选择的库,以及日志和相关问题。
第12章并发性能技术;在迄今为止的计算历史上,软件开发人员通常以顺序格式编写代码。程序设计语言和硬件一般只提供一次处理指令的能力。许多情况下,人们享受到了所谓的“免费午餐”就是购买最新的硬件来提高应用程序的性能。芯片上可用的晶体管数量的增加带来了处理指令性能更好、更强的处理器。
许多读者都曾遇见过这样的情况:将软件搬到一个更大或更新的机器上就能解决容量问题,而不用花钱去查找底层问题或考虑不同的编程范式。
对于希望使用多线程来改进应用程序性能之前应该考虑哪些主题,本章只触及了一些表面内容。在将单线程应用程序转变为并发设计的时候,应该:
- 确保能够准确测量线性处理的性能;
- 应用一个变化,并测试性能是否真的得到了提高;
- 确保性能测试易于重新运行,特别是当系统处理的数据大小可能发生变化时。
第13章剖析;在程序员群体中,剖析(profiling)这个术语的使用并不是非常统一。事实上,可能的剖析方法有很多种,其中最常见的有以下两种:
- 执行
- 分配
本章将涵盖这两个主题。首先重点关注执行剖析,我们会借着这个主题来介绍可用于剖析程序的工具。之后会介绍内存剖析,看一看各种工具是如何提供这种能力的。
我们将探讨对于 Java 开发人员和性能工程师而言,了解剖析器的一般操作方式是多么重要。因为剖析器有可能扭曲应用程序的行为,并表现出明显的偏差。
执行剖析是性能剖析的领域之一,在这个领域中,这些偏差就会凸显出来。谨慎的性能工程师会意识到这种可能性,并通过各种方式来弥补,包括使用多种工具进行剖析,以了解真正发生的情况。
对于性能工程师而言,同样重要的是要解决自己的认知偏差,不要致力于挖掘符合自己预期的性能行为。在第 4 章中遇到的反模式和认知陷阱就是我们训练自己避免这些问题的一个很好的开始。
第14章高性能日志和消息系统;日志记录是所有生产级应用程序中不可或缺的一部分,所使用的日志记录器的类型对整体的应用程序性能有非常大的影响。当涉及日志记录时,重要的是要将应用程序当作一个整体而不只是执行日志记录语句来考虑,同时注意它对其他 JVM 子系统(如线程使用和垃圾收集)的影响。
本章包含一些低延迟库的简单例子,从最底层开始,一直到一个完整的消息系统实现。显然,应该把低延迟系统的目的和目标应用于整个软件栈,从最底层的队列一直到更高层次的应用。低延迟、高吞吐量的系统需要大量的思考、经验和控制,这里讨论的许多开源项目是基于大量丰富的经验构建起来的。如果你需要创建一个新的低延迟系统,只要能从底层设计目标一直坚持到顶层应用程序,这些项目都将为你节省几天甚至几周的开发时间。
本章开始提出的一个问题是 Java 和 JVM 可以在多大程度上应用于高吞吐量的应用程序。使用任何语言编写低延迟、高吞吐量的应用程序都是非常困难的,但是在所有可用的语言中,Java 提供了最好的工具和生产效率。此外,Java 和 JVM 确实增加了另一个抽象层次,我们需要对其进行管理,并在某些情况下加以规避。同时,考虑硬件、JVM 性能和更底层的问题也非常重要。
我们在日常的 Java 开发工作中一般不会遇到这些较为底层的问题。使用本章所讨论的较新的免分配的日志库、数据结构和消息传递协议,可以大大降低进入门槛,因为很多复杂的问题已经被开源社区解决了。
第15章Java9以及Java的未来方向;自第一个版本发布以来,Java 已经发生了很大的变化,即从一开始并没有被明确设计为一种高性能语言,到现在已经成为这样的语言。即使Java 已扩展到很多新应用领域,但是核心的 Java 平台、社区和生态系统仍然保持着健康和活力。
像 Project Metropolis 和 Graal 等大胆的新举措,正在重新塑造核心虚拟机。invokedynamic 也让 Java 走出了其演进的舒适区,为下一个十年重新改造自己。Java 已经表明它不怕进行大胆的更改,比如增加值类型以及重新解决泛型存在的复杂问题。
Java/JVM 性能是一个非常有活力的领域,本章中我们看到了性能在很多领域中仍在取得进步。还有很多其他的项目我们没有时间提及,包括Java/ 原生代码交互(project panama)和新的垃圾收集器(如 Oracle 的ZGC)。
因此,本文所讲的内容并不全面,因为性能工程师还有很多东西需要了解。尽管如此,我们还是希望它能够对读者理解 Java 性能世界有所帮助,也为读者的性能之旅提供一些路标。