HotSpot 即时编译器

君子以自强不息。

解释器与编译器

解释器: 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。

编译器: 在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。

当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。 解释器还可以作为编译器激进优化时的一个“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立时通过逆优化(Deoptimization)退回到解释状态继续执行。

HotSpot 虚拟机中内置了两个即时编译器,分别称为 Client Compiler 和 Server Compiler。目前主流的 HotSpot 虚拟机(Sun 系列 JDK 1.7 及之前版本的虚拟机)中,默认采用解释器与其中一个编译器直接配合的方式工作,程序使用哪个编译器,取决于虚拟机运行的模式,HotSpot 虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在 Client 模式或 Server 模式。

无论采用的编译器是 Client Compiler 还是 Server Compiler,解释器与编译器搭配使用的方式在虚拟机中称为“混合模式”(Mixed Mode),用户可以使用参数“-Xint”强制虚拟机运行于“解释模式”(Interpreted Mode),这时编译器完全不介入工作,全部代码都使用解释方式执行。另外,也可以使用参数“-Xcomp”强制虚拟机运行于“编译模式”(Compiled Mode)[插图],这时将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程,可以通过虚拟机的“-version”命令的输出结果显示出这3种模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
martin@martins-MacBook-Pro ~ % java -version
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, mixed mode)

martin@martins-MacBook-Pro ~ % java -Xint -version
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, Interpreted mode)

martin@martins-MacBook-Pro ~ % java -Xcomp -version
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, Compiled  mode)

编译对象与触发条件

在运行过程中会被即时编译器编译的“热点代码”有两类:

  • 被多次调用的方法。
  • 被多次执行的循环体。

判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测(Hot Spot Detection),其实进行热点探测并不一定要知道方法具体被调用了多少次,目前主要的热点探测判定方式有两种:

  • 基于采样的热点探测(Sample Based Hot Spot Detection):采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”。基于采样的热点探测的好处是实现简单、高效,还可以很容易地获取方法调用关系(将调用堆栈展开即可),缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
  • 基于计数器的热点探测(Counter Based Hot Spot Detection):采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。这种统计方法实现起来麻烦一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对来说更加精确和严谨。

编译过程

在默认设置下,无论是方法调用产生的即时编译请求,还是 OSR 编译请求,虚拟机在代码编译器还未完成之前,都仍然将按照解释方式继续执行,而编译动作则在后台的编译线程中进行。用户可以通过参数 -XX:-BackgroundCompilation 来禁止后台编译,在禁止后台编译后,一旦达到 JIT 的编译条件,执行线程向虚拟机提交编译请求后将会一直等待,直到编译过程完成后再开始执行编译器输出的本地代码。

在后台执行编译的过程中,Server Compiler 和 ClientCompiler 两个编译器的编译过程是不一样的。

Client Compiler: 是一个简单快速的三段式编译器,主要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化手段。

Server Compiler: 是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的高级编译器。

即时编译结果

-XX:+PrintCompilation 参数可以要求虚拟机在即时编译时将被编译成本地代码的方法名称打印出来。

-XX:+PrintInlining 参数可以要求虚拟机输出方法内联信息。

-XX:+PrintAssembly 参数可以要求虚拟机打印编译方法的汇编代码。

如果没有 HSDIS 插件支持,可以使用 -XX:+PrintOptoAssembly(用于 ServerVM)或 -XX:+PrintLIR(用于 Client VM)来输出比较接近最终结果的中间代码。