抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

前言

这篇文章一拖就一个多月了,本来是打算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)

可以看到,已经是LibericaNativeImageKit了。

第二个还要配置的是微软的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

如果出现以下报错的,是因为编码问题导致的,导致获取不到本机的架构乱码了。

解决方案有三个:

  1. 使用 native-image 进行编译时,加上-H:-CheckToolchain 指令来关闭检测。

  2. 第二种方式就是修改VS的语言为英语。

  3. 修改系统的默认编码为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次,取最好的一次结果。

请求详情

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充分预热。

使用协程

使用线程

评论