GraalVM安装和基本使用
GraalVM是Oracle出品的,一个想要统一天下的虚拟机,最主要的是它支持AOT模式,今天就来体验一下吧。
进入下载页面,选择一个合适的版本下载,这里下载的是windows平台,java17的64位ce版本。
下载完之后,解压到你想要放的目录,可以使用以下指令加入到环境变量。
# 打开cmd
setx /M PATH "D:\SDK\JDK\graalvm-ce-java17-22.0.0.2\bin;%PATH%"
setx /M JAVA_HOME "D:\SDK\JDK\graalvm-ce-java17-22.0.0.2"
# 执行完以上指令后,关闭cmd窗口后再重新打开,检查环境变量是否修改成功。
echo %PATH%
echo %JAVA_HOME%
搭建 Native Image 环境
这次主要是想体验下native,所以先搭建naive环境。
如果配置了环境的话,可以跳过这个环节,如果没有的话,可以进入目录的 bin 文件夹下执行。
gu install native-image
如果是Linux,想要编译native镜像,还需要 glibc-devel
,zlib-devel
和 gcc
。
# 如果是Oracle Linux,使用yum安装
sudo yum install glibc-devel zlib-devel gcc
# 如果是 Ubuntu Linux,使用 apt-get 安装
sudo apt-get install build-essential libz-dev zlib1g-dev
# 如果是其他Linux发行版,可以使用费 dnf 包管理工具安装
sudo dnf install glibc-devel zlib-devel libstdc++-static
如果是macOS,使用 xcode
xcode-select --install
如果是 windows 的话,要安装 Visual Studio 和 微软的 Visual C++ (MSVC)。注意要安装 Visual Studio Build Tools 是 Windows 10 SDK。
而且,构建 native image 时,需要工作在 x64 Native Tools Command Prompt 环境下工作。
环境搭建好后,就可以使用 native-image 命令进行编译了。
Windows使用native-image
搜索 x64 Native Tools Command Prompt
,然后打开,进入 graalVM 的 bin 目录。
为了方便,可以把 x64 Native Tools Command Prompt
配置到 Windows Terminal。
简单写一个Java类进行测试。
package org.lgq;
public class HelloLGQ {
public static void main(String[] args) {
System.out.println("Hello, DevLGQ!");
}
}
然后使用javac编译成class
文件。
# 编译
./javac .\org\lgq\HelloLGQ.java
# 运行看看有没有错误
./java org.lgq.HelloLGQ
把class
编译成本地应用,记得这时候要使用 x64 Native Tools Command Prompt
,要不会编译报错。
native-image.cmd org.lgq.HelloLGQ
以上截图就说明编译成功了,之后直接输入 org.lgq.hellolgq.exe
运行看是否有问题。
简单对比下jvm和native的运行速度
测试代码
package org.lgq;
public class CountUppercase {
static final int ITERATIONS = Math.max(Integer.getInteger("iterations", 1), 1);
public static void main(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 < 10_000_000; i++) {
total += sentence.chars().filter(Character::isUpperCase).count();
if (i % 1_000_000 == 0) {
long now = System.currentTimeMillis();
System.out.printf("%d (%d ms)%n", i / 1_000_000, now - last);
last = now;
}
}
System.out.printf("total: %d (%d ms)%n", total, System.currentTimeMillis() - start);
}
}
}
编译运行
# 编译成class
./javac -encoding utf-8 .\org\lgq\CountUppercase.java
# 编译成 native
native-image.cmd org.lgq.CountUppercase
# 使用 openJDK 17 运行
java org.lgq.CountUppercase
# 使用 graalVM 运行
./java org.lgq.CountUppercase
# 运行native
org.lgq.countuppercase.exe
OpenJDK 运行结果
graalVM 运行结果
Native 运行结果
可以看到,native的运行速度反而更低了,graalVM性能反而是最好。造成这样结果的原因,有可能是native还是beta阶段的原因,也有可能是JVM的JIT在多次运行后进行了优化带来的性能提升。
SpringNative
SpringNative官方出品的,支持把Spring的应用编译成native应用,依赖 GraalVM 编译工具的。
相比于 JVM,native的方式可以为多种类型的工作负载提供更便宜和持续的托管,比如:微服务,FaaS等,非常适合容器和K8S,而且在启动速度,内存占用和即时峰值性能都有一定的优势。
环境搭建
环境需求,需要Docker环境,如果是Linux系统,需要配置一个非root的用户,可以使用docker run hello-world
来检查docker是否可用。
如果是mac系统,docker的内存分配至少要8GB,而且分配越多CPU核心越好。对于Windows的话,确保安装了WSL2,然后Docker WSL2是开启的,这样可以获取到更好的性能表现。
这里使用WSL2进行演示,关于WSL2的安装,可以看开发环境配置笔记中配置 WSL 的内容。
项目创建
创建项目,可以直接到https://start.spring.io/生成一个新的Spring应用。
如果在旧工程添加,注意,SpringBoot的版本是和 SpringNative 的版本有关联,比如 Spring Native 0.11.4就需要 Spring Boot 2.6.6 版本。我这边使用的是 SpringBoot 2.6.3 和 SpringNative 0.11.2 版本。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.11.2</version>
</dependency>
</dependencies>
添加 Spring AOT 插件
<build>
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>0.11.2</version>
<executions>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
添加支持编译Native
使用SpringBoot的Buildpacks
构建工具可以直接把SpringBoot应用打包成容器镜像。
注意:tiny有更低的footprint和surface attack,也可以选 base(默认)或 full 。
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
默认下,GraalVM的版本会跟随 Buildpacks
的版本升级。而 Buildpacks 和 GraalVM 的版本是有对应关系的,例如 GraalVM 的 22.1.0,对应的 Buildpacks 版本是 7.1.0。如果你想使用固定的 GraalVM 版本,可以按照以下配置好。
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- ... -->
<image>
<buildpacks>
<buildpack>gcr.io/paketo-buildpacks/java-native-image:7.1.0</buildpack>
</buildpacks>
</image>
</configuration>
</plugin>
添加Maven仓库。
<repositories>
<repository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</repository>
</repositories>
在本地maven配置文件排除一些仓库id,要不会拉取不到某些依赖。maven配置文件如下。
<mirrors>
<mirror>
<id>public</id>
<mirrorOf>*,!spring-snapshots,!spring-milestones,!spring-release</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
<mirror>
<id>google</id>
<mirrorOf>*,!spring-snapshots,!spring-milestones,!spring-release</mirrorOf>
<name>阿里云google仓库镜像</name>
<url>https://maven.aliyun.com/repository/google</url>
</mirror>
<mirror>
<id>central</id>
<mirrorOf>*,!spring-snapshots,!spring-milestones,!spring-release</mirrorOf>
<name>阿里云中央仓库镜像</name>
<url>https://maven.aliyun.com/repository/central</url>
</mirror>
<mirror>
<id>mavenorg</id>
<mirrorOf>*,!spring-snapshots,!spring-milestones,!spring-release</mirrorOf>
<name>mavne-org中央仓库</name>
<url>http://repo.maven.apache.org/maven2</url>
</mirror>
</mirrors>
编译运行
# 进入目标,使用mvn编译
mvn spring-boot:build-image
也可以直接在idea里点击编译。
编译成功,还是挺费时间的。
运行docker images
可以看到打包好的镜像已经推到docker了。
# 启动
docker run --rm -p 8080:8080 nativedemo:0.0.1-SNAPSHOT
可以看到启动速度很快。
作为对比,JVM下启动就明显慢很多。
访问测试接口,也没有问题,正常返回数据。
如果要重新打包镜像的话,记得修改版本号或者先docker rmi containerId
移除镜像再打包。
如果要停止运行的话,可以先用docker ps
找出镜像id再使用docker stop containerId
停止即可。