GraalVM替代JVM的方式方法和不足
GraalVM 基本上可以看成是Oracle大力发展的新一代JVM,但是目前的使用情况还不大乐观,而且逐渐发现大家都有往GO语言转的趋势,本文具体分析一下替代方案和不足之处,为什么大家还是更希望使用GO。
GraalVM与JVM关系
JVM 全称 Java 虚拟机,我们都知道,Java 程序是运行在虚拟机上的,虚拟机提供 Java 运行时,支持解释执行和部分的(JIT)即时编译器,并且负责分配和管理 Java 运行所需的内存,我们所说的各种垃圾收集器都工作在 JVM 中。
比如 Oracle JDK、OpenJDK ,默认的 JVM 是 HotSpot 虚拟机。
GraalVM 可以完全取代上面提到的那几种虚拟机,比如 HotSpot。把你之前运行在 HotSpot 上的代码直接平移到 GraalVM 上,不用做任何的改变,甚至都感知不到,项目可以完美的运行。
GraalVM主要想搭建一个Framework,最终目的是实现支持任何一种语言,可以共同跑在 GraalVM 上,不存在跨语言调用的壁垒。
GraalVM 和JDK关系
Java 虚拟机都是内置在 JDK 中的,比如Orcale JDK、OpenJDK,默认内置的都是 HotSpot 虚拟机。
GraalVM 也是一种 JDK,一种高性能的 JDK。完全可以用它替代 OpenJDK、Orcale JDK。
GraalVM 如何运行 Java 程序
- 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 编译器之后,对性能有会有一定的提升。
但是Graal,VM不支持JDK8
第二种:AOT 编译本地可执行程序
AOT 提前编译,是相对于即时编译而言的。AOT在运行过程中耗费 CPU 资源来进行即时编译,而程序也能够在启动的瞬间就达到理想的性能。例如 C 和 C++语言采用的是AOT静态编译,直接将代码转换成机器码执行。而 Java 一直采用的是解释 + 即时编译技术,大多数情况下 Java 即时编译的性能并不比静态编译差,但是还是一直朝着 AOT 编译的方向努力。
但是 Java 对于 AOT 来说有一些难点,比如类的动态加载和反射调用。
GraalVM 的 AOT 编译实际上是借助了 SubstrateVM 编译框架,可以将 SubstrateVM 理解为一个内嵌精简版的 JVM,包含异常处理,同步,线程管理,内存管理(垃圾回收)和 JNI 等组件。
SubstrateVM 的启动时间非常短,内存开销非常少。
除了运行时占用的内存少之外,用这种方式最终生成的可执行文件也非常小。这对于云端部署非常友好。目前很多场景下都使用 Docker容器的方式部署,打一个 Java 程序的镜像包要包含完整的 JVM 环境和编译好的 Jar 包。而AOT 方式可以最大限度的缩小 Docker 镜像的体积。
缺点
对于反射这种纯粹在运行时才能确定的部分,不可能完全通过优化编译器解决,只能通过增加配置的方式解决。
多语言支持
GraalVM 中的另一个核心组件 Truffle是一个用 Java 写就的语言实现框架。基于 Truffle 的语言实现仅需用 Java 实现词法分析、语法分析以及针对语法分析所生成的抽象语法树(Abstract Syntax Tree,AST)的解释执行器,便可以享用由 Truffle 提供的各项运行时优化。
就一个完整的 Truffle 语言实现而言,由于实现本身以及其所依赖的 Truffle 框架部分都是用 Java 实现的,因此它可以运行在任何 Java 虚拟机之上。
当然,如果 Truffle 运行在附带了 Graal 编译器的 Java 虚拟机之上,那么它将调用 Graal 编译器所提供的API,主动触发对 Truffle 语言的即时编译,将对 AST 的解释执行转换为执行即时编译后的机器码。
安装并使用
GraalVm社区版是基于OpenJDK 11.0.17, 17.0.5, 19.0.1,如果你想用免费的,只能将程序升级到 JDK 11 以上了。
对于Linux来说,下载下来的压缩包,直接解压,然后配置环境变量。把解压目录配置到环境变量的 JAVA_HOME
就可以了。
解压好其实就相当于安装完毕了,查看一下版本。
进入到解压目录下的bin
目录中,运行 java -version
。运行结果和平常使用JDK也没什么差别。
代码运行
这里不讲常用的测试环境代码运行方式,无非是做好配置,然后运行。这里主要讲一下关于使用AOT编译为机器码,然后以可执行文件的形式来做:native-image
可以命令行的形式执行,也可以在配合 Maven 执行,这里主要讲用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
编译打包的过程比较慢,因为要直接编译成机器码,所以比一般的编译过程要慢一些。看到下面的输入日志,说明打包成功了。
4、运行可执行程序包,打开 target 目录,已经看到了graalvm-demo-image
可执行程序包了。
然后就可以运行它了,进入到目录下,执行下面的命令运行,可以看到正常输出了。注意了,这时候已经是没有用到本地 JVM 了。
缺点
- 不能直接编译,需要先用
-agentlib
模式运行程序生成编译需要的配置文件,这个过程需要把程序的大部分功能都运行一遍,以免漏掉反射代码导致编译后的程序运行异常。这一步要耗费大量时间精力,非常难受。使用别的技术直接编译代码就可以了,而使用 native-image 还要有这样一个前置步骤,非常不方便。 - 编译速度过慢,比直接用javac编译慢好几倍,体验较差。
- windows上只有 Serial GC(这相当于直接放弃了windows平台),linux上有Serial GC和G1,ZGC之类不支持。(垃圾回收机制根据操作系统平台不同支持不同)
- 编译出来的包比原本的 jvm + jar 加起来还大,导致运维会比较麻烦,网络不畅通或者使用堡垒机会比较难受。
- native-image 编译出来的程序不支持arthas、jprofiler、visualvm等问题排查和性能分析工具,直接导致java程序一大优势丧失。
- 因为不是jit,峰值性能不如java,对于服务器程序来说也不合适
好处就是节省内存、代码不会被破解,但是对于GO来说,这些直接都被取消了,当然GO最麻烦的其实是需要重新学一门语言比较难受。
GO作为一门Native语言,编译速度快到可以在开发阶段考虑实时编译热更新,但是使用quarkus和SpringBoot Native修改一行的编译还需要时间。
最麻烦的可能是一些第三方库,要支持这些第三方库还需要考虑JDK版本的兼容,但是你知道很多东西其实没有一直更新和维护的。
社会环境问题
首先刚研发的新技术是需要一个生态和环境的,我不知道国外生态怎么样啊,但是国内生态都是KPI,在这种环境下新技术的出现如果不能很明显提高生产效率,而实际上去提高运维效率和一些不是很重要的成本(对于企业来说,硬盘和内存条成本不高,反而是人力成本更高),所以在国内这个土壤就不太适应,当然不乏一些技术大牛会看好这项技术的发展,还有一些性价比玩家会考虑用最低配的硬件来实现最高的性能,但是对于市场化的东西来说就没有可接受性。或许得等SpringBoot4上线之后,才能解决这个问题。