近来不是很好,全是挫折,水一篇翻译,缓解一下焦虑。正如之前提到的,Java后端的基座这几年就没什么变化,而最近发布的 Java 21 以及 Spring Boot 3 我认为或许就是现如今前后5年最大的变化?打败’谷歌’的一定不是下一个’谷歌’,那是谁呢,quarkus?又或者是别的新事物?这个苗头好像还没出现,Java 8 和 Spring Boot 太舒服了?好像舒服到没人在意了。

正文

现在都到齐了:Spring Boot 3.2,GraalVM native images,Java 21以及Project Loom的虚拟线程

已经等待了太久,但我们终于可以创建使用 Spring Boot(通过 Spring Boot 3.2)和 Java 21 虚拟线程(Project Loom)的 GraalVM 原生镜像了!

为什么说这些很重要呢?Project Loom 和 GraalVM 原生镜像拿其中任意一个单独来说都提供了引人注目的运行时特性。我等了很久才看到它们集成在一起!让我们逐个来讨论它们。

GraalVM Native Images

GraalVM 是一个 OpenJDK 发行版,它提供了一些额外的工具和功能,包括一个名为 native-image 的程序,它可以对代码进行预编译 (AOT) 。我们不会在这里详细介绍它的所有工具功能,但是基本上来说它会获取您的代码,去掉您不需要的东西,然后将其余部分编译成速度极快的、操作系统和架构特定的本地代码。使用GraalVM的结果令人惊叹,您可以得到类似于使用 C 或 Go 编译程序所得到的效果。生成的二进制文件启动速度很快,在运行时占用的 RAM 也少很多。想象一下,您可以在只占用几十MB内存的情况下部署一个现有的Spring Boot应用程序,并且在几百毫秒内启动。现在您只需运行./gradlew nativeCompile./mvnw -Pnative native:compile就能实现这个梦想。自 2022 年 11 月 Spring Boot 3.0 发布以来,Spring Boot 已经在生产中支持 GraalVM 本地映像。

Project Loom

Project Loom 将透明的纤程引入到JVM。目前,在 Java 20 或更早版本中,IO 是阻塞的。调用 int InputStream#read()时 ,您可能需要等待下一个字节到达。虽然在 java.io.File 的IO 操作中很少有很大的延迟,然而在网络中,你永远不知道会发生什么。客户端可能会断开连接,可能正在穿过隧道。再强调一次,情况很难预测。在此期间,程序流会被阻塞在执行线程上,无法继续进行。在下面的代码片段中,我们无法知道何时会看到单词 after被打印 。可能是从现在开始的一纳秒,也可能是一周后,总之它正在阻塞。

1
2
3
4
InputStream in = ... 
System.out.println("before");
int next = in.read();
System.out.println("after");

这已经够让人头疼了,但 Java 21 之前的 Java 线程架构让情况变得更加糟糕。目前,每个线程或多或少都是映射到一个本机操作系统线程,而创建新的线程很昂贵,需要大约 2 MB 的内存。

当然,有一些方法可以解决这个问题。您可以使用由 Java NIO ( java.nio.* ) 提供的非阻塞 IO。在这种模型中,您请求字节数据然后注册一个回调,该回调仅在实际有可用字节时执行,如此便无需等待,无需阻塞。这种方法的显着好处是,当我们无事可做时可以远离线程,同时允许其他人在此期间使用这些线程。但美中不足的是,它有点乏味和低级。 Spring 对响应式编程有着良好的支持,它在非阻塞 IO 之上提供了一种函数式编程模型,效果很好。但是,它需要改变您编写代码的方式。如果您可以直接采用现有代码,就像上面演示的那样,并且保证执行正确,在没有发生事件时透明地将执行流程移出线程,然后在有事情发生时恢复执行流程,这样岂不妙哉?而这就是 Loom 项目的承诺,采用上述代码,并确保在虚拟线程中执行它(这很简单,您可以使用 ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor() ),它就可以正常工作了!

SpringBoot 3.2

在典型的 Spring Boot 应用程序中,线程池(ExecutorExecutorService实例)随处可见!例如您的 Web 服务、消息传递逻辑等等。现在,在新的 Spring Boot 3.2 里程碑版本(最终版本将于 2023 年 11 月发布),您可以让 Spring Boot 使用虚拟线程执行器,只需一个简单的属性配置: spring.threads.virtual.enabled=true

All Together Now

请注意:Spring Boot 3.2 尚未正式发布, Java 21 也尚未正式发布(2023年9月19日),支持 Java 21 的 GraalVM 同样也尚未正式发布(2023年9月19日)。事情有点困难,但我一直渴望尝试将所有内容组合在一起:在 Spring Boot 应用程序中使用带有虚拟线程的 GraalVM 原生镜像。当一切看起来都准备就绪时,我发现 GraalVM 编译器中有一个需要克服的小Bug!当然,这并不是什么难事,由于 GraalVM 团队的出色工作,问题很快就得到了解决。但正如我所说:事情有点困难,不过这绝对值得一试!让我们将所有部件都安装到位,这样您就可以亲自尝试一下了。

Installing GraalVM for Java 21

首先,我们需要安装支持 Java 21 的 GraalVM。我使用的是采用 Apple Silicon / ARM 架构的 Mac,因此我选择了最新版本中的 graalvm-community-java21-darwin-aarch64-dev.tar.gz (截至撰写本文时)。您只需下载并解压缩它,并确保正确配置 JAVA_HOMEPATH 等关键环境变量。我是 SDKMan 项目的粉丝,所以我想用它来管理这个新下载的版本。我将 .tar.gz 解压缩到名为 - ~/bin/graalvm-community-openjdk-21/ 的文件夹,然后运行以下命令:

1
sdk install java graalvm-ce-21 $HOME/bin/graalvm-community-openjdk-21/Contents/Home

然后,为了确保它适用于所有操作:

1
sdk default java graalvm-ce-21

打开一个新的 shell 并确认它有效:

1
2
3
4
> native-image --version 
native-image 21 2023-09-19
GraalVM Runtime Environment GraalVM CE 21-dev+35.1 (build 21+35-jvmci-23.1-b14)
Substrate VM GraalVM CE 21-dev+35.1 (build 21+35, serial gc)

译者注:GraalVM 已经正式发布,所以无需手动下载安装包,直接使用sdk install java 21-graalce即可。

Configuring a Spring Boot project to use Java 21

打开访问 Spring Initializr (start.spring.io),指定版本 3.2.0 (M2) (或更高版本),添加 GraalVMWeb ,然后下载打包文件、打开它并将其加载到您的 IDE 中。我们仍然需要配置构建以使用 Java 21。这还不是理想状态,因为 Gradle 还未真正适配 Java 21,但它可以工作,基本上来说。我几乎没有任何使用 Gradle 的技巧和能力,但这个配置似乎有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0-M2'
id 'io.spring.dependency-management' version '1.1.3'
id 'org.graalvm.buildtools.native' version '0.9.24'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '21'
}

graalvmNative {

binaries {
main {
buildArgs.add('--enable-preview')
}
}
}

java {
toolchain { languageVersion = JavaLanguageVersion.of(21) }
}

repositories {
mavenCentral()
maven { url 'https://repo.spring.io/snapshot' }
maven { url 'https://repo.spring.io/milestone' }
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
useJUnitPlatform()
}

将其重新导入到 IDE 的构建配置中。将以下属性添加到 application.properties

1
spring.threads.virtual.enabled=true

然后将您包含 main(String[] args) 的主类更改为如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;
import java.util.Set;

@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}

@RestController
class CustomersHttpController {

@GetMapping("/customers")
Collection<Customer> customers() {
return Set.of(new Customer(1, "A"), new Customer(2, "B"), new Customer(3, "C"));
}

record Customer(Integer id, String name) {
}

}

您可以像平常一样运行程序: ./gradlew bootRun 。它正在使用 Project Loom!但真正令人兴奋的点是:让我们构建一个 GraalVM 原生镜像! ./gradlew nativeCompile ,这个操作这可能需要一两分钟的时间..

执行完成后,您就可以在 build 目录中运行本机二进制文件:./build/native/nativeCompile/demo。现在我们已经在使用 GraalVM 原生镜像了!

我们基本上已经到达了这次小小冒险的终点线,但我需要再次提醒您 - 这还不是 GA 软件!如果一切顺利,那么到 2023 年 11 月底它将会是GA软件,但现在还没有。这就是为什么我认为发布这篇博客非常有价值的原因:我希望您尝试一下全新的内容。即便对于 Project Loom(将在不到两周的时间内即 2023 年 9 月 19 日部分登陆 Java 21),严格来说它也还没有全部完成。我们将在此版本中获得部分支持,但您可以通过预览功能尝试其他两项内容,如果您发现某些问题,那么请这些信息反馈给我们,这非常重要,这样这些问题就能立刻被消除,而不是以后。毕竟,不到两周前我才在 GraalVM 编译器中发现了一个 bug!所以,去尝试一下吧。现在是成为 Java 开发人员的最佳时机,Loom 和 GraalVM 这两个东西就像免费的钱一样。这些内容成功实施后,您将在 Spring Boot 工作运行中获得更好的运行时可扩展性、工作效率、启动时间、内存消耗以及更多优势。升级并尝试一下,我敢打赌您一定会喜欢它。

后记

  1. AOT 编译成本地可执行文件以后,启动速度确实是快,基本上可以达到 100ms 以内,这一点在lambda环境下(也就是所谓的云原生)尤其有用,恰巧我对这一点有实际的应用体会:一个部署在aws lambda 上的 http 转发器,从 java 换成 golang,效率直接大幅飙升,提升原因是 Java 的启动速度慢、需要预热、脉冲式的流量特性(瞬间高峰然后回落)。但是很明显,不是所有场景都需要这种特性,节省的这点启动、预热时间相较于运行时间还是有点微不足道。

  2. 目前 GraalVM 的 AOT 编译速度确实是有点感人,我在 Win(13代i5-13500H) 和 Mac(第8代i5) 都做了尝试。编译单个 HelloWorld 文件,Win 耗时差不多要18s左右,可执行文件约7mb;编译一个空的Spring Boot项目,Win 耗时约1m24s左右,Mac耗时约3m左右,可执行文件大小在80mb左右。相比于 golang 这些,真的有点太慢了,很难想象大把代码的线上项目要编译多久,这会非常影响开发部署效率,不知道以后能改进多少。

  3. 这几个变化看起来很新奇,但是回归本质来说,好像也就那样。正如上文所说,虚拟线程在使用的时候几乎是对现有代码零修改,而且往往是框架提供一些开关。

  4. 目前虚拟线程也不能随便使用,在原文的评论区有提到竞对Quarkus发布的一篇文章,里面提到了几个注意点

    1. Thread Pining,对于 synchornized 导致持有 monitor lock,虚拟线程在锁等待时会保持对操作系统线程的占有。这听起来是个不太好的东西,不知道以后是否会改进。
    2. Thread Monopolization,虚拟线程的调度不是抢占式的,如果执行内容都是长时间的cpu占用,还是线程池更合适。
    3. Carrier thread pool elasticity,不要忽视虚拟线程对应的操作系统线程池,它也有可能出现池大小失控的情况。
    4. Object pooling,以往没有虚拟线程的情况下,很多重量级对象会被设计为线程绑定的形式,以池化的形式提高效率,但是在使用虚拟线程时情况发生了改变,虚拟线程很便宜,随时创建,这就可能导致频繁的创建重量级对象,比如Jackson的ObjectMapper
    5. Stressing thread safety,虚拟线程让并发更加方便,不要忽视线程安全。

    如果以后这些特性持续存在,并且Java 21趋于流行,那这些一定是八股文常客。

  5. 目前这些东西还不是太成熟,AOT对 Win 环境不够友好,IDE不识别等等,我在尝试这些内容的时候遇到了很多烦人的问题,费了我老半天,但是这些东西相信很快会迭代优化。

附录

Win环境下要进行AOT编译,请遵照这篇官博的教程,需要安装visual studio组件,并且要在VS提供的 x64 Native Tools Command Prompt 中执行编译命令才可以(mvn本地编译命令也是如此)。

原文使用Gradle来进行组织和编译,我更偏向使用Maven,Maven也是没问题的。在 Spring Initializr 中选择Maven,然后执行命令mvn -Pnative native:compile -DskipTests即可。