GraalVM——云原生时代的JVM

一、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的工作原理:

1679366445110714.png

  • 启动慢的原因就在于,加载和编译比较慢,class越多,加载编译就越慢,启动就慢;

  • 对于启动速度要求比较高的生产环境就不是很友好,比如云原生中的容器。试想,docker容器已经秒起了,但是里面跑一个jar非常慢,这你受得了吗

所以GraalVM就出现了,提升运行效率。

    2)GraalVM的工作原理可以分为以下几个部分:

  • A:即时编译器(JIT) = Just In Time

    GraalVM包含一个即时编译器(JIT),JIT编译器可以根据程序的执行情况动态生成最优化的本机代码,从而提高程序的性能:

    1679366721122842.png

  • B:提前编译技术(AOT) = Ahead Of Time

    除了JIT编译器,GraalVM还包含一种AOT编译器,将运行时的编译提前到了编译时,大大减少启动时间:

    1679366792887915.png

  • C:多语言运行环境

    可以为多语言提供统一的运行环境,让各个语言相互调用超级简单:

    1679366922428374.png

    GraalVM可以解释多种编程语言,包括:Java、JavaScript、Python、Ruby等。当一个程序在GraalVM上运行时,解释器会解释程序代码,并将其转换为中间表示形式(IR)。

  • D:本机图像生成器

    GraalVM还提供了本机图像生成器,可以将应用程序和所有依赖项编译成单个本机可执行文件,这可以简化应用程序的部署和分发。


二、GraalVM的安装

官网:https://www.graalvm.org/

下载地址:https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-21.3.3.1

我下载的是21.3版本!

1679041936226512.png

1679042009742021.png

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页面,下面是我选择的版本:

1679308944279766.png

上传到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开发的!

1679307729712179.png

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包,观察启动时间:

1679308484701465.png

文件大小: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

此时我们来运行下次二进制可执行文件,看看消耗的时间:

1679309920423137.png

文件大小: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编译环境:

1679309704662921.png

很简单,一股脑安装下gcc编译环境:

yum install git gcc gcc-c++ make automake autoconf libtool pcre pcre-devel zlib zlib-devel openssl-devel wget vim -y

jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐