Oracle已经开发了用Java编写的JIT Graal,作为预期的替代产品。
Graal也可以独立工作,并且是新平台的主要组成部分
GraalVM是支持多种语言(不仅仅是编译为JVM字节码的语言)的下一代多语言VM。
Oracle的Java实现基于开放源代码OpenJDK项目,其中包括HotSpot虚拟机,该虚拟机自Java 1.3以来就存在。 HotSpot包含两个单独的JIT编译器,称为C1和C2(有时称为“客户端”和“服务器”),现代Java安装在正常程序执行期间使用两个JIT编译器。
Java程序以解释模式启动。 在执行了一点之后,便会识别并编译经常调用的方法-首先使用C1,然后,如果HotSpot检测到更多的调用,则将使用C2重新编译该方法。 该策略称为“分层编译”,是HotSpot的默认方法。
对于大多数Java应用程序而言,这意味着C2编译器是环境中最重要的部分之一,因为它会生成与程序最重要的部分相对应的经过高度优化的机器代码。
由于运行时优化对于gcc或Go编译器这样的AOT编译器不可用,C2取得了巨大的成功,并且可以产生与C ++竞争(或比C ++更快)的代码。
但是,近年来C2的收益一直在下降,并且最近几年在编译器中未实现任何重大改进。 不仅如此,C2中的代码也变得很难维护和扩展,并且对于任何新工程师来说,要加快使用C ++特定方言编写的代码库的难度都非常困难。
实际上,(Twitter之类的公司以及Cliff Click之类的专家)普遍认为,当前的设计不可能再进行其他重大改进。 这意味着C2的任何剩余改进都将是微不足道的。
最近发行版中唯一得到改进的领域之一是使用了更多的JVM内在函数,该文档中描述了一种技术(针对@HotSpotIntrinsicCandidate注释),如下所示:
如果HotSpot VM用手写的程序集和/或手写的编译器IR(一种提高性能的编译器)替换了带注释的方法,则该方法将引起人们的兴趣。
JVM启动时,将对其执行的处理器进行探测。 这使JVM可以准确查看CPU可用的功能。 它建立一个特定于使用中的处理器的内在函数表。 这意味着JVM可以充分利用硬件的功能。
这与AOT编译不同,后者必须针对通用芯片进行编译,并对可用的功能进行保守的假设,因为如果AOT编译的二进制文件试图运行运行时CPU所不支持的指令,则会崩溃。
HotSpot已经支持许多内部函数-例如,众所周知的Compare-And-Swap(CAS)指令,用于实现原子整数之类的功能。 在几乎所有现代处理器上,这都是使用单个硬件指令来实现的。
JVM预先了解内部特性,并且依赖于操作系统或CPU体系结构的特定功能所支持。 这使得它们特定于平台,并且并非每个平台都支持所有内部函数。
通常,应将内在函数视为点修复,而不是一般技术。 它们具有功能强大,轻巧和灵活的优点,但由于必须在多个体系结构之间进行支持,因此具有潜在的高昂开发和维护成本。
因此,尽管内在函数取得了进展,但就所有意图和目的而言,C2均已到达生命周期的尽头,必须予以替换。
Oracle公司最近宣布了第一版GraalVM ,这是一个研究项目,可能会导致其最终替代HotSpot。
对于Java开发人员,可以将Graal视为几个独立但相互连接的项目-它是HotSpot的新JIT编译器,也是新的多语言虚拟机。 我们将JIT编译器称为Graal,将新VM称为GraalVM。
Graal工作的总体目标是重新考虑Java编译的工作方式(对于GraalVM也适用于其他语言)。 Graal的基本观察非常简单:
Java的(JIT)编译器将字节码转换为机器代码-用Java来讲,这只是从byte[]到另一个byte[]的转换-那么如果转换代码是用Java编写的,会发生什么呢?
事实证明,用Java编写编译器有一些主要优点,例如:
新的编译器工程师的入门门槛更低
编译器中的内存安全
能够利用成熟的Java工具空间进行编译器开发
新的编译器功能的原型制作速度更快
编译器可以独立于HotSpot
编译器将能够自行编译,从而生成一个更快的,由JIT编译的版本
Graal使用新的JVM编译器接口(JVMCI,以JEP 243的形式提供)插入HotSpot,但它也可以用作GraalVM的主要部分。该技术已经存在并在今天发售,尽管在Java 10中它仍然非常多一种实验技术,可以使用新的JIT编译器的开关是:
-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler
这意味着我们可以通过三种不同的方式来运行一个简单的程序-使用常规的分层编译器,或者使用Java 10上的Graal的JVMCI版本,最后使用GraalVM本身。
为了看到Graal的效果,让我们使用一个简单的示例,尽管它运行了足够长的时间才能看到编译器启动-简单的字符串哈希:
我们可以使用通常的方式设置PrintCompilation标志来执行此代码,以查看编译了哪些方法(它还提供了与Graal运行进行比较的基准):
java -XX:+PrintCompilation -cp target/classes/ kathik.StringHash 》 out.txt
要查看Graal作为在Java 10上运行的编译器的效果,请执行以下操作:
java -XX:+PrintCompilation -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler -cp target/classes/ kathik.StringHash 》 out-jvmci.txt 对于GraalVM:
java -XX:+PrintCompilation -cp target/classes/ kathik.StringHash 》 out-graal.txt
这些将生成三个输出文件-截断为通过运行timeHashing()的前200次迭代而生成的输出时,其外观将类似于以下内容:
$ ls -larth out*-rw-r--r-- 1 ben staff 18K 4 Jun 13:02 out.txt-rw-r--r-- 1 ben staff 591K 4 Jun 13:03 out-graal.txt-rw-r--r-- 1 ben staff 367K 4 Jun 13:03 out-jvmci.txt
不出所料,使用Graal进行的运行会产生更多的输出-这是由于PrintCompilation输出的差异所致。 这一点都不奇怪-Graal的全部观点是,JIT编译器将是首先要编译的事物之一,因此,在VM启动后的最初几秒钟内,将会有很多JIT编译器预热。
让我们看一下使用Graal编译器(通常的PrintCompilation格式)从Java 10运行的一些早期JIT输出:
在讨论GraalVM之前,值得注意的是,还有一种可以在Java 10中使用Graal编译器的方式-Ahead-of-Time编译器模式。
回想一下,Graal(作为编译器)已从头开始编写为符合新的干净接口(JVMCI)的全新编译器。 这种设计意味着Graal可以与HotSpot集成,但不限于此。
我们可以考虑使用Graal在脱机模式下对所有方法进行整体编译,而无需执行代码,而不是使用概要文件驱动的方法仅编译热方法。 这是JEP 295“提前编译”中提到的功能。
在HotSpot环境中,我们可以使用它来生成一个共享的对象/库(在Linux上为.so ,在Mac上为.dylib ),如下所示:
$ jaotc --output libStringHash.dylib kathik/StringHash.class
然后,我们可以在以后的运行中使用编译后的代码:
$ java -XX:AOTLibrary=。/libStringHash.dylib kathik.StringHash
使用Graal只有一个目标-加快启动时间,直到HotSpot中的常规分层编译方法可以接管为止。 绝对而言,在一个完整的应用程序中,尽管细节取决于工作量,但在实际基准测试中,JIT编译有望胜过AOT编译代码。
AOT编译技术仍处于领先地位,并且在技术上仅在linux / x64上受支持(甚至在实验上也是如此)。 例如,当尝试在Mac上编译java.base模块时,会发生以下错误(尽管仍会生成.dylib ):
这些错误可以通过使用编译器指令文件来控制,以将某些方法从AOT编译中排除(有关更多详细信息,请参见JEP 295页面 )。
java -XX:+PrintCompilation -XX:AOTLibrary=。/libStringHash.dylib,libjava.base.dylib kathik.StringHash 通过传递PrintCompilation,我们可以看到产生了多少JIT编译活动-现在几乎完全没有。 现在,仅对初始引导程序所需的一些真正核心方法进行了JIT编译:
111 1 n 0 java.lang.Object::hashCode (native) 115 2 n 0 java.lang.Module::addExportsToAllUnnamed0 (native) (static) 结果,我们可以得出结论,我们简单的Java应用程序现在已经以几乎100%AOT编译的形式运行。
转向GraalVM,让我们看一下该平台提供的主要功能之一-能够在GraalVM中运行的Java应用程序中完全嵌入多语言的语言。
可以认为这等效于或替代了JSR 223 (Java平台的脚本),但是Graal方法比以前的HotSpot功能中的同类技术更进一步,更深入。
该功能依赖于GraalVM和Graal SDK-作为GraalVM默认类路径的一部分提供,但应明确包含在IDE项目中,例如:
《dependency》 《groupId》org.graalvm《/groupId》 《artifactId》graal-sdk《/artifactId》 《version》1.0.0-rc1《/version》《/dependency》 最简单的示例是Hello World-让我们使用Javascript实现,因为GraalVM默认情况下会提供此代码
这意味着Truffle只能在GraalVM上运行(至少目前是这样)。
自Java 6以来,通过引入Scripting API,已经存在一种形式的多语言功能。 随着Java的基于invokedynamic的实现Nashorn的到来,它在Java 8中得到了显着增强。
GraalVM中的技术与众不同之处在于,该生态系统现在明确包括一个SDK和支持工具,用于实现多种语言,并使它们在基础VM上以同等且可互操作的公民身份运行。
迈出这一步的关键是名为Truffle的组件和一个简单的裸机VM SubstrateVM,它能够执行JVM字节码。
Truffle提供了用于创建新语言实现的SDK和工具。 通用方法是:
从语言语法开始
应用解析器生成器(例如Coco / R )
使用Maven构建解释器和简单语言运行时
在GraalVM上运行最终的语言实现
等待Graal(处于JIT模式)启动以自动增强新语言的性能
[可选]在AOT模式下使用Graal将解释器编译为本地启动器
现成的GraalVM附带JVM字节码,JavaScript和LLVM支持。 如果我们尝试调用另一种语言,例如Ruby,如下所示:
context.eval(“ruby”, “puts ”Hello World: Ruby“”);
然后GraalVM抛出运行时异常
要使用(目前仍为beta)Ruby的Truffle版本(或其他语言),我们需要下载并安装它。 对于Graal版本RC1(即将由RC2取代),可以通过以下方式实现:
gu -v install -c org.graalvm.ruby
请注意,如果GraalVM已在系统范围内作为多个用户的标准$ JAVA_HOME安装,则将需要sudo。 如果使用非OSS EE版本的GraalVM(Mac上目前唯一可用的版本),则可以进一步采取这一措施-可以将Truffle解释器转换为本机代码。
重建语言的本机映像(启动程序)将提高性能,但这需要使用命令行工具,例如:(假设GraalVM安装在系统范围内,因此需要root用户):
$ cd $JAVA_HOME
$ sudo jre/lib/svm/bin/rebuild-images ruby
这仍在开发中,并且有一些手动步骤,但是开发团队希望随着时间的推移使过程更加顺畅。
如果在重建本机组件时遇到任何问题,请不用担心-它无需重建本机映像仍可以正常工作。
让我们看一个更复杂的多语言编码示例:
这段代码有点难以阅读,但是同时使用了TruffleRuby和JavaScript。 首先,我们将这段Ruby代码称为:
class HelloWorld def hello(name) “Hello #{name}” endendhi = HelloWorld.newhi.hello(“Ruby”)
这将创建一个新的Ruby类,在其上定义一个方法,然后实例化一个Ruby对象,最后在其上调用hello()方法。 此方法返回一个(Ruby)字符串,该字符串在Java运行时中被强制转换为Java字符串。
然后,我们创建一个简单JavaScript匿名函数,如下所示:
function(x) print(‘Hello World: JavaScript with ’+ x +‘!’);
我们通过execute()调用此函数,并将Ruby调用的结果传入JS运行时中的函数,该函数将其打印出来。
请注意,当我们创建Context对象时,我们需要允许扩展访问上下文。 这是针对Ruby的,而对于JS我们则不需要它,因此在安装过程中会进行更复杂的构造。 这是当前Ruby实现的限制,将来可能会删除。
让我们看一个最终的多语言示例,看看我们可以走多远:
在此版本中,我们将返回一个实际的Ruby对象,而不仅仅是一个String。 不仅如此,我们也没有将其强制转换为任何Java类型,而是直接将其传递给此JS函数:
function(x) print(‘Hello World: JS with ’+ x.hello(‘Cross-call’) +‘!’);
它可以工作,并产生预期的输出:
Hello World: Java!
Hello World: JS with Hello Ruby: Cross-call!
这意味着JS运行时可以在具有无缝类型转换的情况下(至少在简单情况下)在单独的运行时中在对象上调用外部方法。
JVM工程师已经讨论了很长一段时间(至少十年),这种具有跨不同语义和类型系统的语言具有可互换性的能力,并且随着GraalVM的到来,它已经迈向了主流,迈出了非常重要的一步。
让我们快速了解一下这些异物如何在GraalVM中表示,方法是使用这部分JS来打印出传入的Ruby对象:
function(x) print(‘Hello World: JS with ’+ x +‘!’);
这将输出以下内容(或类似内容):
显示了将外部对象表示为一包DynamicObject对象,这将委派语义操作,在许多情况下,这些操作将返回给该对象的原始运行时。
总结本文,我们应该说一下基准和许可。 必须清楚地了解,尽管Graal和GraalVM具有巨大的前景,但它目前仍处于早期/实验技术。
它尚未针对通用用例进行优化或量产,要与HotSpot / C2达到同等水平尚需时日。 微基准测试通常也容易引起误解-在某些情况下它们可以指明方向,但最终只有整个生产应用程序的用户级基准测试才可以进行性能分析。
考虑这一点的一种方法是,C2本质上是性能的局部最大化,并且处于其设计寿命的尽头。 Graal为我们提供了突破该局部最大值并转移到更好的新区域的机会-并有可能重写我们一直以来对VM设计和编译器的认识。 尽管它仍然是不成熟的技术-而且再过几年它仍不可能完全成为主流。
这意味着,今天进行的任何性能测试都应格外谨慎。 对比性能测试(尤其是HotSpot + C2与GraalVM)将苹果与橙子进行了比较-成熟的,生产级的运行时与早期的实验相比。
还需要指出的是,GraalVM的许可制度可能与迄今为止所见的任何制度都不同。 当Oracle收购Sun时,他们收购了HotSpot,并将其作为一种非常成熟的现有产品,获得了免费软件的许可。 在HotSpot核心产品(例如UnlockCommercialFeatures开关)上进行增值和获利的尝试有限。 随着这些功能的淘汰(例如Mission Control的开源 ),可以公平地说该模型并不是巨大的商业成功。
Graal与众不同-它始于Oracle研究项目,现在正转向生产产品。 Oracle为使Graal成为现实投入了大量资金-该项目所需的个人和团队供不应求,而且价格便宜。 由于Oracle基于不同的基础技术,因此可以自由使用HotSpot的不同商业模型,并尝试通过GraalVM在更大范围的客户中获利-包括目前不为HotSpot运行时付费的客户。 Oracle甚至可能会决定GraalVM的某些功能将仅提供给在Oracle Cloud上运行的客户使用。
目前,Oracle正在交付免费提供给开发者和生产者使用的GPL许可社区版(CE),以及免费给开发者和评估版使用的企业版(EE)。 可以从Oracle的GraalVM站点下载这两个版本,还可以在其中找到更多详细信息。