新的Java JIT编译器Graal简介

18-11-19 banq
         

在本教程中,我们将深入研究名为Graal的新Java实时(JIT)编译器。

让我们首先解释JIT编译器的作用。

当我们编译Java程序时(例如,使用  javac命令),我们最终将源代码编译成代码的二进制表示 - 一个JVM字节码。这个字节码比我们的源代码更简单,更紧凑,但我们计算机中的传统处理器无法执行它。

为了能够运行Java程序,JVM解释字节码。由于解释器通常比在真实处理器上执行的本机代码慢得多,因此  JVM可以运行另一个编译器,该编译器现在将我们的字节码编译成可由处理器运行的机器代码。这种所谓的即时编译器比javac编译器复杂得多,它运行复杂的优化以生成高质量的机器代码。

Oracle的JDK实现基于开源OpenJDK项目。这包括从Java 1.3版开始提供的HotSpot虚拟机。它包含两个传统的JIT编译器:客户端编译器,也称为C1和服务器编译器,称为opto或C2。

C1设计为运行速度更快,产生的优化代码更少,而另一方面,C2需要更多的时间来运行,但产生更好的优化代码。客户端编译器更适合桌面应用程序,因为我们不希望长时间暂停JIT编译。对于可能在编译上花费更多时间的长时间运行的服务器应用程序,服务器编译器更好。

1. 分层编译

Java安装在正常程序执行期间使用两个JIT编译器。

正如我们前面提到的,由javac编译的Java程序以解释模式开始执行。JVM跟踪每个经常调用的方法并编译它们,为此,它使用C1进行编译;但是,HotSpot仍然关注这些方法的未来调用。如果调用次数增加,JVM将再次重新编译这些方法,但这次使用C2。

这是HotSpot使用的默认策略,称为分层编译。

2.服务器编译器

现在让我们关注C2,因为它是两者中最复杂的。C2已经过极度优化,可生成可与C ++竞争或更快的代码。服务器编译器本身是用C ++的特定方言编写的。

但是,它带来了一些问题。由于C ++中可能存在分段错误,因此可能导致VM崩溃。此外,在过去几年中,编译器没有实现重大改进。C2中的代码变得难以维护,因此我们无法期望使用当前设计进行新的主要增强。考虑到这一点,新的JIT编译器正在名为GraalVM的项目中创建。

GraalVM

Project GraalVM是Oracle创建的一个研究项目,目标是完全替换HotSpot。我们可以将Graal视为几个连接项目:HotSpot的新JIT编译器和新的多语言虚拟机。它提供了一个全面的生态系统,支持大量语言(Java和其他基于JVM的语言; JavaScript,Ruby,Python,R,C / C ++和其他基于LLVM的语言)。

我们当然会关注Java。

1. Graal - 用Java编写的JIT编译器

Graal是一个高性能的JIT编译器。 它接受JVM字节码并生成机器代码。

在Java中编写编译器有几个关键优势。首先,安全,意味着没有崩溃,但异常反而没有真正的内存泄漏。此外,我们将获得良好的IDE支持,我们将能够使用调试器或分析器或其他方便的工具。此外,编译器可以独立于HotSpot,它可以生成更快的JIT编译版本。

Graal编译器的创建考虑了这些优点。 它使用新的JVM编译器接口--JVMCI与VM通信。要启用新JIT编译器,我们需要在从命令行运行Java时设置以下选项:

-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler

这意味着我们可以用三种不同的方式运行一个简单的程序:使用常规的分层编译器,使用Java 10上的JVMCI版本或GraalVM本身。

2. JVMCI:JVM编译器接口

自JDK 9以来,JVMCI是OpenJDK的一部分,因此我们可以使用任何标准的OpenJDK或Oracle JDK来运行Graal。

JVMCI实际允许我们做的是排除标准的分层编译并插入我们全新的编译器(即Graal),而无需更改JVM中的任何内容。

界面非常简单。当Graal编译一个方法时,它会传递该方法的字节码作为JVMCI的输入。作为输出,我们将获得编译的机器代码。输入和输出都只是字节数组:

interface JVMCICompiler {
    byte[] compileMethod(byte[] bytecode);
}

在现实场景中,我们通常需要更多信息,例如局部变量的数量,堆栈大小以及从解释器中的分析中收集的信息,以便我们知道代码在实践中如何运行。

本质上,当调用JVMCICompiler  接口的  compileMethod()时  ,我们需要传递  CompilationRequest  对象。然后它将返回我们想要编译的Java方法,在该方法中,我们将找到所需的所有信息。

3. Graal本身

Graal本身由VM执行,所以它首先被解释并在它变热时进行JIT编译。让我们看一个例子,也可以在  GraalVM的官方网站上找到。编译这个例子:

javac CountUppercase.java
java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler

输出:

1 (1581 ms)

2 (480 ms)

3 (364 ms)

4 (231 ms)

5 (196 ms)

6 (121 ms)

7 (116 ms)

8 (116 ms)

9 (116 ms)

total: 59999994 (3436 ms)

我们可以看到一开始需要更多时间。预热时间取决于各种因素,例如应用程序中的多线程代码量或VM使用的线程数。如果内核较少,则预热时间可能会更长。

如果不使用JVMCI编译器:

java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:-UseJVMCICompiler 
1 (510 ms)
2 (375 ms)
3 (365 ms)
4 (368 ms)
5 (348 ms)
6 (370 ms)
7 (353 ms)
8 (348 ms)
9 (369 ms)
total: 59999994 (4004 ms)

可以缩短初始时间。

Ahead-of-Time编译器模式

我们也可以在Java 10中的Ahead-of-Time编译器模式中使用Graal编译器。正如我们已经说过的那样,Graal编译器是从头开始编写的。它符合一个新的干净界面JVMCI,它使我们能够将它与HotSpot集成。

使用方法是使用配置文件驱动的方法来仅编译热方法,但我们也可以使用Graal在脱机模式下对所有方法进行全面编译而不执行代码。这是所谓的“Ahead-of-Time Compilation”,JEP 295,  但我们不会深入研究AOT编译技术。

我们以这种方式使用Graal的主要原因是为了加快启动时间,直到HotSpot中的常规分层编译方法可以接管为止。