内容简介:甲骨文的Java实现是基于开源的OpenJDK项目,其中包括自Java 1.3以来一直存在的HotSpot虚拟机。HotSpot包含两个独立的JIT编译器,分别是C1和C2(有时称为“客户端”编译器和“服务器端”编译器),现在的Java通常会在运行程序期间同时使用这两个JIT编译器。Java程序首先在解释模式下启动,在运行了一段时间之后,经常被调用的方法会被识别出来,并使用JIT编译器进行编译——先是使用C1,如果HotSpot检测到这些方法有更多的调用,就使用C2重新编译这些方法。这种策略被称为“分层编
关键要点
- Java的C2 JIT编译器寿终正寝。
- 新的JVMCI编译器接口支持可插拔编译器。
- 甲骨文开发了Graal,一个用 Java 编写的JIT,作为潜在的编译器替代方案。
- Graal也可以独立运行,是新平台的主要组件。
- GraalVM是下一代VM,支持多种语言(不仅仅是那些可编译为JVM字节码的语言)。
甲骨文的Java实现是基于开源的OpenJDK项目,其中包括自Java 1.3以来一直存在的HotSpot虚拟机。HotSpot包含两个独立的JIT编译器,分别是C1和C2(有时称为“客户端”编译器和“服务器端”编译器),现在的Java通常会在运行程序期间同时使用这两个JIT编译器。
Java程序首先在解释模式下启动,在运行了一段时间之后,经常被调用的方法会被识别出来,并使用JIT编译器进行编译——先是使用C1,如果HotSpot检测到这些方法有更多的调用,就使用C2重新编译这些方法。这种策略被称为“分层编译”,是HotSpot默认采用的方式。
对于大多数Java应用程序来说,C2编译器是整个运行环境中最重要的一个部分,因为它为程序中最重要的部分代码生成了高度优化的机器码。
C2非常成功,可以生成与C++相媲美(甚至比C++更快)的代码,这要归功于C2的运行时优化,而这些在AOT(Ahead of Time)编译器(如gcc或 Go 编译器)中是没有的。
不过,近年来C2并没有带来多少重大的改进。不仅如此,C2中的代码变得越来越难以维护和扩展,新加入的工程师很难修改使用C++特定方言编写的代码。
事实上,人们(Twitter等公司以及像Cliff Click这样的专家)普遍认为,在当前的基础上根本不可做出重大的改进。也就是说,任何后续的C2改进都是微不足道的。
在最近发布的版本中有一些改进,比如使用了更多的JVM内联函数(intrinsic),文档中是这样描述的这项技术的(主要用于描述 @HotSpotIntrinsicCandidate 注解):
如果HotSpot VM使用手写汇编或手写编译器IR(一种旨在提升性能的编译器内联函数)替换带注解的方法,那么这个方法就是内联的。
JVM在启动时会探测它运行在哪个处理器上,因此JVM可以准确地知道CPU支持哪些特性。它创建了一个特定于当前处理器的内联函数表,也就是说JVM可以充分利用硬件的能力。
这与AOT编译不同,后者在编译时考虑的是通用芯片,并对可用的特性做出保守的假设,因为如果AOT编译的二进制文件在运行时试图执行当前CPU不支持的指令,就会崩溃。
HotSpot已经支持了不少内联函数——例如众所周知的Compare-And-Swap(CAS)指令,可用于实现原子整数等功能。在几乎所有的现代处理器上,这都是通过单个硬件指令来实现的。
JVM预先知道这些内联函数,并依赖于操作系统或CPU架构对特定功能的支持。因此,它们特定于平台,并非每个平台都支持所有的内联函数。
一般来说,内联函数应该被视为点修复,而不是一种通用技术。它们具有强大、轻量级和灵活的优点,但要支持多种架构,带来了潜在的高开发和维护成本。
因此,尽管在内联函数方面取得了进展,但不管怎样,C2已经走到了生命的尽头,必须被替换掉。
甲骨文最近宣布推出第一版 GraalVM ,这是一个研究项目,可能会成为HotSpot的替代方案。
Java开发人员可以认为Graal是由几个独立但互相关联的项目组成的——它既是HotSpot的新型JIT编译器,也是一个新的多语言虚拟机。我们使用Graal来称呼这个新的编译器,使用GraalVM来称呼这个新虚拟机。
Graal的总体目标是重新思考如何更好地编译Java(以及GraalVM支持的其他语言)。Graal最初的出发点非常简单:
Java的(JIT)编译器将字节码转换为机器码——在Java中,只不过是从一个byte[]到另一个byte[]的转换——那么如果转换代码是用Java编写的话会怎样呢?
事实证明,用Java编写编译器有如下的一些优点:
- 工程师开发新编译器的进入门槛要低得多。
- 编译器的内存安全性。
- 能够利用成熟的Java工具进行编译器开发。
- 更快的新编译器功能原型设计。
- 编译器可以独立于HotSpot。
- 编译器能够自己编译自己,以生成更快的JIT编译版本。
Graal使用了新的JVM编译器接口(JVMCI,对应 JEP 243 ),可以用在HotSpot中,也可以作为GraalVM的主要组成部分。Graal已经发布,尽管它在Java 10中仍然是处于实验性阶段。要切换到新的JIT编译器,可以这样做:
-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler
我们可以通过三种不同的方式运行一个简单的程序——使用常规的分层编译器,或者使用Java 10上的Graal,或者使用GraalVM本身。
为了展示Graal的效果,我们使用了一个简单的例子,它可以长时间运行,这样就看到编译器的启动过程——进行简单的字符串哈希:
package kathik; public final class StringHash { public static void main(String[] args) { StringHash sh = new StringHash(); sh.run(); } void run() { for (int i=1; i<2_000; i++) { timeHashing(i, 'x'); } } void timeHashing(int length, char c) { final StringBuilder sb = new StringBuilder(); for (int j = 0; j < length * 1_000_000; j++) { sb.append(c); } final String s = sb.toString(); final long now = System.nanoTime(); final int hash = s.hashCode(); final long duration = System.nanoTime() - now; System.out.println("Length: "+ length +" took: "+ duration +" ns"); } }
我们可以设置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
这些将生成三个输出文件——前200次调用timeHashing()后生成的输出看起来像这样:
$ 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编译器预热动作。
让我们看一下在Java 10上使用Graal编译器的JIT输出(常规的PrintCompilation格式):
$ grep graal out-jvmci.txt | head 229 293 3 org.graalvm.compiler.hotspot.HotSpotGraalCompilerFactory::adjustCompilationLevelInternal (70 bytes) 229 294 3 org.graalvm.compiler.hotspot.HotSpotGraalCompilerFactory::checkGraalCompileOnlyFilter (95 bytes) 231 298 3 org.graalvm.compiler.hotspot.HotSpotGraalCompilerFactory::adjustCompilationLevel (9 bytes) 353 414 ! 1 org.graalvm.compiler.serviceprovider.JDK9Method::invoke (51 bytes) 354 415 1 org.graalvm.compiler.serviceprovider.JDK9Method::checkAvailability (37 bytes) 388 440 1 org.graalvm.compiler.hotspot.HotSpotForeignCallLinkageImpl::asJavaType (32 bytes) 389 441 1 org.graalvm.compiler.hotspot.word.HotSpotWordTypes::isWord (31 bytes) 389 443 1 org.graalvm.compiler.core.common.spi.ForeignCallDescriptor::getResultType (5 bytes) 390 445 1 org.graalvm.util.impl.EconomicMapImpl::getHashTableSize (43 bytes) 390 447 1 org.graalvm.util.impl.EconomicMapImpl::getRawValue (11 bytes)
像这样的小实验应该谨慎对待。例如,太多的屏幕IO可能会影响预热性能。不仅如此,随着时间的推移,为不断增加的字符串分配的缓冲区将会变得越来越大,以至于必须在Humongous Region(G1回收器为大对象保留的特殊区域)中进行分配——Java 10和GraalVM默认使用了G1回收器。这意味着在一段时间之后,G1垃圾回收主要由G1 Humongous主导,而这通常是非常规的情况。
在讨论GraalVM之前,我们需要注意的是,Java 10为Graal编译器提供了另一种使用方式,即Ahead-of-Time编译器模式。
Graal(作为编译器)是一个从头开始开发的全新编译器,符合新的JVM接口(JVMCI)。所以,Graal可以与HotSpot集成,但又不受其约束。
我们可以考虑使用Graal在离线模式下对所有方法进行全面编译而不执行代码,而不是使用配置驱动的方式编译热方法。这也就是“Ahead-of-Time编译”(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文件):
$ jaotc --output libjava.base.dylib --module java.base Error: Failed compilation: sun.reflect.misc.Trampoline.invoke(Ljava/lang/reflect/Method;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;: org.graalvm.compiler.java.BytecodeParser$BytecodeParserError: java.lang.Error: Trampoline must not be defined by the bootstrap classloader at parsing java.base@10/sun.reflect.misc.Trampoline.invoke(MethodUtil.java:70) Error: Failed compilation: sun.reflect.misc.Trampoline.<clinit>()V: org.graalvm.compiler.java.BytecodeParser$BytecodeParserError: java.lang.NoClassDefFoundError: Could not initialize class sun.reflect.misc.Trampoline at parsing java.base@10/sun.reflect.misc.Trampoline.<clinit>(MethodUtil.java:50)
我们可以使用编译器指令文件来控制这些错误,从AOT编译中排除掉某些方法(有关详细信息,请参阅 JEP 295 )。
尽管存在编译器错误,我们仍然可以尝试将AOT编译的基本模块代码和用户代码一起运行,如下所示:
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默认的类路径中包含了Graal SDK,但在IDE中需要显式指定,例如:
<dependency> <groupId>org.graalvm</groupId> <artifactId>graal-sdk</artifactId> <version>1.0.0-rc1</version> </dependency>
最简单的例子是Hello World——让我们使用GraalVM默认提供的Javascript实现:
import org.graalvm.polyglot.Context; public class HelloPolyglot { public static void main(String[] args) { System.out.println("Hello World: Java!"); Context context = Context.create(); context.eval("js", "print('Hello World: JavaScript!');"); } }
这在GraalVM上可以按预期运行,但尝试在Java 10上运行时,即使使用了Graal SDK,仍然会产生这个(不足为奇的)错误:
$ java -cp target/classes:$HOME/.m2/repository/org/graalvm/graal-sdk/1.0.0-rc1/graal-sdk-1.0.0-rc1.jar kathik.HelloPolyglot Hello Java! Exception in thread "main" java.lang.IllegalStateException: No language and polyglot implementation was found on the classpath. Make sure the truffle-api.jar is on the classpath. at org.graalvm.polyglot.Engine$PolyglotInvalid.noPolyglotImplementationFound(Engine.java:548) at org.graalvm.polyglot.Engine$PolyglotInvalid.buildEngine(Engine.java:538) at org.graalvm.polyglot.Engine$Builder.build(Engine.java:367) at org.graalvm.polyglot.Context$Builder.build(Context.java:528) at org.graalvm.polyglot.Context.create(Context.java:294) at kathik.HelloPolyglot.main(HelloPolyglot.java:8)
自Java 6以来,随着Scripting API的引入,已经支持多语言。随着Nashorn(基于invokedynamic的JavaScript实现)的出现,Java 8对多语言的支持有了显著增强。
GraalVM的与众不同之处在于,Java生态系统现在明确提供了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会抛出一个运行时异常:
Exception in thread "main" java.lang.IllegalStateException: A language with id 'ruby' is not installed. Installed languages are: [js, llvm]. at com.oracle.truffle.api.vm.PolyglotEngineImpl.requirePublicLanguage(PolyglotEngineImpl.java:559) at com.oracle.truffle.api.vm.PolyglotContextImpl.requirePublicLanguage(PolyglotContextImpl.java:738) at com.oracle.truffle.api.vm.PolyglotContextImpl.eval(PolyglotContextImpl.java:715) at org.graalvm.polyglot.Context.eval(Context.java:311) at org.graalvm.polyglot.Context.eval(Context.java:336) at kathik.HelloPolyglot.main(HelloPolyglot.java:10)
要使用(当前为测试版)Truffle版本的Ruby(或其他语言),需要下载并安装它。对于Graal版本的RC1(很快会推出RC2),可以通过以下方式安装:
gu -v install -c org.graalvm.ruby
要注意,如果GraalVM是在系统级别安装的,则需要sudo。如果使用的是GraalVM的非OSS EE版本(目前Mac上只有这个版本可用),则可以更进一步——可以将Truffle解释器转为本机代码。
为语言重建本机镜像(启动程序)可以提高它的性能,但这需要使用命令行工具,比如(假设GraalVM是安装在系统级别,因此需要root权限):
$ cd $JAVA_HOME $ sudo jre/lib/svm/bin/rebuild-images ruby
这个 工具 还处于开发阶段,所以需要进行一些手动操作,开发团队希望在后续让这个流程变得更加顺畅。
如果在重建本机组件时遇到任何问题,请不要担心——即使不重建本机镜像仍然可以正常使用它。
让我们看一个更复杂的多语言示例:
Context context = Context.newBuilder().allowAllAccess(true).build(); Value sayHello = context.eval("ruby", "class HelloWorld\n" + " def hello(name)\n" + " \"Hello #{name}\"\n" + " end\n" + "end\n" + "hi = HelloWorld.new\n" + "hi.hello(\"Ruby\")\n"); String rubySays = sayHello.as(String.class); Value jsFunc = context.eval("js", "function(x) print('Hello World: JavaScript with '+ x +'!');"); jsFunc.execute(rubySays);
这段代码有点难以阅读,它同时用到了TruffleRuby和JavaScript。首先,我们调用了一段 Ruby 代码:
class HelloWorld def hello(name) "Hello #{name}" end end hi = HelloWorld.new hi.hello("Ruby")
这将创建一个新的Ruby类,并为这个类定义了一个方法,然后实例化了一个Ruby对象,最后调用它的hello()方法。这个方法返回一个(Ruby)字符串,该字符串在Java运行时中被强制转换为Java字符串。
然后我们创建了一个简单的JavaScript匿名函数,如下所示:
function(x) print('Hello World: JavaScript with '+ x +'!');
我们通过execute()调用这个函数,并将Ruby返回的结果传给函数,该函数在JS运行时中将其打印出来。
请注意,我们在创建Context对象时,需要放开该对象的访问权限。这样做是为了Ruby——JS没有这个问题——所以在创建对象时稍微复杂了一些。这是由当前的Ruby实现限制造成的,这个限制将来可能会被移除。
让我们看一个最终的多语言示例:
Value sayHello = context.eval("ruby", "class HelloWorld\n" + " def hello(name)\n" + " \"Hello Ruby: #{name}\"\n" + " end\n" + "end\n" + "hi = HelloWorld.new\n" + "hi"); Value jsFunc = context.eval("js", "function(x) print('Hello World: JS with '+ x.hello('Cross-call') +'!');"); jsFunc.execute(sayHello);
在这个版本中,我们返回一个实际的Ruby对象,而不仅仅是一个字符串。这次我们没有将它强制转换为任何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工程师已经讨论了很长一段时间(至少10年),而随着GraalVM的到来,它向主流迈出了非常重要的一步。
让我们使用这一小段打印Ruby对象的JS代码演示这些外部对象是如何在GraalVM中表示的:
function(x) print('Hello World: JS with '+ x +'!');
输出如下(或类似这样的):
Hello World: JS with foreign {is_a?: DynamicObject@540a903b<Method>, extend: DynamicObject@238acd0b<Method>, protected_methods: DynamicObject@34e20e6b<Method>, public_methods: DynamicObject@15ac59c2<Method>, ...}!
这些输出显示了外部对象被表示为一系列DynamicObject对象,在大多数情况下,它将语义操作委托给对象的主运行时。
在结束本文之前,我们应该谈谈基准和许可。我们必须搞清楚的是,尽管Graal和GraalVM有着巨大的前景,但目前仍处于早期阶段/实验技术阶段。
它尚未针对通用场景进行优化,并且尚需时日才能与HotSpot/C2平起平坐。微基准通常也会产生误导——在某些情况下它们可以指明方向,但对于性能分析来说,只有最终的用户级基准才算数。
我们可以这样想,C2已经最大限度地提升了局部性能,并且即将寿终正寝。Graal让我们有机会突破局部最大化,并转到一个更好的新领域——并且有可能会重新构思我们对VM设计和编译器的许多想法。但它仍然不够成熟,并且不太可能在几年内完全成为主流。
这意味着现在进行的任何性能测试都应该进行谨慎分析。性能测试的比较(特别是HotSpot/C2与GraalVM)是苹果与橙子之间的比较——一个成熟的生产级运行时与一个还处于早期阶段的实验性产品。
还需要指出的是,GraalVM的许可制度可能与迄今为止看到的有所不同。甲骨文在收购Sun公司时,HotSpot已经是非常成熟的产品,并被冠以自由软件许可。他们很少在HotSpot核心产品之上增加价值和进行变现——例如UnlockCommercialFeatures开关。随着这些功能的退出(比如开源Mission Control),可以说,该模型并没有取得巨大的商业成功。
Graal与众不同——它起源于甲骨文Research项目,现在正朝着生产产品的方向发展。甲骨文已投入大量资金让Graal成为现实——该项目所需的人才和团队不足,而且他们都不便宜。因为使用了不同的底层技术,甲骨文可以自由地使用不同的商业许可模型,并尝试基于更广泛的客户群为GraalVM变现——包括那些目前不为HotSpot运行付费的客户。甲骨文甚至可以将GraalVM的某些功能定向提供给甲骨文云客户使用。
目前,甲骨文正在发布一个基于GPL许可的社区版本(CE),它可以免费用于开发和生产用途,以及一个企业版(EE),它可以免费用于开发和评估。这两个版本都可以从甲骨文的 GraalVM网站 下载,其中还可以找到更详细的信息。
关于作者
Ben Evans 是JVM性能优化公司jClarity的联合创始人。他是LJC(伦敦JUG)的组织者,也是JCP执行委员会的成员,帮助定义Java生态系统的标准。Ben是Java Champion、3次JavaOne Rockstar演讲者,“The Well-Grounded Java Developer”、新版“Java in a Nutshell”和“Optimizing Java”的作者。他是Java平台、性能、架构、并发、初创公司和相关主题的演讲常客。Ben有时也接受演讲、教学、写作和咨询活动的邀请,具体可以联系他。
以上所述就是小编给大家介绍的《解密新一代Java JIT编译器Graal》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 华为王成录:解密方舟编译器和EMUI未来四大演进方向!
- LLVM接受NVIDIA的“f18” Fortran编译器作为官方Fortran编译器
- Go 编译器介绍
- Go 编译器介绍
- C++编译器优化
- 编译器笔记与实验记录
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Algorithms Sequential & Parallel
Russ Miller、Laurence Boxer / Charles River Media / 2005-08-03 / USD 59.95
With multi-core processors replacing traditional processors and the movement to multiprocessor workstations and servers, parallel computing has moved from a specialty area to the core of computer scie......一起来看看 《Algorithms Sequential & Parallel》 这本书的介绍吧!