1. GraalVM诞生的背景
1.1. Java在微服务/云原生时代的困境及解决方案
事实
Java总体上是面向大规模、长时间的服务端应用而设计的。
即时编译器(JIT)、性能优化、垃圾回收等有代表性的特征需要一段时间来达到最佳性能。
矛盾
微服务时代对启动速度达到最高性能的时间提出了新的要求!
在微服务的背景下,提倡服务围绕业务能力构建,不再追求实现上的严谨一致。
单个微服务就不再需要再面对数十、数百GB乃至TB的内存。
有了高可用的服务集群,也无须追求单个服务要7×24小时不可间断地运行,它们随时可以中断和更新。
所以微服务对应用的容器化(Docker)亲和度(包容量、内存消耗等)、启动速度、达到最高性能的时间等方面提出了新的要求,这些恰恰是Java的弱项。
比如:现在启动一个微服务项目(Docker运行6个子服务),动不动就1分钟,如下图:
问题根源
Java离不开虚拟机。
所以Java应用启动的时候,必须要启动虚拟机,进行类加载,无论是启动时间,还是占用空间都不是最优解。
解决方案
革命派
直接革掉Java和Java生态的性命,创造新世界,譬如Golang。
保守派
保留原有主流Java生态和技术资产,朝着微服务、云原生环境靠拢与适应(GraaIVM)。
2. GraalVM入门
GraalVM 是一个高性能 JDK 发行版,旨在加速用Java和其它JVM语言编写的应用程序的执行(JVM不只支持Java语言),并支持 JavaScript、Ruby、Python 和许多其他流行语言(翻译自官网 https://www.graalvm.org/)。
GraalVM底层也是HosSpot,只是做了二次开发,打包成JDK。GraalVM能让支持的语言使用HotSpot虚拟机。
GraalVM可以代替对应的JDK版本使用。
GraalVM想成为一统天下的“最终”虚拟机!而GraalVM要做到原因也很简单:
大部分脚本语言或者有动态特效的语言都需要一个语言虚拟机运行,比如CPython,Lua,Erlang,Java,Ruby,R,JS,PHP,Perl,APL等等,但是这些语言的虚拟机水平很烂,比如CPython的VM就不忍直视,而HotSpotVM是虚拟机的大神级别,如果能用上HotSpot,能用上顶级的即时编译器(JIT)、性能优化、垃圾回收等技术,岂不爽歪歪!
3. GraalVM特征
1. GraalVM是一款高性能的可嵌入式多语言虚拟机,它能运行不同的编程语言
能运行基于JVM的语言,比如Java, Scala, Kotlin和Groovy。
能运行解释型语言,比如JavaScript, Ruby, R和Python。
能运行,能配合LLVM运行的原生语言,比如C, C++, Rust和Swift。
2. GraalVM的设计目标是可以在不同的环境中运行程序在JVM中。
将代码编译成独立的本地镜像(不需要JVM),比如将Java代码编译成本地镜像就可以直接运行,不需要安装JDK环境。(符合云原生的要求不需要在环境里头再安装JDK)
将Java及本地代码模块集成为更大型的应用。
4. 安装配置GraalVM
下载压缩包
GraalVM分成了社区版与企业版,企业版肯定比社区版好,企业版可能涉及到收费问题。
这边因为演示使用社区版就够了,按JDK版本下载GraalVM对应的压缩包,请下载Java 17对应的版本,不然后面实战演示SpringBoot3(需要JDK17)可能会有问题。
下载地址 https://github.com/graalvm/graalvm-ce-builds/releases
下载完后,解压。
配置环境变量
类似这样
新开一个cmd测试,不同版本显示不一样。
安装native-image
cmd下运行gu list,查看有没安装native-image。
- 在线安装
执行gu install native-image 命令安装即可。
2. 离线安装
以 GraalVM for JDK 17 Community 17.0.8 对应的23.0.1 版本为例, 地址:
下载地址:https://github.com/graalvm/graalvm-ce-builds/releases/tag/graal-23.0.1。
找到 native-image-installable-svm-java17-windows-amd64-23.0.1.jar 下载。
打开cmd, 进入 native-image-installable-svm-java17-windows-amd64-23.0.1.jar 下载的文件夹。
执行:gu install -L native-image-installable-svm-java17-windows-amd64-23.0.1.jar 安装即可。
验证运行gu list查看是否安装成功。
注意
1. 如graalvm 和native-image 版本不一致,会出现如下图的错误,找到对应的版本,重新安装即可;
安装Visual Studio Build Tools
因为在Windows上 使用要使用GraalVM的native-image打包需要c++环境,而VisualStudio 可以提供c++开发环境,所以我们要先下载安装好VisualStudio。
打开 visualstudio.microsoft.com这个网站 ,下载Visual Studio Installer。
选择C++桌面开发和Windows 11 SDK,然后进行下载和安装,安装后重启操作系统。
要使用GraalVM的native-image将代码编译成本地文件,不能使用普通的windows自带的命令行窗口。得使用VS提供的 x64 Native Tools Command Prompt for VS版本号。
如下
如果没有可以执行前面Visual Studio Build Tools安装目录文件夹下的
Microsoft Visual Studio\版本号\BuildTools\VC\Auxiliary\Build\vcvars64.bat
**脚本来安装。
安装完之后可以运行x64 Native Tools Command Prompt for VS 版本号 ,去使用native-image命令去进行编译代码生成本地文件了。
如果后续在编译过程中编译失败了,出现以下错误:
那么可以执行cl.exe来进行编译,cl.exe如果是中文,那就得修改为英文。
可以通过再次启用Visual Studio Installer来修改你的安装配置,比如:
可能一开始只选择了中文,手动选择英文,去掉中文,然后安装(修改)即可。
再次检查,这样就可以正常的编译了。
5. Graal Compiler(了解就好)
Graal Compiler是GraalVM与HotSpotVM(从JDK10起)共同拥有的服务端即时编译器,是C2编译器的替代者。
Graal VM结构图如下
Graal Java–就是Graal编译器。
JVMCI—针对JVM编译的接口,因为Graal编译器是java写的,通过Compiler Interface调用JVM很麻烦就使用这个替代。
即时编译器(JIT编译器)是 Java 虚拟机中相对独立的模块,它主要负责接收 Java 字节码,并生成可以直接运行的二进制码。传统情况下(JDK8),即时编译器是与 Java 虚拟机紧耦合的。也就是说,对即时编译器的更改需要重新编译整个 Java 虚拟机。这对于开发相对活跃的 Graal 来说显然是不可接受的。
为了让 Java 虚拟机与 Graal编译器解耦合,引入了Java 虚拟机编译器接口(JVM Compiler Interface,JVMCI),因为Graal编译器是java编写的,将即时编译器的功能抽象成一个 Java 层面的接口。这样一来,在 Graal 所依赖的 JVMCI 版本不变的情况下,我们仅需要替换 Graal 编译器相关的 jar 包(Java 9 以后的 jmod 文件),便可完成对 Graal 的升级。
6. C2编译器的Bug
C2还存在一些小BUG。
例如
packageenjoy.jvm.jit;
/**
* @author King老师
* C2的BUG
* -Xint 参数强制虚拟机运行于只有解释器的编译模式
*/
publicclassC2Bug{publicvoidtest(){int i =8;while((i -=3)>0);System. out .println("i = "+ i);}publicstaticvoidmain(String[] args){C2Bug c2bug =newC2Bug();for(int i =0; i <50_000; i++){//5万次 前1万次 不会触发JIT及时编译(没有性能优化)、后4万次JIT
c2bug.test();}}
}
使用-Xint参数强制虚拟机运行于只有解释器的编译模式,就不会出现问题。
另外把循环次数降低,降低到5000次,就不会触发JIT,就不会触发C2的优化也不会出现问题。
把JDK改成GraalVM,则触发热点探测会使用GraalVM的即时编译器器进行编译优化,也能避免上述Bug。
7. Graal 即时编译器和C2即时编译器区别
Graal 和 C2 最为明显的一个区别是:Graal 是用 Java 写的,而 C2 是用 C++ 写的。
相对来说,Graal 更加模块化,也更容易开发与维护,毕竟,连C2的开发者都不想去维护C2了。
许多人会觉得用 C++ 写的 C2 肯定要比 Graal 快。实际上,在充分预热的情况下,Java 程序中的热点代码早已经通过即时编译转换为二进制码,在执行速度上并不亚于静态编译的 C++ 程序。
Graal 的内联算法对新语法、新语言更加友好,例如 Java 8 的 lambda 表达式以及 Scala 语言。
对比例子
packageenjoy.jvm.jit;
publicclassJavaCountUppercase{staticfinalintITERATIONS=Math.max(Integer.getInteger("iterations",1),1);publicstaticvoidmain(String[] args){String sentence =String.join(" ", args);for(int iter =0; iter <ITERATIONS; iter++){if(ITERATIONS!=1)System.out.println("-- iteration "+(iter +1)+" --");long total =0, start =System.currentTimeMillis(), last = start;for(int i =1; i <100_000_000; i++){
total += sentence.chars().filter(Character::isUpperCase).count();if(i %10_000_000==0){long now =System.currentTimeMillis();System.out.printf("%d (%d ms)%n", i /10_000_000, now - last);
last = now;}}System.out.printf("total: %d (%d ms)%n", total,System.currentTimeMillis()- start);}}
}
使用原生JDK8触发热点探测,使用C2编译器进行即时编译结果如下。
JDK换成GraalVM运行结果如下。
8. GraalVM的AOT技术和实战
8.1. Graal的AOT技术
Graal 的Aot 属于GraalVM 中的一项技术,说白就是Ahead-of-time compile(提前编译),在编译期时,会把需要编译的东西,包含**一个基底的 VM,一起编译成机器码(二进制)**。代码直接编译成机器码后可直接运行,类似C++编译代码一样。
Spring6.0开始也引入了自己的AOT技术,也就是Spring Boot3.0借助AOT技术在GraalVM环境下可以将项目打包成本地文件直接执行。
GraalVM在编译成二进制可执行文件时,GraalVM不会把代码都编译成到二进制文件中。需要确定该应用到底用到了哪些类、其中用到了哪些方法、哪些属性,从而把这些用到的代码编译为机器指令(也就是exe文件)。
比如我们一个应用中某些类可能是动态生成的,也就是应用运行后才生成的,为了解决这个问题,GraalVM提供了配置的方式,可以让我们在编译时告诉GraalVM哪些类是动态生成类需要用到,可以通过proxy-config.json、reflect-config.json来进行配置。
比如在reflect-config.json这样配置告诉GraalVM,代码反射中使用到类有哪些,反射使用的构造方法是什么,该类反射创建对象后会使用反射方法是什么。
Spring Boot3.0可以通过Runtime Hints机制来配置GraalVM的配置文件,也就是通过类方式配置这些信息,GraalVM打包Spring Boot3.0项目成本地文件过程中,会根据这些类写配置信息到GraalVM的配置文件中(这个机制怎么配,这边不说了知道就好)。
8.2. Graal的AOT技术的优缺点
缺点
1. 没法动态生成机器码,不像JAVA可以动态生成类。GraalVM在编译成二进制可执行文件时,需要确定该应用到底用到了哪些类、哪些方法、哪些属性,从而把这些代码编译为机器指令(也就是exe文件)。但是我们一个应用中某些类可能是动态生成的,也就是应用运行后才生成的,为了解决这个问题,GraalVM提供了配置的方式,可以让我们在编译时告诉GraalVM哪些类会动态生成类,比如我们可以通过proxy-config.json、reflect-config.json来进行配置。
2. 编译慢。
优点
1. 项目编译后的文件,是能直接执行的文件,启动和执行速度都很快,只依赖操作系统。
2. 编译时候考虑编译出的文件太大,GraalVM不会把用到的类都编译成到二进制文件中,而是会把用到的代码编译到二进制文件中。
8.3. 实战例子
8.3.1. Hello World实战
新建一个简单的Java工程:
我们可以直接把graalvm当作普通的jdk的使用
打开x64 Native Tools Command Prompt for VS 版本号,进入工程目录下,并利用javac将java文件编译为class文件:javac -d . src/com/zhouyu/App.java。
-d. 这个命令意思根据当前目录找指定的相对路径下的App.java文件进行编译,然后编译后输出的目录是相对路径。根据App.java的package指定的目录。比如我App.java的packge改成com,那么就会输出到当前目录(src目录下)com文件夹下。
此时的class文件因为有main方法,所以用java命令可以运行。
利用native-image来编译生成的字节码文件,转成exe文件。
注意
1. 为啥指定class文件要使用com.zhouyu.App,因为App的package是com.zhouyu。上面命令代码意思是会以当前所在目录为基础,去运行com/zhouyu下的App的class文件。
然后按照class文件的package指定的包路径做为相对路径转化为文件路径去搜索class文件。
2. 可以使用-o参数来指定exe文件的名字:
native-image com.zhouyu.App-o app
编译需要一些时间。
编译完了之后就会在当前目录生成一个exe文件:
可以直接运行这个exe文件
并且运行这个exe文件是不需要操作系统上安装了JDK环境的。
8.3.2. GraalVM下的SpringBoot 3.0实战
新建一个Maven工程,添加SpringBoot3.0相关依赖,就是一个SpringBoot 3.0项目了。
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.0.0</version>
</parent>
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
</dependencies>
配置变量
<properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.build-image.imageName>自己项目名字</spring-boot.build-image.imageName></properties>
要编译成exe需要下面这些插件。这些插件跟Spring AOT有关,同时借助这些插件一键打包项目生成执行本地文件。
<build><plugins><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><version>0.9.28</version></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins>
</build>
创建一些代码
@RestController
publicclassZhouyuController{@AutowiredprivateUserService userService;@GetMapping("/demo")publicStringtest(){return userService.test();}
}
packagecom.zhouyu;
importorg.springframework.stereotype.Component;
@Component
publicclassUserService{publicStringtest(){return"hello zhouyu";}
}
packagecom.zhouyu;
importorg.springframework.boot.SpringApplication;
importorg.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
publicclassMyApplication{publicstaticvoidmain(String[] args){SpringApplication.run(MyApplication.class, args);}
}
这本身就是一个普通的SpringBoot工程,可以使用之前的方式打成jar包运行。
同时也支持用native-image命令把整个SpringBoot工程编译成为一个exe文件执行。在 x64 Native Tools Command Prompt for VS 版本号中,进入到工程目录下,执行mvn -Pnative native:compile(如果报mvn不存在,注意配置下maven的路径到环境变量)进行编译就可以了,这命令包含项目编译成class过程,不需要再编译项目成字节码再去编,就能在项目的target目录下生成对应的exe文件,后续只要运行exe文件就能启动应用了。
在执行命令之前,请确保环境变量中设置了Graal VM 的路径,因为过程需要用到Graal VM 编译成本地文件。
编译完成截图:
如果编译过程中日志没有滚动回车下。
直接运行jar包和直接运行exe启动速度区别。
注意
1. 项目存放目录不要有中文,不然打包编译会报错。
版权归原作者 jsliucode 所有, 如有侵权,请联系我们删除。