大家好,我是风筝,公众号「古时的风筝」,专注于 Java技术 及周边生态。
文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面。

今天说一说 GraalVM。

GraalVM 是 Oracle 大力发展和想要推广的新一代 JVM ,目前很多框架都已经渐渐支持 GraalVM 了,比如我们在用的 Spring 也已经推出了对 GraalVM 兼容的工具包了。

既然说的这么厉害,那么它到底是何方神圣呢。

GraalVM 和 JVM 的关系

既然叫做VM,那肯定和 JVM 有关系的吧。JVM 全称 Java 虚拟机,我们都知道,Java 程序是运行在虚拟机上的,虚拟机提供 Java 运行时,支持解释执行和部分的(JIT)即时编译器,并且负责分配和管理 Java 运行所需的内存,我们所说的各种垃圾收集器都工作在 JVM 中。

比如 Oracle JDK、OpenJDK ,默认的 JVM 是 HotSpot 虚拟机,这是当前应用最广泛的一个虚拟机。我们平时见到的各种将虚拟机的书籍、文章、面试题,基本上都是说的 HotSpot 虚拟机。

除此之外,还有一些商用,或者说小众的虚拟机存在,比如IBM 的J9 JVM,商用的 Zing VM 等。

那 GraalVM 是另一种 Java 虚拟机吗?

是,又不全是。

GraalVM 可以完全取代上面提到的那几种虚拟机,比如 HotSpot。把你之前运行在 HotSpot 上的代码直接平移到 GraalVM 上,不用做任何的改变,甚至都感知不到,项目可以完美的运行。

但是 GraalVM 还有更广泛的用途,不仅支持 Java 语言,还支持其他语言。这些其他语言不仅包括嫡系的 JVM 系语言,例如 Kotlin、Scala,还包括例如 JavaScript、Nodejs、Ruby、Python 等。

过两年 JVM 可能就要被 GraalVM 替代了

GraalVM 的野心不止于此,看上面的图,它的目的是搭建一个 Framework,最终的目标是想要支持任何一种语言,无论哪种语言,可以共同跑在 GraalVM 上,不存在跨语言调用的壁垒。

GraalVM 和JDK有什么关系

Java 虚拟机都是内置在 JDK 中的,比如Orcale JDK、OpenJDK,默认内置的都是 HotSpot 虚拟机。

GraalVM 也是一种 JDK,一种高性能的 JDK。完全可以用它替代 OpenJDK、Orcale JDK。

GraalVM 如何运行 Java 程序

说了半天,是不是还是不知道 GraalVM 到底是什么。

GraalVM 提供了两种方式来运行 Java 程序。

第一种:结合 HotSpot 使用

上面说了,GraalVM 包含 Graal (JIT)即时编译器,自从 JDK 9u 版本之后,Orcale JDK 和 OpenJDK 就集成了 Graal 即时编译器。我们知道 Java 既有解释运行也有即时编译。

当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。即时编译器可以选择性地编译热点代码,省去了很多编译时间,也节省很多的空间。比如多次执行的方法或者循环、递归等。

JDK 默认使用的是 C2 即时编译器,C2是用C++编写的。而使用下面的参数可以用 Graal 替换 C2。

-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler

Graal 编译器是用 Java 实现的,用 Java 实现自己的编译器。Graal 基于一些假设的条件,采取更加激进的方式进行优化。采用 Graal 编译器之后,对性能有会有一定的提升。

但是如果你还是在用 JDK8,那对不起了,GraalVM 的一切都用不了。

第二种:AOT 编译本地可执行程序

这是 GraalVM 真正厉害的地方。

AOT 提前编译,是相对于即时编译而言的。AOT在运行过程中耗费 CPU 资源来进行即时编译,而程序也能够在启动的瞬间就达到理想的性能。例如 C 和 C++语言采用的是AOT静态编译,直接将代码转换成机器码执行。而 Java 一直采用的是解释 + 即时编译技术,大多数情况下 Java 即时编译的性能并不比静态编译差,但是还是一直朝着 AOT 编译的方向努力。

但是 Java 对于 AOT 来说有一些难点,比如类的动态加载和反射调用。

GraalVM 显然是已经克服了这些问题,使用 GraalVM 可以直接将 Java 代码编译成本地机器码形态的可执行程序。

我们目前运行 Java 一定要安装 JDK 或者 JRE 对不对,如果将程序直接编译成可执行程序,就不用在服务器上安装 JDK 或 JRE 了。那就是说运行 Java 代码其实也可以不用虚拟机了是吗?

GraalVM 的 AOT 编译实际上是借助了 SubstrateVM 编译框架,可以将 SubstrateVM 理解为一个内嵌精简版的 JVM,包含异常处理,同步,线程管理,内存管理(垃圾回收)和 JNI 等组件。

SubstrateVM 的启动时间非常短,内存开销非常少。用这种方式编译出的 Java 程序的执行时间可与C语言持平。

下图是使用即时编译(JVM运行)与 AOT (原生可执行程序)两种方式的 CPU 和内存使用情况对比,可以看出来,AOT 方式下 CPU 和内存的使用都非常少。

过两年 JVM 可能就要被 GraalVM 替代了

除了运行时占用的内存少之外,用这种方式最终生成的可执行文件也非常小。这对于云端部署非常友好。目前很多场景下都使用 Docker 容器的方式部署,打一个 Java 程序的镜像包要包含完整的 JVM 环境和编译好的 Jar 包。而AOT 方式可以最大限度的缩小 Docker 镜像的体积。

缺点

好处多多,当然也有一些弊端。对于反射这种纯粹在运行时才能确定的部分,不可能完全通过优化编译器解决,只能通过增加配置的方式解决。麻烦是麻烦了一点,但是是可行的,Spring Boot 2.7的版本已经支持原生镜像了,Spring 这种非常依赖反射的框架都可以支撑,我们用起来也应该没问题。

GraalVM 如何支持多语言

要支持多语言,就要说到 GraalVM 中的另一个核心组件 Truffle 了。

Truffle 是一个用 Java 写就的语言实现框架。基于 Truffle 的语言实现仅需用 Java 实现词法分析、语法分析以及针对语法分析所生成的抽象语法树(Abstract Syntax Tree,AST)的解释执行器,便可以享用由 Truffle 提供的各项运行时优化。

就一个完整的 Truffle 语言实现而言,由于实现本身以及其所依赖的 Truffle 框架部分都是用 Java 实现的,因此它可以运行在任何 Java 虚拟机之上。

当然,如果 Truffle 运行在附带了 Graal 编译器的 Java 虚拟机之上,那么它将调用 Graal 编译器所提供的 API,主动触发对 Truffle 语言的即时编译,将对 AST 的解释执行转换为执行即时编译后的机器码。

过两年 JVM 可能就要被 GraalVM 替代了

目前除了 Java, JavaScript、Ruby、Python 和许多其他流行语言都已经可以运行在 GraalVM 之上了。

GraalVM 官方还提供了完整的文档,当有一天你开发了一款新的语言,也可以用 Truffle 让它跑在 GraalVM 上。

过两年 JVM 可能就要被 GraalVM 替代了

安装和使用

GraalVm 目前的最新版本是 22.3,分为社区版和企业版,就好像 OpenJDK 和 商用的 Orcale 的 JDK ,企业版会多一些性能分析的功能,用来帮助更大程度的优化性能。

社区版是基于OpenJDK 11.0.17, 17.0.5, 19.0.1,而商业版基于Oracle JDK 8u351, 11.0.17, 17.0.5, 19.0.1,所以,如果你想用免费的,只能将程序升级到 JDK 11 以上了。

过两年 JVM 可能就要被 GraalVM 替代了

GraalVM 支持 Windows、Linux、MacOS ,可以用命令安装最新版,或者直接下载对应 Java 版本的。

过两年 JVM 可能就要被 GraalVM 替代了

我是下载的 Java 11 的版本,下载下来的压缩包,直接解压,然后配置环境变量。把解压目录配置到环境变量的 JAVA_HOME就可以了。

解压好其实就相当于安装完毕了,查看一下版本。

进入到解压目录下的bin目录中,运行 java -version。运行结果如下:

过两年 JVM 可能就要被 GraalVM 替代了

运行代码

常用方式运行

也就是我们平时一直在用的这种方式,把 GrralVM 当做 OpenJDK 使用,只不过把即时编译器换成了 Graal 。就是前面说的第一种方式。

安装完成后,就可以把它当做正常的 JDK 使用了,比如 javacjpsjmap等都可以直接用了。大多数人还是用 IDEA 的,所以就直接在 IDEA 中使用就好了。

1、先随意创建一个 Java 项目。

2、创建完成后,打开项目设置。

过两年 JVM 可能就要被 GraalVM 替代了

3、在打开的项目设置弹出框中选择 SDKs,点击加号,选择前面解压的 GraalVM 目录。

过两年 JVM 可能就要被 GraalVM 替代了

4、然后选择刚刚添加的这个 JDK。

过两年 JVM 可能就要被 GraalVM 替代了

5、最后运行一段测试代码。

public class HelloWorld {
    public static void main(String[] args) throws Exception {
        System.out.println("Hello GraalVM!");
        Thread.sleep(1000 * 100 * 100);
    }
}

过两年 JVM 可能就要被 GraalVM 替代了

上面这样的运行方式,其实就相当于前面说的第一种运行方式

native-image 方式运行

这种方式就是 AOT 编译成机器码,已可执行文件的形式出现。native-image 可以命令行的形式执行,也可以在配合 Maven 执行,我这儿就直接演示用 Maven 形式的了,毕竟IDEA 搭配 Maven 用习惯了。

1、安装native-image 工具包

native-image 是用来进行 AOT 编译打包的工具,先把这个装上,才能进行后面的步骤。

安装好 GraalVM 后,在 bin目录下有一个叫做 gu的工具,用这个工具安装,如果将 bin目录添加到环境中,直接下面的命令安装就行了。

gu install native-image

如果没有将 bin目录加到环境变量中,要进入到 bin目录下,执行下面的命令安装。

./gu install native-image

这个过程可能比较慢,因为要去 github 上下载东西,如果一次没成功(比如超时),多试两次就好了。

2、配置 Maven

配置各种版本

 <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>${java.specification.version}		</maven.compiler.source>
    <maven.compiler.target>${java.specification.version}</maven.compiler.target>
    <native.maven.plugin.version>0.9.12</native.maven.plugin.version>
    <imageName>graalvm-demo-image</imageName>
    <mainClass>org.graalvm.HelloWorld</mainClass>
  </properties>

native.maven.plugin.version是要用到的编译为可执行程序的 Maven 插件版本。

imageName是生成的可执行程序的名称。

mainClass是入口类全名称。

配置 build 插件

  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>3.0.0</version>
        <executions>
          <execution>
            <id>java-agent</id>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <executable>java</executable>
              <workingDirectory>${project.build.directory}</workingDirectory>
              <arguments>
                <argument>-classpath</argument>
                <classpath/>
                <argument>${mainClass}</argument>
              </arguments>
            </configuration>
          </execution>
          <execution>
            <id>native</id>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <executable>${project.build.directory}/${imageName}</executable>
              <workingDirectory>${project.build.directory}</workingDirectory>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>${maven.compiler.source}</source>
          <target>${maven.compiler.source}</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.2</version>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>true</addClasspath>
              <mainClass>${mainClass}</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>true</addClasspath>
              <mainClass>${mainClass}</mainClass>
            </manifest>
          </archive>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
        </configuration>
      </plugin>
    </plugins>
  </build>

配置 profiles

  <profiles>
    <profile>
      <id>native</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.graalvm.buildtools</groupId>
            <artifactId>native-maven-plugin</artifactId>
            <version>${native.maven.plugin.version}</version>
            <extensions>true</extensions>
            <executions>
              <execution>
                <id>build-native</id>
                <goals>
                  <goal>build</goal>
                </goals>
                <phase>package</phase>
              </execution>
              <execution>
                <id>test-native</id>
                <goals>
                  <goal>test</goal>
                </goals>
                <phase>test</phase>
              </execution>
            </executions>
            <configuration>
              <fallback>false</fallback>
              <buildArgs>
                <arg>-H:DashboardDump=fortune -H:+DashboardAll</arg>
              </buildArgs>
              <agent>
                <enabled>true</enabled>
                <options>
                  <option>experimental-class-loader-support</option>
                </options>
              </agent>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>

3、使用 maven 编译,打包成本地可执行程序。

执行 Maven 命令

mvn clean package

或者

mvn  -Pnative -Dagent package

编译打包的过程比较慢,因为要直接编译成机器码,所以比一般的编译过程要慢一些。看到下面的输入日志,说明打包成功了。

过两年 JVM 可能就要被 GraalVM 替代了

4、运行可执行程序包,打开 target 目录,已经看到了graalvm-demo-image可执行程序包了,大小为 11.58M。

过两年 JVM 可能就要被 GraalVM 替代了

然后就可以运行它了,进入到目录下,执行下面的命令运行,可以看到正常输出了。注意了,这时候已经是没有用到本地 JVM 了。

./graalvm-demo-image
Hello GraalVM!

这时候,用 jps -l命令已经看不到这个进程了,只能通过 ps看了。

总结

虽然我们还没有看到有哪个公司说在用 GraalVM 了,但是 QuarkusSpring BootSpring等很多的框架都已经支持 GraalVM 的 Native-image 模式,而且在 Orcale 的大力推广下,相信不久之后就会出现在更多的产品中。赶紧体验一下吧。


如果觉得还不错的话,给个推荐吧!

公众号「古时的风筝」,Java 开发者,专注 Java 及周边生态。坚持原创干货输出,你可选择现在就关注我,或者看看历史文章再关注也不迟。长按二维码关注,跟我一起变优秀!

过两年 JVM 可能就要被 GraalVM 替代了