解密新一代Java JIT编译器Graal

\

关键要点

\\
  • Java的C2 JIT编译器寿终正寝。\\t
  • 新的JVMCI编译器接口支持可插拔编译器。\\t
  • 甲骨文开发了Graal,一个用Java编写的JIT,作为潜在的编译器替代方案。\\t
  • Graal也可以独立运行,是新平台的主要组件。\\t
  • 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编写编译器有如下的一些优点:

\\
  • 工程师开发新编译器的进入门槛要低得多。\\t
  • 编译器的内存安全性。\\t
  • 能够利用成熟的Java工具进行编译器开发。\\t
  • 更快的新编译器功能原型设计。\\t
  • 编译器可以独立于HotSpot。\\t
  • 编译器能够自己编译自己,以生成更快的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\u0026lt;2_000; i++) {\            timeHashing(i, 'x');\        }\    }\\    void timeHashing(int length, char c) {\        final StringBuilder sb = new StringBuilder();\        for (int j = 0; j \u0026lt; 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 \u0026gt; out.txt
\\

要查看Graal在Java 10上运行的效果:

\\
\java -XX:+PrintCompilation \\\     -XX:+UnlockExperimentalVMOptions \\\     -XX:+EnableJVMCI \\\     -XX:+UseJVMCICompiler \\\     -cp target/classes/ \\\     kathik.StringHash \u0026gt; out-jvmci.txt
\\

对于GraalVM:

\\
\java -XX:+PrintCompilation \\\     -cp target/classes/ \\\     kathik.StringHash \u0026gt; 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.\u0026lt;clinit\u0026gt;()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.\u0026lt;clinit\u0026gt;(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中需要显式指定,例如:

\\
\\u0026lt;dependency\u0026gt;\    \u0026lt;groupId\u0026gt;org.graalvm\u0026lt;/groupId\u0026gt;\    \u0026lt;artifactId\u0026gt;graal-sdk\u0026lt;/artifactId\u0026gt;\    \u0026lt;version\u0026gt;1.0.0-rc1\u0026lt;/version\u0026gt;\\u0026lt;/dependency\u0026gt;
\\

最简单的例子是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\
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值