4.2 调优入门:选择编译器类型(Client、Server或二者同用)
有两种“口味”的 JIT 编译器,选择哪种常常是应用运行时所需做的仅有的编译器调优。事实上,甚至在安装 Java 之前就必须考虑如何选择编译器,因为不同的 Java 安装包包含了不同的编译器。我们来逐步分析。首先找出何种环境下该用哪种编译器。
这两种编译器被称为 client 和 server。名字来自于命令行上用于选择编译器的参数(例如 -client 或 -server)。JVM 开发者(甚至一些工具)通常称这些编译器为 C1(编译器 1,client 编译器)和 C2(编译器 2,server 编译器)。它们的名字似乎意味着如何选择编译器受程序运行的硬件平台的影响,但这并不完全正确:特别是到现在,这些术语已经超过了整整 15 年,你的“client”笔记本也已经是 4 到 8 核的 CPU 和 8 GB 内存(处理能力比 Java 刚诞生时的中型服务器还要强)。
与众不同的编译器标志
选择编译器的 Java 标志与大多数标志不同,大都没有使用
-XX。标准的编译器标志是这几个简单的词语:-client、-server或-d64。分层编译(tiered compilation)是个例外,标志采用常见的开启形式
-XX:+TieredCompilation。分层编译意味着必须使用 server 编译器。下面的命令行隐含着关闭分层编译,因为与 client 编译器的选择冲突。
% java -client -XX:+TieredCompilation other_args
两种编译器的最主要的差别在于编译代码的时机不同。client 编译器开启编译比 server 编译器要早。意味着在代码执行的开始阶段,client 编译器比 server 编译器要快,因为它的编译代码相比 server 编译器而言要多。
此处工程上考虑的权衡是,server 编译器等待编译的时候是否还能做更有价值的事:server 编译器在编译代码时可以更好地进行优化。最终,server 编译器生成的代码要比 client 编译器快。从用户角度看,权衡的取舍在于程序要运行多久,程序的启动时间有多重要。
此处最明显的问题是,为什么需要人来做这种选择?为什么 JVM 不能在启动时用 client 编译器,然后随着代码变热使用 server 编译器?这种技术被称为分层编译。代码先由 client 编译器编译,随着代码变热,由 server 编译器重新编译。
Java 7 的早期发布版中曾经提供了分层编译的实验性版本,结果却发现技术上存在许多难点(尤其是两种编译器架构的不同),所以这些实验版的分层编译效果并不好。从 Java 7u4 开始,这些难点很大程度上得到了解决,所以分层编译常常可以让应用发挥最佳性能。
Java 7 中的分层编译有些瑕疵,所以没能成为默认设置。尤其是 Java 7 的分层编译容易超出 JVM 代码缓存的大小,使得代码无法优化编译(虽然很容易处理,参见 4.4 节“编译器的中间代码优化”的讨论)。使用分层编译需要指定 server 编译器(-server 或者确认它是特定 Java 安装包所用的默认值),并确保 Java 命令行包括标志 -XX:+TieredCompilation(默认值为 false)。Java 8 中,分层编译默认为开启。
为了解其中的权衡,我们来看一些例子。
4.2.1 优化启动
当快速启动时间是首要目标时,最常使用 client 编译器。不同应用使用不同编译器标志的差别见表 4-1。
表4-1:不同应用在不同编译器标志下的启动时间
| 应用 |
-client
|
-server
|
-XX:+TieredCompilation
|
|---|---|---|---|
| HelloWorld | 0.08 | 0.08 | 0.08 |
| NetBeans | 2.83 | 3.92 | 3.07 |
| BigApp | 51.5 | 54.0 | 52.0 |
对于简单的 HelloWorld 应用,没有编译器占据优势,因为都没有足够的代码可以运行以便优化。并且对于执行时间只有 80 毫秒的任务,我们也很难注意到性能差异是否真的存在。
NetBeans 是相当典型、规模适中的 Java GUI 应用。它启动时,装载约 10 000 个类,初始化一些图形对象等。启动时,client 编译器有很明显的优势:显而易见,server 编译器的启动慢了 38.5%,大约相差了 1 秒。注意分层编译器并不是很快,虽然只慢了约 8%,相差并不大。
这就是为什么 NetBeans——许多 GUI 程序喜欢它,包括 Web 浏览器所用的 Java 插件——默认使用 client 编译器的原因。性能是王道:其他部分还不错的情况下,启动若快,便是晴天,用户就会倾向于认为整个程序都像启动那样快。
启动时间要紧吗?
关于 GUI 程序有个重要的观点,即整体性能比启动性能更重要,而且这种场景更适合使用 server 编译器。
如果 server 编译器优化了应用中的 GUI 代码,最终 GUI 的响应性会有所提高,不过最终用户可能不太会注意到这种差别。但是,如果程序执行了大量其他计算,这样做就有意义了。比如,NetBeans 可以进行大范围的(和昂贵的)代码重构,如果使用 server 编译器,速度就会很快。
通常程序供应商会考虑默认的编译器应该是哪种(因为启动时间是大家首先讨论的事情之一,所以这些程序通常被优化成有最佳的启动时间)。如果你的应用与此不同,不要犹豫,应该尽量使用 server 编译器或分层编译器。
最后来看 BigApp:一个很大的服务器程序,装载超过 20 000 个类,初始化范围很广。因为是服务器应用,可以肯定它需要使用 server 编译器。尽管运行着许多进程,对 client 编译器仍然有些许优势。这个例子的重点是第 1 章中所提到的:问题并不总在 JVM。在这个示例中,需要从磁盘读取大量的 JAR 文件,从而制约了性能(除此以外,启动上的差异更有利于 client 编译器)。
快速小结
1. 如果应用的启动时间是首要的性能考量,那 client 编译器就是最有用的。
2. 分层编译的启动时间可以非常接近于 client 编译器所获得的启动时间。
4.2.2 优化批处理
对于批处理应用来说——处理的工作量固定——编译器的选择,归根到底取决于哪种编译器使得应用运行的时间最优。表 4-2 是一个例子。
表4-2:批处理应用在不同编译器下的执行时间
| 股票数量 |
-client(秒)
|
-server(秒)
|
-XX:+TieredCompilation(秒)
|
|---|---|---|---|
| 1 | 0.142 | 0.176 | 0.165 |
| 10 | 0.211 | 0.348 | 0.226 |
| 100 | 0.454 | 0.674 | 0.472 |
| 1000 | 2.556 | 2.158 | 1.910 |
| 10 000 | 23.78 | 14.03 | 13.56 |
使用第 2 章中的代码即股票的例子,此处获取 1 年的历史信息(以及平均数和标准差),股票数量从 1 到 10 000。
股票数量为 1 到 100 时,client 编译器最快完成启动任务,说明如果处理的股票数量少于 100 只,client 编译器是最佳选择。之后,性能优势就偏向了 server 编译器(特别是分层编译的 server 编译器)。即便是少量股票,分层编译的性能也很接近于 client 编译器,所以分层编译是适合所有情况的很好的备选方案。
还有一点比较重要,分层编译总是比标准的 server 编译器好一些。理论上,一旦程序足够运行,编译了所有的热点,server 编译器就应该达到最佳(或至少等同于)的性能。但任何程序都有一小部分代码很少执行。最好是编译这些代码——即便编译不是最好的方法——而不是以解释模式运行。正如本章后面(参见 4.4.2 节“编译阈值”)所讨论的,实际上即便应用永远运行,server 编译器也不可能编译它的所有代码。
快速小结
1. 对于计算量固定的任务来说,应该选择实际执行任务最快的编译器。
2. 分层编译是批处理任务合理的默认选择。
4.2.3 优化长时间运行的应用
最后我们来看看在不同的编译器下,长时间运行的应用之间的性能差别。衡量长时间运行的应用的性能,通常来说,是在应用“热身”之后——意味着它已经运行了足够长的时间,重要的代码都已经被编译——测量它处理的吞吐量。
例子使用基本的股票计算,并在 servlet 中进行。每次调用 servlet 就会随机抽取一只股票 25 年的信息。表 4-3 是用第 2 章介绍的 fhb 程序获取的数据,显示热身期为 0、60 和 300 秒时,每秒的调用次数。
表4-3:服务器应用在不同热身期下的吞吐量
| 热身期(秒) |
-client
|
-server
|
-XX:+TieredCompilation
|
|---|---|---|---|
| 0 | 15.87 | 23.72 | 24.23 |
| 60 | 16.00 | 23.73 | 24.26 |
| 300 | 16.85 | 24.42 | 24.43 |
由于测试的周期为 60 秒,所以即便没有热身期,编译器仍然可以获得足够的信息编译热点,因此 server 编译器在本例中总是比较好。(另外,大量代码会在应用服务器启动时进行编译。)如之前所讨论的,相比单独的 server 编译器,分层编译可以编译更多代码,提供更多性能。
快速小结
对于长时间运行的应用来说,应该一直使用 server 编译器,最好配合分层编译。
快速小结