11.5 实战:深入理解Graal编译器 11.5.3 JVMCI编译器接口
11.5.3 JVMCI编译器接口
现在请读者来思考一下,如果让您来设计JVMCI编译器接口,它应该是怎样的?既然JVMCI面向的是Java语言的编译器接口,那它至少在形式上是与我们已经见过无数次的Java接口是一样的。我们来考虑即时编译器的输入是什么。答案当然是要编译的方法的字节码。既然叫字节码,顾名思义它就应该是“用一个字节数组表示的代码”。那接下来它输出什么?这也很简单,即时编译器应该输出与方法对应的二进制机器码,二进制机器码也应该是“用一个字节数组表示的代码”。这样的话,JVMCI接口就应该看起来类似于下面这种样子:
1 | interface JVMCICompiler { |
事实上JVMCI接口只比上面这个稍微复杂一点点,因为其输入除了字节码外,HotSpot还会向编译器提供各种该方法的相关信息,譬如局部变量表中变量槽的个数、操作数栈的最大深度,还有分层编译在底层收集到的统计信息等。因此JVMCI接口的核心内容实际就是代码清单11-13总所示的这些。
1 | interface JVMCICompiler { |
我们在Eclipse中找到JVMCICompiler接口,通过继承关系分析,可以清楚地看到有一个实现类HotSpotGraalCompiler实现了JVMCI,如图11-12所示,这个就是我们要分析的代码的入口。
为了后续调试方便,我们先准备一段简单的代码,并让它触发HotSpot的即时编译,以便我们跟踪观察编译器是如何工作对的。具体代码如清单11-14所示。
1 | public class Demo { |
由于存在无限循环,workload()方法肯定很快就会被虚拟机发现是热点代码因而进行编译。实际上除了workload()方法以外,这段简单的代码还会导致相当多的其他方法的编译,因为一个最简单的Java 类的加载和运行也会触发数百个类的加载。为了避免干扰信息太多,笔者加入了参数-XX: CompileOnly来限制只允许workload()方法被编译。先采用以下命令,用标准的服务端编译器来运行清单11-14中所示的程序。
1 | $ javac Demo.java |
上面显示wordload()方法确实被分层编译了多次,“made not entrant”的输出就表示了方法的某个已编译版本被丢弃过。从这段信息中我们清楚看到,分层编译机制及最顶层的服务端编译都已经正常工作了,下一步就是用我们在Eclipse中的Graal编译器代替HotSpot的服务端编译器。
为简单起见,笔者加上-XX:-TieredCompilation关闭分层编译,让虚拟机只采用有一个JVMCI编译器而不是由客户端编译器和JVMCI混合分层。然后使用参数-XX:+EnableJVMCI、-XX: +UseJVMCICompiler来启用JVMCI接口和JVMCI编译器。由于这些目前尚属实验阶段的功能,需要再使用-XX:+UnlockExperimentalVMOptions参数进行解锁。最后,也是最关键的一个问题,如何让HotSpot找到Graal编译器的位置呢?
如果采用特殊版的JDK 8,那虚拟机将会自动去查找JAVA_HOME/jre/lib/jvmci目录。假如这个目录不存在,那就会从-Djvmci.class.path.append参数中搜索。它查找的目标,即Graal编译器的JAR包,刚才我们已经通过mx build命令成功编译出来,所以在JDK 8下笔者使用的启动参数如代码清单11-15所示。
代码清单11-15 JDK8的运行配置
1 | -Djvmci.class.path.append=~/graal/compiler/mxbuild/dists/jdk1.8/graal.jar:~/graal/sdk/mxbuild/dists/jdk1.8/graal |
如果读者采用JDK 9或以上版本,那原本的Graal编译器是实现在jdk.internal.vm.compiler模块中的,我们只要用–upgrade-module-path参数指定这个模块的升级包即可,具体如代码清单11-16所示。
1 | --module-path=~/graal/sdk/mxbuild/dists/jdk11/graal.jar |
通过上述参数,HotSpot就能顺利找到并应用我们编译的Graal编译器了。为了确认效果,我们对HotSpotGraalCompiler类的compileMethod()方法做一个简单改动,输出编译的方法名称和编译耗时,具体如下(黑色加粗代码是笔者在源码中额外添加的内容):
1 | public CompilationRequestResult compileMethod(CompilationRequest request) { |
在Eclipse里面运行这段代码,不需要重新运行mx build,马上就可以看到类似如下所示的输出结果:
1 | 97 1 Demo::workload (4 bytes) |
[^1]: 本节部分示例和图片来自于Chris Seaton的文章《Understanding How Graal Works-a Java JIT Compiler Written in Java》:https://chrisseaton.com/truffleruby/jokerconf17/。