这个月,我着手撰写一篇文章,分析一个写得很糟糕的微评测。毕竟,我们 的程序员一直受性能困扰,我们也都想了解我们编写、使用或批评的代码的性能 特征。当我偶然间写到性能这个主题时,我经常得到这样的电子邮件:“我写的 这个程序显示,动态 frosternation 要比静态 blestification 快,与您上一 篇的观点相反!”许多随这类电子邮件而来的所谓“评测“程序,或者它们运行 的方式,明显表现出他们对于 JVM 执行字节码的实际方式缺乏基本认识。所以 ,在我着手撰写这样一篇文章(将在未来的专栏中发表)之前,我们先来看看 JVM 幕后的东西。理解动态编译和优化,是理解如何区分微评测好坏的关键(不 幸的是,好的微评测很少)。
动态编译简史
Java 应用程序的编译过程与静态编译语言(例如 C 或 C++)不同。静态编 译器直接把源代码转换成可以直接在目标平台上执行的机器代码,不同的硬件平 台要求不同的编译器。Java 编译器把 Java 源代码转换成可移植的 JVM 字节 码,所谓字节码指的是 JVM 的“虚拟机器指令”。与静态编译器不同,javac 几乎不做什么优化 —— 在静态编译语言中应当由编译器进行的优化工作,在 Java 中是在程序执行的时候,由运行时执行。
第一代 JVM 完全是解释的。JVM 解释字节码,而不是把字节码编译成机器码 并直接执行机器码。当然,这种技术不会提供最好的性能,因为系统在执行解释 器上花费的时间,比在需要运行的程序上花费的时间还要多。
即时编译
对于证实概念的实现来说,解释是合适的,但是早期的 JVM 由于太慢,迅速 获得了一个坏名声。下一代 JVM 使用即时 (JIT) 编译器来提高执行速度。按照 严格的定义,基于 JIT 的虚拟机在执行之前,把所有字节码转换成机器码,但 是以惰性方式来做这项工作:JIT 只有在确定某个代码路径将要执行的时候,才 编译这个代码路径(因此有了名称“ 即时 编译”)。这个技术使程序能启动得 更快,因为在开始执行之前,不需要冗长的编译阶段。
JIT 技术看起来很有前途,但是它有一些不足。JIT 消除了解释的负担(以 额外的启动成本为代价),但是由于若干原因,代码的优化等级仍然是一般般。为了避免 Java 应用程序严重的启动延迟,JIT 编译器必须非常迅速,这意味着 它无法把大量时间花在优化上。所以,早期的 JIT 编译器在进行内联假设 (inlining assumption)方面比较保守,因为它们不知道后面可能要装入哪个类 。
虽然从技术上讲,基于 JIT 的虚拟机在执行字节码之前,要先编译字节码, 但是 JIT 这个术语通常被用来表示任何把字节码转换成机器码的动态编译过程 —— 即使那些能够解释字节码的过程也算。
HotSpot 动态编译
HotSpot 执行过程组合了编译、性能分析以及动态编译。它没有把所有要执 行的字节码转换成机器码,而是先以解释器的方式运行,只编译“热门”代码 —— 执行得最频繁的代码。当 HotSpot 执行时,会搜集性能分析数据,用来决 定哪个代码段执行得足够频繁,值得编译。只编译执行最频繁的代码有几项性能 优势:没有把时间浪费在编译那些不经常执行的代码上;这样,编译器就可以花 更多时间来优化热门代码路径,因为它知道在这上面花的时间物有所值。而且, 通过延迟编译,编译器可以访问性能分析数据,并用这些数据来改进优化决策, 例如是否需要内联某个方法调用。
为了让事情变得更复杂,HotSpot 提供了两个编译器:客户机编译器和服务 器编译器。默认采用客户机编译器;在启动 JVM 时,您可以指定 -server 开关 ,选择服务器编译器。服务器编译器针对最大峰值操作速度进行了优化,适用于 需要长期运行的服务器应用程序。客户机编译器的优化目标,是减少应用程序的 启动时间和内存消耗,优化的复杂程度远远低于服务器编译器,因此需要的编译 时间也更少。
HotSpot 服务器编译器能够执行各种样的类。它能够执行许多静态编译器中 常见的标准优化,例如代码提升( hoisting)、公共的子表达式清除、循环展开 (unrolling)、范围检测清除、死代码清除、数据流分析,还有各种在静态编译 语言中不实用的优化技术,例如虚方法调用的聚合内联。