11.2 HotSpot虚拟机内的即时编译器
在本节中,我们将要了解HotSpot虚拟机内的即时编译器的运作过程,同时,还要解决以下几个问题:
为何HotSpot虚拟机要使用解释器与编译器并存的架构?
为何HotSpot虚拟机要实现两个不同的即时编译器?
程序何时使用解释器执行?何时使用编译器执行?
哪些程序代码会被编译为本地代码?如何编译为本地代码?
如何从外部观察即时编译器的编译过程和编译结果?
11.2.1 解释器与编译器
尽管并不是所有的Java虚拟机都采用解释器与编译器并存的架构,但许多主流的商用虚拟机,如HotSpot、J9等,都同时包含解释器与编译器[1]。解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释执行节约内存,反之可以使用编译执行来提升效率。同时,解释器还可以作为编译器激进优化时的一个“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立,如加载了新类后类型继承结构出现变化、出现“罕见陷阱”(Uncommon Trap)时可以通过逆优化(Deoptimization)退回到解释状态继续执行(部分没有解释器的虚拟机中也会采用不进行激进优化的C1编译器[2]担任“逃生门”的角色),因此,在整个虚拟机执行架构中,解释器与编译器经常配合工作,如图11-1所示。
图 11-1 解释器与编译器的交互
HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler和Server Compiler,或者简称为C1编译器和C2编译器(也叫Opto编译器)。目前主流的HotSpot虚拟机(Sun系列JDK 1.7及之前版本的虚拟机)中,默认采用解释器与其中一个编译器直接配合的方式工作,程序使用哪个编译器,取决于虚拟机运行的模式,HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。
无论采用的编译器是Client Compiler还是Server Compiler,解释器与编译器搭配使用的方式在虚拟机中称为“混合模式”(Mixed Mode),用户可以使用参数“-Xint”强制虚拟机运行于“解释模式”(Interpreted Mode),这时编译器完全不介入工作,全部代码都使用解释方式执行。另外,也可以使用参数“-Xcomp”强制虚拟机运行于“编译模式”(Compiled Mode)[3],这时将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程,可以通过虚拟机的“-version”命令的输出结果显示出这3种模式,如代码清单11-1所示,请注意黑体字部分。
代码清单11-1 虚拟机执行模式
C:\>java-version
java version"1.6.0_22"
Java(TM)SE Runtime Environment(build 1.6.0_22-b04)
Dynamic Code Evolution 64-Bit Server VM(build 0.2-b02-internal,19.0-b04-internal,mixed mode)
C:\>java-Xint-version
java version"1.6.0_22"
Java(TM)SE Runtime Environment(build 1.6.0_22-b04)
Dynamic Code Evolution 64-Bit Server VM(build 0.2-b02-internal,19.0-b04-internal,interpreted mode)
C:\>java-Xcomp-version
java version"1.6.0_22"
Java(TM)SE Runtime Environment(build 1.6.0_22-b04)
Dynamic Code Evolution 64-Bit Server VM(build 0.2-b02-internal,19.0-b04-internal,compiled mode)
由于即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度更高的代码,所花费的时间可能更长;而且想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释执行的速度也有影响。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机还会逐渐启用分层编译(Tiered Compilation)[4]的策略,分层编译的概念在JDK 1.6时期出现,后来一直处于改进阶段,最终在JDK 1.7的Server模式虚拟机中作为默认编译策略被开启。分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,其中包括:
第0层,程序解释执行,解释器不开启性能监控功能(Profiling),可触发第1层编译。
第1层,也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。
第2层(或2层以上),也称为C2编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
实施分层编译后,Client Compiler和Server Compiler将会同时工作,许多代码都可能会被多次编译,用Client Compiler获取更高的编译速度,用Server Compiler来获取更好的编译质量,在解释执行的时候也无须再承担收集性能监控信息的任务。
[1]作为三大商用虚拟机之一的JRockit是个例外,它内部没有解释器,因此会存在本书中所说的“启动响应时间长”之类的缺点,但它主要是面向服务端的应用,这类应用一般不会重点关注启动时间。
[2]在虚拟机中习惯将Client Compiler称为C1,将Server Compiler称为C2。
[3]在最新的Sun HotSpot中,已经去掉了-Xcomp参数。
[4]Tiered Compilation的概念在JDK 1.6时期出现,但JDK 1.7之前需要使用-XX:+TieredCompilation参数来手动开启,如果不开启分层编译策略,而虚拟机又运行在Server模式,Server Compiler需要性能监控信息提供编译依据,则可以由解释器收集性能监控信息供Server Compiler使用。分层编译的相关资料可参见:http://weblogs.java.net/blog/forax/archive/2010/09/04/tiered-compilation。