简介:Java是使用最广泛的编程语言之一。近日,甲骨文发布了Java的最新版本Java10。在此版本中,Oracle引入了109项新功能,其中最引人注目的是Graal,Java的新Jit编译器。在这个编译器中,我们可以使用Java来做Java的Jit编译器。这篇文章的作者详细描述了这个特性,非常值得一读。简介对于大多数应用程序开发人员来说,Java编译器指的是JDK自带的javac指令。该指令可以将Java源程序编译成.class文件,其中包含的代码格式称为Java字节码(Javabytecode)。这种代码格式不能直接运行,可以在不同平台的JVM中被解释器解释执行。由于解释器的效率低下,JVM中的JIT编译器(just-in-timecompiler)会在运行时选择性地将运行次数较多的方法编译成二进制代码,直接在底层硬件上运行。Oracle的HotSpotVM带有两个用C++实现的JIT编译器:C1和C2。与JVM的解释器、GC等其他子系统相比,JIT编译器不依赖直接内存访问等低级语言特性。它可以看作是一个输入Java字节码输出二进制码的黑盒子,其实现取决于开发者对开发效率和可维护性的要求。Graal是一个以Java为主要编程语言的面向Java字节码的编译器。与用C++实现的C1、C2相比,其模块化更加明显,更易于维护。Graal可以用作动态编译器,在运行时编译热点方法;也可以作为静态编译器来实现AOT编译。在Java10中,Graal作为实验性JIT编译器(JEP317)发布。本文将介绍Graal在动态编译上的应用。有关静态编译的信息,请参阅JEP295或SubstrateVM。分层编译在介绍Graal之前,我们先了解一下HotSpot中的分层编译。如前所述,HotSpot集成了两个JIT编译器——C1和C2(或客户端和服务器)。两者的区别在于前者不应用激进的优化技术,因为这些优化往往伴随着耗时的代码分析。因此,C1编译速度更快,C2编译的方法运行速度更快。在Java7之前,用户必须根据自己的应用场景选择合适的JIT编译器。例如,将C1用于偏好高启动性能的GUI客户端应用程序,将C2用于偏好高峰值性能的服务器应用程序。Java7引入了分层编译的概念,结合了C1的高启动性能和C2的高峰值性能。这两个JIT编译器和解释器将HotSpot的执行模式分为五个级别:0级:解释器解释执行1级:C1编译,无profiling2级:C1编译,只有方法和循环后端执行次数的profiling级别3:C1编译,除了level2的profiling之外,还包括branch的profilinglevel4(针对分支跳转字节码)和receivertype(针对成员方法调用或者类检测,比如checkcast,instnaceof,aastore字节码):C2Compilation其中,Level1和Level4为accepting状态——除非编译的方法失效(通常在反优化中触发),HotSpot将不再对该方法发出编译请求。上图列出了4种编译模式(不是全部)。通常,一个方法首先被解释执行(0级),然后由C1编译(3级),然后由获取profile数据的C2编译(4级)。如果编译出来的对象很简单,虚拟机认为用C1编译和C2编译没有区别,直接用C1编译,不插入profiling代码(level1)。当C1忙时,解释器会触发profiling,然后方法直接被C2编译;当C2繁忙时,该方法将首先由C1编译,并保持较少的分析(级别2)以获得更高的执行效率(与级别3相比提高30%)。Graal可以替代C2成为HotSpot的顶级JIT编译器,也就是上面提到的level4。与C2相比,Graal采用了更激进的优化方式,因此当程序达到稳定状态时,其执行效率(峰值性能)会更有优势。早期的Graal与C1和C2一样,与HotSpot紧密结合。这意味着每次编译Graal时都需要重新编译HotSpot。JEP243将Graal中依赖HotSpot的代码分离出来,形成Java-LevelJVMCompilerInterface(JVMCI)。该接口主要提供以下三个功能:响应HotSpot的编译请求,分发给Java-LevelJIT编译器允许Java-LevelJIT编译器访问HotSpot中与JIT编译相关的数据结构,包括类、字段、方法和profilingdata等,并在Java层面提供这些数据结构的抽象。提供HotSpotcodecache的Java抽象,让Java-LevelJIT编译器部署编译后的二进制代码,综合利用这三个功能。我们可以将Java-Level编译器(不限于Graal)集成到HotSpot中,响应HotSpot发出的4级编译请求,并将编译后的二进制代码部署到HotSpot的codecache中。另外,单独使用上面的第三个函数可以绕过HotSpot的编译系统——Java-Level编译器会将编译后的二进制代码直接部署为上层应用的类库。Graal自带的单元测试依赖于直接部署,而不是等待HotSpot发出编译请求;Truffle也通过这种机制部署了编译型语言解释器。Graalvs。C2前面提到JITCompiler不依赖底层语言特性,它只是一种代码形式到另一种代码形式的转换。因此,理论上,任何在C2中用C++实现的优化,都可以在Graal中用Java实现,反之亦然。事实上,许多在C2中实现的优化都被移植到了Graal,例如最近由其他开发人员贡献的String.compareTo内在函数的移植。当然,限于C++的开发/维护难度(个人猜测),很多在Graal中被证明有效的优化并没有成功移植到C2中,包括Graal的内联算法和部分逃逸分析(PEA)。内联是指在编译时识别调用点的目标方法,将其方法体纳入编译范围,并用其返回结果替换原来的调用点。最简单直观的例子就是Java中常见的getter/setter方法——内联可以将方法中调用getter/setter的callsite优化为一条内存访问指令。内联被业界称为优化之母,因为它可以带来更多的优化。但是在实践中,我们往往受限于编译单元的大小或者编译时间,所以无法抑制递归内联。因此,内联算法和策略在很大程度上决定了编译器的优劣,尤其是在使用Java8的streamAPI或者Scala语言的场景下。这两种场景对应的Java字节码中包含了大量多层的单一方法调用。Graal有两个内联实现。inliner的社区版本使用深度优先搜索方法。在分析一个方法的时候,一旦遇到不值得inline的callsite,就会回溯到方法的调用者。Graal允许自定义策略来确定某个callsite值是否不值得内联。默认情况下,Graal采用相对贪婪的策略,根据调用站点的目标方法的大小做出决定。Graalenterprise的内联器对所有调用点进行加权排序,其加权算法取决于目标方法的大小和可能的优化。当目标方法被内联时,它包含的调用点也会进入加权队列。这两种搜索方式都比较适合单方法调用多层的应用场景。逃逸分析(EA)是一种识别对象动态范围的程序分析。在编译器中有两种常见的应用:如果对象只被单线程访问,可以去掉对该对象的锁操作;如果对象是堆分配的,并且只能通过单一方法访问(再次体现内联的重要性),则将对象转换为堆栈分配。后者通常伴随着标量替换,将对象字段的访问替换为对虚拟本地操作数的访问,从而进一步将对象从堆栈分配转换为虚拟分配。这样不仅节省了原本用来存放对象头的内存空间,还借助寄存器分配器将(部分)对象字段存放在寄存器中,在节省内存的同时提高了执行效率(内存访问转化为寄存器访问)。Java中常见的for-each循环是EA的主要目标客户。我们知道for-each循环会调用被遍历对象的iterator方法,返回一个实现了Iterator接口的对象,并使用其hasNext和next接口进行遍历。Java集合中的容器类(如ArrayList)通常会构造一个新的Iterator实例,其生命周期仅限于for-each循环。如果Iterator实例的构造函数和hasNext、next方法调用(连同其方法体中以this为接收者的方法调用,如checkForComodification())都是内联的,EA会认为实例没有逃逸,并进行堆栈分配和标量替换。理想情况下,Foo.bar将被优化为以下代码:HotSpot的C2已经通过应用控制流无关的EA实现了标量替换。在此基础上,Graal的PEA引入了控制流信息,虚拟化了所有的堆分配操作,只有在对象确定时才会物化逃逸的分支。与C2的EA相比,PEA分析效率较低,但可以在对象未逃??逸的分支上实现标量替换。如下例所示,如果then-branch的执行概率为1%,那么经过PEA优化的代码99%的情况下不会进行堆分配,而C2的EA会100%进行堆分配。另一个典型的例子是渲染引擎Sunflow——当运行DaCapo基准测试套件自带的默认工作负载时,Graal的PEA确定大约27%的堆分配(总共700M)可以被虚拟化。这个数字远远超过了C2的EA。使用Graal在Java10(Linux/x64,macOS/x64)中,HotSpot仍然默认使用C2,但可以通过在java命令中添加-XX:+UnlockExperimentalVMOptions-XX:+UseJVMCICompiler参数将C2替换为Graal。OracleLabsGraalVM是直接来自OracleLabs的JDK版本。它基于Java8,包括GraalEnterprise。如果您对源代码感兴趣,可以直接查看GraalCommunityEdition的GitHubrepo。源码的编译需要mx工具和labsjdk的帮助(注意:请在页面底部下载labsjdk,直接使用GraalVM可能会出现编译问题)。使用graal/compiler目录下的mxeclipseinit、mxintellijinit或mxnetbeansinit分别生成Eclipse、IntelliJ或NetBeans的项目配置文件。
