前言
这篇文章一拖就一个多月了,本来是打算JDK21出就写的,辞职后玩得不亦乐乎就忘记了。
言归正传,之前写过 GraalVM初步体验,但是当时的SpringBoot3还没有正式发布,并不是正式版,体验不是很好。如今SpringBoot3已经正式版了,之前9月底也发布了LTS的 JDK21,本来是想和JDK21正式发布的协程一起体验的,但是当前最新的 3.2.0-SNAPSHOT
版本不支持 Tomcat11
,会因为调用了Tomcat11
不存在的方法而报错(该方法在Tomcat10
标记过时了),而且Tomcat11
还在alpha
早期阶段。
然后也尝试过在Tomcat10
的基础上把执行器替换为协程(附录提供修改方式和压测结果对比图),但是吞吐量并不好,甚至比线程效果差很多,应该是没有完全适配的原因,所以这次先不体验了,之后等正式版了再补上,还有这里只做简单的体验,深入的探讨之后再写一篇文章。
环境搭建
Windows环境配置
系统配置如下:
第一个需要的是能够构建Native Image的JDK,可以选择Graalvm的,也可以选择Liberica Native Image Kit,这里就使用Liberica
的了。
下载好之后,配好环境变量,配好后进终端输入java -version
看下是否配置正确。
❯ java -version
openjdk version "21.0.1" 2023-10-17 LTS
OpenJDK Runtime Environment Liberica-NIK-23.1.1-1 (build 21.0.1+12-LTS)
OpenJDK 64-Bit Server VM Liberica-NIK-23.1.1-1 (build 21.0.1+12-LTS, mixed mode, sharing)
可以看到,已经是Liberica
的NativeImageKit
了。
第二个还要配置的是微软的MSVC
编译环境,之前的 GraalVM初步体验 这文章有,在这就不重复了。
MAC环境配置
系统配置如下:
可以去 Liberica 下载NIK,然后配置环境变量即可。
上面Windows使用这种方式了,所以mac换种方式,使用sdkman的方式安装。
# 安装SDKMAN
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk version
# 查询需要安装的 NIK
sdk list java
# 找到需要安装的最新版本,然后安装
sdk install java 23.1.r21-nik
# 设置为默认jdk
sdk default java 23.1.r21-nik
# 注意,当前的 JAVV_HOME 环境变量也要指向 NIK,否则会有java版本不对的问题
❯ echo $JAVA_HOME
/Users/devlgq/.sdkman/candidates/java/current
❯ java -version
openjdk version "21" 2023-09-19 LTS
OpenJDK Runtime Environment Liberica-NIK-23.1.0-1 (build 21+37-LTS)
OpenJDK 64-Bit Server VM Liberica-NIK-23.1.0-1 (build 21+37-LTS, mixed mode, sharing)
Linux环境配置
这里使用虚拟机演示,配置如下:
这里换种方式,使用手动配置环境变量的方式,想方便可以使用sdkman的方式。
# 安装依赖
apt-get install gcc
# 下载相关依赖
wget https://dlcdn.apache.org/maven/maven-3/3.8.8/binaries/apache-maven-3.8.8-bin.tar.gz
wget https://download.bell-sw.com/vm/23.1.1/bellsoft-liberica-vm-core-openjdk21.0.1+12-23.1.1+1-linux-amd64.tar.gz
# 编辑环境变量
vim ~/.bashrc
export MAVEN_HOME=/usr/sdk/apache-maven-3.8.8
export JAVA_HOME=/usr/sdk/liberica-nik-21
export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH
# 更新环境变量
source ~/.bashrc
java -version
openjdk version "21.0.1" 2023-10-17 LTS
OpenJDK Runtime Environment Liberica-NIK-23.1.1-1 (build 21.0.1+12-LTS)
OpenJDK 64-Bit Server VM Liberica-NIK-23.1.1-1 (build 21.0.1+12-LTS, mixed mode, sharing)
案例编写
去spring initializr创建一个初始化工程,选择自己需要的依赖。
编写测试案例接口。Controller层代码:
package org.lgq.contorller;
import lombok.AllArgsConstructor;
import org.lgq.dao.UserDao;
import org.lgq.pojo.User;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author DevLGQ
* @version 1.0
*/
@RestController
@RequestMapping("user")
@AllArgsConstructor
public class UserController {
UserDao userDao;
@GetMapping("/detail/{id}")
public User getUserInfo(@PathVariable("id") Integer id) {
return userDao.findById(id).orElse(new User());
}
}
Dao层代码,这里使用JPA,MyBatis现在不支持。
package org.lgq.dao;
import org.lgq.pojo.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
* @author DevLGQ
* @version 1.0
*/
public interface UserDao extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User> {
}
POJO实体对象代码:
package org.lgq.pojo;
import jakarta.persistence.*;
/**
* @author DevLGQ
* @version 1.0
*/
@Entity
@Table(name = "account")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "name")
private String name;
@Column(name = "money")
private Float money;
// 省略get/set方法
}
application.yml
配置文件
server:
port: 8081
tomcat:
max-connections: 1000 # 等待队列数,超过则拒绝请求
connection-timeout: 3s # 连接超时时间
accept-count: 100 # 底层socket接收数
threads:
max: 1000 # 最大线程数,默认设置为200
min-spare: 100 # 初始化时创建的线程数,默认设置为10
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://xxxx:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
username: xxxx
password: xxxx
hikari:
maximum-pool-size: 2000
minimum-idle: 50
镜像编译
Windows编译
打开随VS安装的x64 Native Tools Command Prompt for VS 2022
应用,然后cd到项目目录。
执行编译指令:mvn -Pnative native:compile -Dmaven.test.skip=true
测试:mvn -PnativeTest test
打包镜像:mvn -Pnative spring-boot:build-image
如果出现以下报错的,是因为编码问题导致的,导致获取不到本机的架构乱码了。
解决方案有三个:
使用 native-image 进行编译时,加上
-H:-CheckToolchain
指令来关闭检测。第二种方式就是修改VS的语言为英语。
修改系统的默认编码为UTF-8,windows下默认中文编码是GBK的。
这里使用第二种方式。修改完之后再次编译。
看到这个,就说明成功了,在target
目录下面,会生成相关依赖和执行文件。
直接运行spring-native-demo-21.exe
,访问接口没有问题,而且启动速度非常快。
MAC编译
可以在ide窗口的maven菜单点击编译。
或者终端进入项目目录,执行下面指令,这里使用这种方式。
# 执行编译
mvn -Pnative native:compile
可以看到,把CPU都干满了,内存也吃了不少。
出现BUILD SUCCESS
就编译完成了,编译了快四分钟了,还是挺慢的。
进入target
目录,直接运行spring-native-demo-21
即可。
访问接口,正常返回。
Linux编译
进入工程目录,执行编译指令
mvn -Pnative native:compile -Dmaven.test.skip=true
遇到错误,/usr/bin/ld: cannot find -lz
,看提示就知道了,缺了libz.a
库。
去debian软件包搜索下,找到对应框架的软件包名安装即可。
apt-get install zlib1g-dev
安装好后再编译即可。
编译过程可以看到,把虚拟机分配的四核都干满了,内存也吃到了6g。
编译成功,花费时间3分18秒。
进入target
目录,直接运行./spring-native-demo-21
。
访问接口,正常返回。
简单压测
测试机配置如下:
压测机就是上面的Windows编译机,这里就不重复了。
Native和JVM先都使用默认参数启动。
压测参数:并发1000,循环50次,重复3次,取最好的一次结果。
请求详情
Navite方式运行
Native运行压测结果
刚启动时内存占用144.1MB。
压测后内存占用765.3MB。
JVM方式运行
JVM运行压测结果
刚启用内存占用531.2MB。
压测后内存占用1119.5MB。
调整JVM参数
上面运行并没有加限制,JVM默认最大堆是系统的1/4,我这机子是32g的,那最大堆就是8g了,实际这样是不太符合生产环境的,所以先压测下,找个合适的启动参数。按照默认参数压测3次后,观察这时候的老年代占用是117.9m,通过指令或工具触发Full GC,观察老年代。
Full GC后,老年代占用内存75.51m。
一般堆内存的设置,大小为 Full GC之后老年代存活容量的 3 到 4 倍,所以现在这个值是 75.51 * 4 = 302.04,而年轻代的话是 Full GC之后老年代存活容量的 1 到 1.5 倍,75.51 * 1.5 = 113.265,向上取个好看的,使用以下参数启动:
java -Xmx320m -Xms320m -Xmn120m -jar spring-native-demo-21-0.0.1-SNAPSHOT.jar
JVM方式运行
启动后内存占用417.1m。
压测后结果如下:
压测完内存占用694.8m,比之前没有限制低了不少,但是并发吞吐量差距不大。
Native方式运行
./spring-native-demo-21.exe -Xmx320m -Xms320m -Xmn120m
启动后内存占用169.9MB。
压测结果
压测后内存占用292.3MB。
总结
运行方式 | 编译时间 | 启动时间 | 启动内存占用 | 压测后内存峰值 | 吞吐量 | 平均响应时间 |
---|---|---|---|---|---|---|
native运行(默认参数) | 138s | 0.142s | 144.1MB | 765.3MB | 11307.1/s | 70ms |
jvm运行(默认参数) | 8.849s | 5.145s | 531.2MB | 1119.5MB | 12503.1/s | 60ms |
native运行(调整堆) | 138s | 0.167s | 169.9MB | 292.3MB | 10964.9/s | 69ms |
jvm运行(调整堆) | 8.849s | 5.09s | 417.1MB | 694.8MB | 12600.8/s | 60ms |
综上来看,可以知道Native方式在启动方面非常占优,速度快,内存占用也低,很适合云原生,而性能方面,两者相差不算太大(实际严谨点的话,还需要看CPU的峰值占用),而且JIT已经发展很多年了,有点差距也很正常。
虽然Native使用还是有些限制,比如 Java的反射等动态特性是不被支持的,因此需要提供特殊的Metadata
配置来绕过这些限制,但是这些开源社区都已经在跟进了,相信不久的将来会越来越好。
其实Java这些年的发展还是令人很满意的,无论是AOT还是协程的引入,Spring生态的积极跟进,Java一直都在努力开创或者吸纳各种新特性、新功能。
最后,作为一个Java开发者,真诚地希望,Java能一直流行下去。
附录
Tomcat10
修改执行器为协程的。
@Configuration
public class TomcatCustomConfig {
/**
* 异步线程池使用协程。这个如果有使用Spring的 @Async 可以用到。
*/
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
/**
* tomcat执行器使用协程
*/
@Bean
public WebServerFactoryCustomizer<ConfigurableTomcatWebServerFactory> webServerFactoryCustomizer() {
return new TomcatVirtualThreadsWebServerFactoryCustomizer();
}
}
压测参数:并发1000,循环10次。压测多次,让JVM充分预热。
使用协程
使用线程