在JDK10以前,能进行性能优化的即时编译器只有C2,但C2编译器的代码据说已经变得非常庞大且臃肿,同时伴随着云原生时代的到来,Java这种需要借助JDK才能运行的语言就显得格外臃肿,于是就有了Graal编译器的诞生,Graal编译器提供了非常强悍的能力,所以通常把Graal编译器与HotSpot合称为新一代的虚拟机——GraalVM。
GraalVM是一个高性能JDK发行版,旨在加速用Java和其他语言编写的应用程序的执行,并支持JavaScript、Ruby、Python和许多其他流行语言。
从上面的图也可以看出来GraalVM并不是一个全新的异于HotSpot的虚拟机,而是在HotSpot的基础上,提供了功能非常强大的JIT编译器Graal Compiler。
GraalVM的设计目标是为了可以在不同的环境中运行程序,可以在JVM上运行,也可以直接将程序编译成独立的本地镜像(不需要JDK环境就能运行),或者将Java及本地代码模块集成为更大型的应用。
一、GraalVM下载和安装
GraalVM下载地址:https://www.oracle.com/downloads/graalvm-downloads.html,企业版目前也是免费的,可以使用企业版来学习使用。
选择GraalVM 20的版本,然后JDK版本选择8
在linux服务器上下载完压缩包以后,通过下面的命令解压:
tar -zxf graalvm-ee-java8-linux-amd64-20.3.5.tar.gz
然后配置环境变量
[root@lizhi graalvm-ee-java8-20.3.5]# vi /etc/profile
JAVA_HOME=/usr/local/graalvm/graalvm-ee-java8-20.3.5
PATH=/usr/local/graalvm/graalvm-ee-java8-20.3.5/bin:$PATH
export JAVA_HOME PATH
刷新配置文件,使其立即生效
[root@lizhi graalvm-ee-java8-20.3.5]# source /etc/profile
然后通过java -version
查看
[root@lizhi graalvm-ee-java8-20.3.5]# java -version
java version "1.8.0_321"
Java(TM) SE Runtime Environment (build 1.8.0_321-07)
Java HotSpot(TM) 64-Bit Server VM GraalVM EE 20.3.5 (build 25.321-b07-jvmci-20.3-b28, mixed mode)
使用 GraalVM Enterprise,可以将 Java 字节码编译为特定于平台的、自包含的本机可执行文件(本机映像 Native Image
),以实现更快的启动和更小的应用程序占用空间。
安装命令如下:
[root@lizhi graalvm]# gu install native-image
Downloading: Release index file from oca.opensource.oracle.com
Downloading: Component catalog for GraalVM Enterprise Edition 20.3.5 on jdk8 from oca.opensource.oracle.com
Skipping ULN EE channels, no username provided.
Downloading: Component catalog from www.graalvm.org
Processing Component: Native Image
The component(s) Native Image requires to accept the following license: Oracle GraalVM Enterprise Edition Native Image License
Enter "Y" to confirm and accept all the license(s). Enter "R" to the see license text.
Any other input will abort installation: Y
Downloading: Contents of "Oracle GraalVM Enterprise Edition Native Image License" from oca.opensource.oracle.com
Downloading: Component native-image: Native Image from oca.opensource.oracle.com
Installing new component: Native Image (org.graalvm.native-image, version 20.3.5)
然后通过gu list
命令查看是否已经安装
[root@lizhi graalvm]# gu list
ComponentId Version Component name Origin
--------------------------------------------------------------------------------
js 20.3.5 Graal.js
graalvm 20.3.5 GraalVM Core
native-image 20.3.5 Native Image oca.opensource.oracle.com
二、GraalVM初体验
下面通过一个简单的程序来体验一下GraalVM的强大之处。
创建一个简单的带有main()
方法的类:
public class GraalVMTest {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
然后编译执行
[root@lizhi graalvm]# javac GraalVMTest.java
[root@lizhi graalvm]# ll
total 408752
drwxr-xr-x 10 root root 252 May 25 17:12 graalvm-ee-java8-20.3.5
-rw-r--r-- 1 root root 418550564 Mar 12 14:39 graalvm-ee-java8-linux-amd64-20.3.5.tar.gz
-rw-r--r-- 1 root root 427 May 25 17:18 GraalVMTest.class
-rw-r--r-- 1 root root 123 May 25 17:17 GraalVMTest.java
[root@lizhi graalvm]# java GraalVMTest
Hello World
下面通过native-image
将其打包成一个本地镜像文件
[root@lizhi graalvm]# native-image GraalVMTest
[graalvmtest:1217273] classlist: 3,329.43 ms, 1.14 GB
[graalvmtest:1217273] (cap): 920.18 ms, 1.62 GB
[graalvmtest:1217273] setup: 4,131.87 ms, 1.62 GB
[graalvmtest:1217273] (clinit): 150.62 ms, 1.71 GB
[graalvmtest:1217273] (typeflow): 4,826.96 ms, 1.71 GB
[graalvmtest:1217273] (objects): 3,083.11 ms, 1.71 GB
[graalvmtest:1217273] (features): 173.80 ms, 1.71 GB
[graalvmtest:1217273] analysis: 8,392.21 ms, 1.71 GB
[graalvmtest:1217273] universe: 466.57 ms, 1.71 GB
[graalvmtest:1217273] (parse): 1,589.48 ms, 1.71 GB
[graalvmtest:1217273] (inline): 1,485.74 ms, 1.71 GB
[graalvmtest:1217273] (compile): 17,884.23 ms, 2.08 GB
[graalvmtest:1217273] compile: 21,618.90 ms, 2.08 GB
[graalvmtest:1217273] image: 824.10 ms, 2.08 GB
[graalvmtest:1217273] write: 195.61 ms, 2.08 GB
[graalvmtest:1217273] [total]: 39,220.82 ms, 2.08 GB
[root@lizhi graalvm]# ll
total 412732
drwxr-xr-x 10 root root 252 May 25 17:12 graalvm-ee-java8-20.3.5
-rw-r--r-- 1 root root 418550564 Mar 12 14:39 graalvm-ee-java8-linux-amd64-20.3.5.tar.gz
-rwxr-xr-x 1 root root 4074968 May 25 17:21 graalvmtest
-rw-r--r-- 1 root root 427 May 25 17:18 GraalVMTest.class
-rw-r--r-- 1 root root 123 May 25 17:17 GraalVMTest.java
[root@lizhi graalvm]# ./graalvmtest
Hello World
这样就生成了一个可执行文件,相比于Java的编译过程,打包成镜像文件的过程就很繁琐,即便是这样一个简单的小程序就要近一分钟的时间。这个过程就是将Java字节码编译成特性平台、自包含的本机可执行文件(本地镜像 Native Image),以实现更快的启动和更小应用程序空间。
可以通过time
命令比对一下上面两种程序执行方式的区别:
- 通过JVM来运行
[root@lizhi graalvm]# time java GraalVMTest
Hello World
real 0m0.063s
user 0m0.045s
sys 0m0.020s
- 直接执行镜像文件
[root@lizhi graalvm]# time ./graalvmtest
Hello World
real 0m0.002s
user 0m0.000s
sys 0m0.002s
通过比对就可以发现,通过本地镜像启动程序,启动速度就要快很多,阿里通过这种方式加快容器的启动速度,直接将启动速度提升20倍。
最重要的一点是,这种可执行文件是不需要JDK环境的,所以可以非常便捷的完成快速的容器化部署,这才符合云原生的要求。
将镜像文件拷贝到本地的虚拟机,本地虚拟机没有安装JDK,拷贝过来后还要先授权chmod 777
,然后就可以运行
lizhi@Dog-li:~/test$ java -version
Command 'java' not found, but can be installed with:
sudo apt install default-jre
sudo apt install openjdk-11-jre-headless
sudo apt install openjdk-8-jre-headless
lizhi@Dog-li:~/test$ ./graalvmtest
Hello World
三、C2编译器与Graal编译器的区别
即时编译器是 Java 虚拟机中相对独立的模块,它主要负责接收 Java 字节码,并生成可以直接运行的二进制码。
但在Graal之前,以JDK8为例,即时编译器与Java虚拟机时紧耦合的,也就是说对即时编译器的更改需要重新编译整个Java虚拟机,这对于开发相对活跃的Graal来说是难以接收的。
为了让Java虚拟机与Graal解耦合,引入了Java虚拟机编译器接口(JVM Compiler Interface,JVMCI),将即时编译器的功能抽象成一个Java层面的接口。这样一来,在Graal所依赖的JVMCI版本不变的情况下,我们仅需要替换Graal 译器相关的jar包(Java 9 以后的 jmod 文件),便可完成对Graal的升级
Graal和C2最显著的区别就是:Graal是用Java写的,而C2是用C++写的。相对来说,Graal更加模块化,也更容易开发与维护,毕竟,连C2的开发者都不想去维护C2了。
Graal 的内联算法对新语法、新语言更加友好,例如 Java 8 的 lambda 表达式以及 Scala 语言。
以下面的例子来看这两者对于优化的效率:
public class Test {
public static void main(String[] args) {
String sentence = String.join(" ",args);
long total = 0L,start = System.currentTimeMillis(),last = start;
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10_000_000; j++) {
total += sentence.chars().filter(Character::isUpperCase).count();
if (j % 10_000_000 == 0){
long now = System.currentTimeMillis();
System.out.printf("%d (%d ms)%n",i,now-last);
last = now;
}
}
}
System.out.printf("total: %d (%d ms)%n ",total,System.currentTimeMillis() -start);
}
}
上面这段代码循环内需要运行一千万次,会触发即时编译,所以下面通过对于在JDK8和GraalVM中运行这端代码的耗时:
JDK8:
[root@zhuyuzhu test]# java -version
openjdk version "1.8.0_302"
OpenJDK Runtime Environment (build 1.8.0_302-b08)
OpenJDK 64-Bit Server VM (build 25.302-b08, mixed mode)
[root@zhuyuzhu test]# java Test
0 (74 ms)
1 (717 ms)
2 (638 ms)
3 (616 ms)
4 (607 ms)
5 (600 ms)
6 (624 ms)
7 (594 ms)
8 (597 ms)
9 (621 ms)
total: 0 (6292 ms)
GraalVM:
[root@lizhi graalvm]# java -version
java version "1.8.0_321"
Java(TM) SE Runtime Environment (build 1.8.0_321-07)
Java HotSpot(TM) 64-Bit Server VM GraalVM EE 20.3.5 (build 25.321-b07-jvmci-20.3-b28, mixed mode)
[root@lizhi graalvm]# java Test
0 (62 ms)
1 (124 ms)
2 (448 ms)
3 (16 ms)
4 (16 ms)
5 (15 ms)
6 (16 ms)
7 (15 ms)
8 (16 ms)
9 (15 ms)
total: 0 (759 ms)
从运行结果可以明显的看出来,Graal即时编译器的优化能力要比C2强很多。