关键要点
\\
- 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\