一、GraalVM介绍
1、什么是GraalVM?
GraalVM是开发人员编写和执行Java代码的工具。具体来说,GraalVM是由Oracle创建的Java虚拟机(JVM)和Java开发工具包(JDK)。它是一个高性能的运行时,可以提高应用程序的性能和效率。
GraalVM的目标包括:编写一个更快、更易于维护的编译器,提高在JVM上运行的语言的性能,减少应用程序启动时间,将多语言支持集成到Java生态系统中,以及为此提供一组编程工具。
2、为什么要使用GraalVM?
-
GraalVM 与传统的虚拟机不同,它不仅支持 Java 语言,还支持其他编程语言,如 JavaScript、Python、Ruby 和 R 等;
-
GraalVM 使用一种称为“本机图像”的技术,将应用程序编译成本机可执行文件,以提高性能和降低内存使用量;
-
总之,GraalVM 提供了一个全面的解决方案,使开发人员能够在单个虚拟机中构建和运行多种语言应用程序,从而提高应用程序的性能和效率。
3、GraalVM的特点:
-
高性能:Graalvm主要体现在启动高,省内存;
-
云原生:想达到的效果是部署java项目时,不用先安装jdk,直接在目标机器运行;
-
通晓多语言:除了运行基于Java和JVM的语言之外,GraalVM的语言实现框架(Truffle)还可以在JVM上运行JavaScript,Ruby,Python和许多其他流行语言。
4、GraalVM的工作原理:
1)先回顾下HotSpot的工作原理:
启动慢的原因就在于,加载和编译比较慢,class越多,加载编译就越慢,启动就慢;
对于启动速度要求比较高的生产环境就不是很友好,比如云原生中的容器。试想,docker容器已经秒起了,但是里面跑一个jar非常慢,这你受得了吗
所以GraalVM就出现了,提升运行效率。
2)GraalVM的工作原理可以分为以下几个部分:
-
A:即时编译器(JIT) = Just In Time
GraalVM包含一个即时编译器(JIT),JIT编译器可以根据程序的执行情况动态生成最优化的本机代码,从而提高程序的性能:
-
B:提前编译技术(AOT) = Ahead Of Time
除了JIT编译器,GraalVM还包含一种AOT编译器,将运行时的编译提前到了编译时,大大减少启动时间:
-
C:多语言运行环境
可以为多语言提供统一的运行环境,让各个语言相互调用超级简单:
GraalVM可以解释多种编程语言,包括:Java、JavaScript、Python、Ruby等。当一个程序在GraalVM上运行时,解释器会解释程序代码,并将其转换为中间表示形式(IR)。
-
D:本机图像生成器
GraalVM还提供了本机图像生成器,可以将应用程序和所有依赖项编译成单个本机可执行文件,这可以简化应用程序的部署和分发。
二、GraalVM的安装
下载地址:https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-21.3.3.1
我下载的是21.3版本!
1、将下载好的拷贝到虚拟机上/opt/software/目录:
[root@jiguiquan software]# pwd /opt/software [root@jiguiquan software]# ls graalvm-ce-java11-linux-amd64-21.3.3.1.tar.gz
2、解压到/opt/module/目录下并重命名:
[root@jiguiquan software]# tar -zxvf graalvm-ce-java11-linux-amd64-21.3.3.1.tar.gz -C /opt/module/ [root@jiguiquan software]# cd /opt/module/ [root@jiguiquan module]# mv graalvm-ce-java11-21.3.3.1/ graalvm/
3、将graalvm添加到环境变量:
[root@jiguiquan module]# vim /etc/profile.d/my_env.sh # my_env.sh内容如下: export JAVA_HOME=/opt/module/graalvm export PATH=$PATH:$JAVA_HOME/bin export CLASSPATH=.:$JAVA_HOME/jre/lib
是环境变量生效:
[root@jiguiquan graalvm]# source /etc/profile [root@jiguiquan graalvm]# java -version openjdk version "11.0.16" 2022-07-19 OpenJDK Runtime Environment GraalVM CE 21.3.3.1 (build 11.0.16+8-jvmci-21.3-b20) OpenJDK 64-Bit Server VM GraalVM CE 21.3.3.1 (build 11.0.16+8-jvmci-21.3-b20, mixed mode, sharing)
整个过程与普通JDK安装一样!
4、添加native-image命令:
现在离线安装包的地址还是在上面的github页面,下面是我选择的版本:
上传到Linux服务器:
[root@jiguiquan software]# ll -h 总用量 405M -rw-r--r--. 1 root root 391M 3月 17 20:13 graalvm-ce-java11-linux-amd64-21.3.3.1.tar.gz -rw-r--r--. 1 root root 15M 3月 18 00:27 native-image-installable-svm-java11-linux-amd64-21.3.3.1.jar
使用 gu 命令来安装native-image这个命令:
[root@jiguiquan software]# gu -L install native-image-installable-svm-java11-linux-amd64-21.3.3.1.jar Processing Component archive: native-image-installable-svm-java11-linux-amd64-21.3.3.1.jar Installing new component: Native Image (org.graalvm.native-image, version 21.3.3.1) [root@jiguiquan software]# native-image --version GraalVM 21.3.3.1 Java 11 CE (Java Version 11.0.16+8-jvmci-21.3-b20)
三、准备一个java应用的jar包
我使用的是jdk11开发的!
1、代码就是如此简单:
@RestController @RequestMapping("/demo") public class DemoController { @Value("${server.port}") private String port; @GetMapping public ResponseEntity<String> demo(){ return new ResponseEntity<String>("这是一个Demo, 运行的端口是" + port, HttpStatus.OK); } }
2、将上面的代码打成jar包,并拖到linux上以备运行:
[root@jiguiquan jiguiquan]# pwd /opt/jiguiquan [root@jiguiquan jiguiquan]# ll -h 总用量 17M -rw-r--r--. 1 root root 17M 3月 18 00:16 testGraalvm.jar
3、使用java -jar 运行上面的jar包,观察启动时间:
文件大小:17M
耗时:2.1秒
4、使用Graalvm将上面的jar包打包成2进制可执行文件后运行:
[root@jiguiquan jiguiquan]# time native-image -jar testGraalvm.jar [testGraalvm:5025] classlist: 1,999.81 ms, 0.96 GB [testGraalvm:5025] (cap): 572.54 ms, 0.96 GB [testGraalvm:5025] setup: 2,445.48 ms, 0.96 GB [testGraalvm:5025] (clinit): 205.18 ms, 1.21 GB [testGraalvm:5025] (typeflow): 8,077.88 ms, 1.21 GB [testGraalvm:5025] (objects): 11,412.63 ms, 1.21 GB [testGraalvm:5025] (features): 1,107.79 ms, 1.21 GB [testGraalvm:5025] analysis: 21,301.07 ms, 1.21 GB [testGraalvm:5025] universe: 1,155.74 ms, 1.21 GB Warning: Reflection method java.lang.Class.forName invoked at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:46) Warning: Reflection method java.lang.Class.getMethod invoked at org.springframework.boot.loader.jar.JarFileEntries.<clinit>(JarFileEntries.java:66) Warning: Reflection method java.lang.Class.getDeclaredMethod invoked at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:47) Warning: Reflection method java.lang.Class.getDeclaredConstructor invoked at org.springframework.boot.loader.jar.Handler.getFallbackHandler(Handler.java:200) Warning: Aborting stand-alone image build due to reflection use without configuration. Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception [testGraalvm:5025] [total]: 27,272.39 ms, 1.21 GB # Printing build artifacts to: /opt/jiguiquan/testGraalvm.build_artifacts.txt [testGraalvm:5094] classlist: 1,806.33 ms, 0.96 GB [testGraalvm:5094] (cap): 474.96 ms, 0.96 GB [testGraalvm:5094] setup: 2,390.24 ms, 0.96 GB [testGraalvm:5094] (clinit): 204.15 ms, 1.21 GB [testGraalvm:5094] (typeflow): 6,079.19 ms, 1.21 GB [testGraalvm:5094] (objects): 5,207.33 ms, 1.21 GB [testGraalvm:5094] (features): 500.88 ms, 1.21 GB [testGraalvm:5094] analysis: 12,292.61 ms, 1.21 GB [testGraalvm:5094] universe: 967.76 ms, 1.21 GB [testGraalvm:5094] (parse): 986.08 ms, 1.23 GB [testGraalvm:5094] (inline): 1,486.63 ms, 1.22 GB [testGraalvm:5094] (compile): 9,893.42 ms, 1.25 GB [testGraalvm:5094] compile: 13,037.24 ms, 1.25 GB [testGraalvm:5094] image: 1,388.39 ms, 1.25 GB [testGraalvm:5094] write: 564.67 ms, 1.25 GB [testGraalvm:5094] [total]: 32,768.16 ms, 1.25 GB # Printing build artifacts to: /opt/jiguiquan/testGraalvm.build_artifacts.txt Warning: Image 'testGraalvm' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary). real 1m1.629s user 2m55.006s sys 0m36.161s
可以看到编译耗时很久,编译后的文件大小是比编译前小的:
[root@jiguiquan jiguiquan]# ll -h 总用量 28M -rwxr-xr-x. 1 root root 11M 3月 18 00:34 testGraalvm -rw-r--r--. 1 root root 26 3月 18 00:34 testGraalvm.build_artifacts.txt -rw-r--r--. 1 root root 17M 3月 18 00:16 testGraalvm.jar
此时我们来运行下次二进制可执行文件,看看消耗的时间:
文件大小:11M
耗时:1.75秒
感觉提升也不是非常明显,这可能是由于我使用的虚拟机的性能问题,以及项目实在太简单了,没有多少可压缩的空间!
按照官方的数据,整个启动速度最大可以提升2个数量级!
四、验证此二进制文件是否可以脱离JVM运行——不能
1、现象——不能:
[root@node3 ~]# java -version -bash: java: command not found [root@node3 ~]# ./testGraalvm Error: No bin/java and no environment variable JAVA_HOME
2、原因分析:反射的存在
我们回顾上面编译时候的结果输出的Warning:
Warning: Image 'testGraalvm' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary).
说的很清楚:这个包是个“降级镜像”,何为“降级”:即GraalVM将此二进制文件降级成基于JVM运行的镜像!
如果我们强行通过参数的方式,禁用降级行为 –no-fallback 会不会有效呢,实验结果是打包可以正常完成,但是运行时会报错!
那是因为我们的程序中存在反射,编译时无法知道哪些方法是会被最终调用的,就会认为这些方法是不可达的,所以就不会被打包进来,最终程序就没办法正常运行;
对于简单的程序,我们可以通过在编译目录下,新建一个 reflect-config.json 文件:
[ { "name": "HelloReflection", "methods": [{"name":"foo", "parameterTypes": []}] } ]
但是对于负责的框架来说,这显然是不可能完成的工作!
3、解决办法:Spring Native + 编译插件:
首先需要说明一下,Spring Native目前还属于实验特性,最新Beta版本为0.12.1,还没有推出稳定的1.0版本(按照官方预期是2022年内会推出),需要Spring Boot最低版本是2.6.6,后续Spring Boot 3.0中也会默认支持Native Image。
https://github.com/spring-attic/spring-native
-
那么,Spring Native给我们带来了什么呢?
首先是Spring框架的Native化支持,包括IOC、AOP等各种Spring组件及能力的Native支持;其次是Configuration支持,允许通过@NativeHint注解来动态生成Native Image Configuration(reflect-config.json, proxy-config.json等);最后就是Maven Plugin,可以通过Maven构建获得Native Image,而不需要再手动去执行native-image命令
五、Spring Native + 编译插件的使用(Springboot2.5.7)
Springboot3.0以上的版本引入Spring Native比较方便,在start.spring.io就可以直接引入依赖,但是Springboot3最低支持JDK17^_^;
<3.0的版本,在配置时,有一点麻烦,而且经常遇到版本不兼容,导致编译失败;
1、引入依赖:
首先确保Spring Boot的版本在2.6.6以上,然后在一个基础Spring Boot项目的基础上,引入以下依赖:
<dependency> <groupId>org.springframework.experimental</groupId> <artifactId>spring-native</artifactId> <version>0.10.4</version> </dependency>
2、在build标签中引入plugin:
<plugin> <groupId>org.springframework.experimental</groupId> <artifactId>spring-aot-maven-plugin</artifactId> <version>0.10.4</version> <executions> <execution> <id>generate</id> <goals> <goal>generate</goal> </goals> </execution> <execution> <id>test-generate</id> <goals> <goal>test-generate</goal> </goals> </execution> </executions> </plugin>
3、指定native build的profile:
<profiles> <profile> <id>native</id> <dependencies> <!-- Required with Maven Surefire 2.x --> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-launcher</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> <version>0.9.8</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> <!-- ... --> </configuration> </plugin> <!-- Avoid a clash between Spring Boot repackaging and native-maven-plugin --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <classifier>exec</classifier> </configuration> </plugin> </plugins> </build> </profile> </profiles>
4、如果依赖的jar包或者plugin下载不下来,可以添加Spring的官方仓库:
<repositories> <repository> <id>spring-release</id> <name>Spring release</name> <url>https://repo.spring.io/release</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-release</id> <name>Spring release</name> <url>https://repo.spring.io/release</url> </pluginRepository> </pluginRepositories>
5、将此代码拷贝到Linux上进行运行打包:
为什么不在Windows上运行,是因为Window生成的二进制可执行文件和Linux上的二进制可执行文件是互相不兼容的!
mvn clean package -DskipTests -Pnative
此命令编译成功后,会在target目录下生成我们需要的二进制可执行文件:
[root@jiguiquan TestGraalvm]# ll -h target/ |grep TestGraalvm -rwxr-xr-x. 1 root root 66M 3月 21 09:17 TestGraalvm -rw-r--r--. 1 root root 26 3月 21 09:17 TestGraalvm.build_artifacts.txt
而且,此二进制文件,是可以脱离JRE环境的:
[root@jiguiquan TestGraalvm]# java -version -bash: java: 未找到命令 [root@jiguiquan target]# ./TestGraalvm 2023-03-21 12:58:28.584 INFO 6245 --- [ main] o.s.nativex.NativeListener : This application is bootstrapped with code generated with Spring AOT . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.5.7) 2023-03-21 12:58:28.586 INFO 6245 --- [ main] c.jiguiquan.www.TestGraalvmApplication : Starting TestGraalvmApplication v1.0 using Java 11.0.16 on jiguiquan with PID 6245 (/opt/code/TestGraalvm/target/TestGraalvm started by root in /opt/code/TestGraalvm/target) 2023-03-21 12:58:28.586 INFO 6245 --- [ main] c.jiguiquan.www.TestGraalvmApplication : No active profile set, falling back to default profiles: default 2023-03-21 12:58:28.630 INFO 6245 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 9090 (http) 2023-03-21 12:58:28.630 INFO 6245 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2023-03-21 12:58:28.630 INFO 6245 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.55] 2023-03-21 12:58:28.633 INFO 6245 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2023-03-21 12:58:28.633 INFO 6245 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 45 ms 2023-03-21 12:58:28.661 INFO 6245 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9090 (http) with context path '' 2023-03-21 12:58:28.661 INFO 6245 --- [ main] c.jiguiquan.www.TestGraalvmApplication : Started TestGraalvmApplication in 0.105 seconds (JVM running for 0.116)
可以看到,此二进制文件运行不依赖于JVM,而且启动速度飞快,只需要0.1秒!
(注意,我的Springboot版本修改为了2.5.7)
编译过程中可能出现的问题:
问题一、缺少gcc编译环境:
很简单,一股脑安装下gcc编译环境:
yum install git gcc gcc-c++ make automake autoconf libtool pcre pcre-devel zlib zlib-devel openssl-devel wget vim -y