解释器和编译器
HotSpot虚拟机中内置了两个即时编译器,分别为Client Compiler
和Server Compiler
,或者简称为C1编译器和C2编译器。目前主流的HotSpot虚拟机,默认采用解释器和其中一个编译器直接配合的方式工作,程序使用哪个编译器,取决于虚拟机运行的模式,HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用-client
或-server
参数去强制指定虚拟机运行在Client模式或者Server模式。
解释器和编译器搭配使用的方式在虚拟机中称为混合模式
(Mixed Mode)。
用户可以使用参数-Xint
强制虚拟机运行于解释模式
(Interpreted Mode),这时编译器完全不介入工作,全部的代码都使用解释方式执行。
也可以使用参数-Xcomp
强制虚拟机运行于编译模式
(Compiled Mode),这时将优先采用编译的方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程.(在最新的Sun HotSpot中,已经去掉了-Xcomp参数)
可以通过虚拟机的-version
目录显示这三种模式,例如
C:\Users\HAPPY>java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
C:\Users\HAPPY>java -Xint -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, interpreted mode)
C:\Users\HAPPY>java -Xcomp -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, compiled mode)
(注意每条信息的结尾显示的是模式)
编译对象与触发条件
热点代码类型
在运行过程中,会被即时编译器编译的热点代码
有两类,即
- 被多次调用的方法
- 被多次执行的循环体
对于第一种情况,由于是由方法调用触发的编译,编译器自然会把整个方法都作为编译对象,这也是虚拟机中标准的JIT编译方式。
对于第二种情况,尽管编译动作是由循环体所触发的,但是编译器仍然会以整个方法(而不是单独的循环体)作为编译对象。(因为这种情况是为了解决一个方法制备调用过一次或者少量的几次,但方法体内部存在循环次数较多的循环体的问题)。这种编译方式因为编译发生在方法执行过程中,所以形象地称之为栈上替换
(On Stack Replacement),简称OSR编译,即方法栈帧还在栈上,方法就被替换了。
热点代码的判定
判断一段代码是不是热点代码,是否需要触发即时编译,这样的行为称为热点探测
(Hot Spot Detection)。
目前主要的热点探测判定方式有两种:
- 基于采样的热点探测:用这种方法的虚拟机会周期性检测各个线程的栈顶,如果某个方法经常出现在栈顶,那这个方法就是热点方法。
- 基于计数器的热点探测: 用这种方法的虚拟机会为每个方法甚至是代码块建立计数器,统计方法的执行次数,如果一个方法的执行次数超过一定的阈值就认为是热点方法。
在HotSpot虚拟机中使用的是第二种---基于计数器的热点探测方法,因此它为每个方法准备了两类计数器,方法调用计数器和回边计数器。
还有例如基于轨迹
(Trace)的热点探测,像在FireFox的TraceMonkey和Dalvik中的新的JIT编译器都采用了这种热点探测方式。
编译过程
在默认情况下,无论是方法调用产生的即时编译请求,还是OSR编译请求,虚拟机在代码编译器还未完成之前,都仍然将按照解释方式继续执行,而编译动作则在后台的编译线程中进行。用户可以通过-XX: -BackgroundCompilation
来禁止后台编译,在禁止后台编译以后,一旦达到JIT的编译条件,执行线程向虚拟机提交编译请求后会一直等待,直到编译过程完成后再开始执行编译器输出的本地代码。
在后台编译的过程中,Server Compiler和Client Compiler这两个编译器的编译过程是不一样的。
Client Compiler
对于Client Compiler来说,它是一个简单快速的三段式编译器,主要在于局部性的优化,放弃了很多耗时较长的全局优化手段。
- 第一个阶段,将字节码构造成高级中间代码表示(High-Level Intermediate Representation,
HIR
). HIR使用静态单分配(SSA)的形式来代表代码值,以便更容易实现优化。 - 第二个阶段,从HIR中产生低级中间代码表示(Low-Level Intermediate Representation, LIR),再作一些优化。
- 最后一个阶段,使用线性扫描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窥孔(
Pepehole
)优化,然后产生机器代码。
Client Compiler架构
Server Compiler
Server Compiler为服务端性能配置特别调整过的编译器,也是一个充分优化过的高级编译器,它会执行所有经典的优化动作以及一些和JAVA语言特性相关的优化技术。
Server Compiler的寄存器分配器是一个全局图着色分配器,可以充分利用某些处理器架构例如RISC上的大寄存器集合。
基于全局图着色的寄存器分配
1、如果变量(在指令/语句序列中,或称程序“基本块”)不再被use(def也是use),则它dead
2、否则变量live(活着)
3、如果2个变量在一个block/program中都是live,则不能赋以相同的寄存器,在对应的RIG(register inference graph)中,在这2个变量节点之间连接一条边
由上面的描述可以看到,这里的寄存器分配实际上是一个全局算法。并不是只针对单个basic block的。
OK,设限制为k个寄存器,则寄存器分配问题转换为RIG的k-着色问题。是NP-complete问题。
评论:这里的寄存器分配似乎没有涉及到IR?并且是把程序代码中的变量直接映射为机器物理寄存器?对于早期的偏重于数值计算的Fortran语言来说,当然很合理。但是现代语言早已不是这个样子的了:OOP、并发、FP、VM、IR、SSA。那么关于上面的基本约束:如果2个变量同时live,则不能赋以相同寄存器。这条约束是否还有意义呢?
基于图着色模型的寄存器分配算法仅仅是个理论模型,实际所谓的最优并不可能达到,从而需要使用某些启发式。既然如此,为什么不一开始就直接使用某些实用的固定规则呢?假如这样得到的寄存器分配效率并不差多少?
Spilling:将(全局范围的)变量分配到内存而不是寄存器,这样每次用到时都需要先load最后将更新后的值store回去。这将使得一个大的program可以拆分为多个小的sub range,每个range内可使用更少的k-着色。(不过这种思路实在有点见鬼,为什么不在一开始就直接利用利用启发式这么做呢?非得等全局的图着色寄存器分配算法分析过后再rollback?)
具体可以参见
- https://max.book118.com/html/2017/0418/100997798.shtm
- https://blog.csdn.net/shuitawuhen/article/details/17127357
查看和分析即时编译过程
实例代码
/**
* Created by HAPPY
*/
public class HotCompileTest {
public static final int NUM = 20000;
public static int doubleValue(int i) {
for (int j = 0; j < 1000000; j++) ;
return i * 2;
}
public static long calcSum() {
long sum = 0;
for (int i = 0; i <= 100; i++) {
sum += doubleValue(i);
}
return sum;
}
public static void main(String[] args) {
for (int i = 0; i < NUM; i++) {
calcSum();
}
}
}
添加虚拟机参数-XX:+PrintCompilation
将虚拟机在即时编译时将被编译成本地代码的方法名称打印出来。(结果中,带有%
的输出说明是由回边计数器触发的OSR编译)
输出结果:(省略了在此不重要的函数)
......
98 33 % 3 HotCompileTest::doubleValue @ 2 (18 bytes)
98 34 3 HotCompileTest::doubleValue (18 bytes)
98 35 % 4 HotCompileTest::doubleValue @ 2 (18 bytes)
98 33 % 3 HotCompileTest::doubleValue @ -2 (18 bytes) made not entrant
99 36 4 HotCompileTest::doubleValue (18 bytes)
99 34 3 HotCompileTest::doubleValue (18 bytes) made not entrant
99 37 3 HotCompileTest::calcSum (26 bytes)
99 38 % 4 HotCompileTest::calcSum @ 4 (26 bytes)
100 39 4 HotCompileTest::calcSum (26 bytes)
101 37 3 HotCompileTest::calcSum (26 bytes) made not entrant
此外,还可以使用参数-XX:+PrintInlining
要求虚拟机输出方法内的内联信息。(在Product版的虚拟机中,还要加入-XX:+UnlockDiagnosticVMOptions
参数打开虚拟机的诊断模式)
需要下载hsdis反汇编适配器插件(win64),并放置在JRE/bin/client或者/server目录下,只要和jvm.dll的路径相同就可以被虚拟机调用。
可以使用参数-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
使虚拟机打印编译方法的汇编代码。
如果没有hsdis插件的支持,可以用参数-XX:+PrintLIR
(需要在DEBUG或FastDebug版本的Client VM)或者-XX:+PrintOptoAssembly
(在Server VM)
将编译过程中的各个数据输出到文件。
-
-XX: PrintIdealGraphLevel=2 -XX:PrintIdealGraphFile=ideal.xml
(Server Compiler),编译后会产生名为ideal.xml的文件。可以使用Ideal Graph Visualizer
来对此信息进行分析。 -
-XX:+PrintCFGToFile
(使用ClientCompiler)
网友评论