大家好,我是风筝。公众号「古时的风筝」,专注于后端技术,尤其是 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 的野心不止于此,看上面的图,它的目的是建立一个 Framework,最终的目标是想要支撑任何一种言语,无论哪种言语,能够共同跑在 GraalVM 上,不存在跨言语调用的壁垒。

GraalVM 和JDK有什么联系

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

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

GraalVM 怎么运转 Java 程序

说了半天,是不是仍是不知道 GraalVM 究竟是什么。

  • GraalVM – 还包括 Graal (JIT)即时编译器,能够结合 HotSpot 运用

  • GraalVM – 是一种高功能 JDK,旨在加速 Java 运用程序功能,同时消耗更少的资源。

  • GraalVM – 是一种支撑多言语混编的虚拟机程序,不只能够运转 JVM 系列的言语,也可支撑其他言语。

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 可能就要被它替代了

除了运转时占用的内存少之外,用这种方法最终生成的可履行文件也十分小。这关于云端部署十分友爱。现在许多场景下都运用 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 可能就要被它替代了

现在除了 Java, JavaScript、Ruby、Python 和许多其他盛行言语都现已能够运转在 GraalVM 之上了。

GraalVM 官方还供给了完好的文档,当有一天你开发了一款新的言语,也能够用 Truffle 让它跑在 GraalVM 上。

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

装置和运用

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 支撑 Windows、Linux、MacOS ,能够用指令装置最新版,或许直接下载对应 Java 版别的。

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

我是下载的 Java 11 的版别,下载下来的压缩包,直接解压,然后装备环境变量。把解压目录装备到环境变量的 JAVA_HOME就能够了。

解压好其实就相当于装置结束了,查看一下版别。

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

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

运转代码

常用方法运转

也便是咱们平常一向在用的这种方法,把 GrralVM 作为 OpenJDK 运用,只不过把即时编译器换成了 Graal 。便是前面说的第一种方法。

装置完成后,就能够把它作为正常的 JDK 运用了,比方 javacjpsjmap等都能够直接用了。大多数人仍是用 IDEA 的,所以就直接在 IDEA 中运用就好了。

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

2、创立完成后,翻开项目设置。

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

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

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

4、然后挑选刚刚增加的这个 JDK。

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

5、最终运转一段测试代码。

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

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

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

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 可能就要被它替代了

4、运转可履行程序包,翻开 target 目录,现已看到了graalvm-demo-image可履行程序包了,大小为 11.58M。

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

然后就能够运转它了,进入到目录下,履行下面的指令运转,能够看到正常输出了。注意了,这时候现已是没有用到本地 JVM 了。

./graalvm-demo-image
Hello GraalVM!

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

总结

虽然咱们还没有看到有哪个公司说在用 GraalVM 了,可是 QuarkusSpring BootSpring等许多的结构都现已支撑 GraalVM 的 Native-image 形式,并且在 Orcale 的大力推广下,相信不久之后就会出现在更多的产品中。赶忙体验一下吧。

各位假如觉得有用的话,给个赞吧!最好关注我的公众号「古时的风筝」,等着你来呦

本文正在参加「金石计划 . 分割6万现金大奖」