Java 在云原生时代的挑战
首先引用下周志明在 2020 年 QCon 上的分享《云原生时代,Java 的危与机》,总结了 Java 在云原生时代受到的挑战,有了这个起点才有了我后面对 Java 发起的诸多项目的关注与语言核心层面的变化,更深刻的理解语言发展动因。
Java 设计之初的许多假设,比如「一次编写,到处运行」,「一切皆为对象」等都遇到了动摇。
比如一次编写,到处运行。原有优势是屏蔽操作系统与指令集差异,提供语言层面虚拟化,做到平台无关(Platform Independent)与架构中立(Architecture Neutral),改变程序分发通过二进制发型包或源代码再编译的方式。从而实现一套代码在不同指令集硬件、不同操作系统上能得到相同的运行效果,方便程序分发,也通过统一线程模型、内存模型、字节序等底层细节降低了语言使用门槛。
在容器出现后平台无关性的优势就可以通过实现不可变基础设施,向应用代码隐藏环境复杂性,提供操作系统层面虚拟化,将程序与运行时一起封装到镜像里来分发程序。因为微服务的盛行,围绕业务而不是技术构建应用,内存占用大幅降低、高可用集群对服务长时间运行要求降低。
这就要求 Java 具备容器亲和性,在镜像体积、启动速度、内存消耗、到达最佳性能时间等饱受诟病问题上下手。
再比如,一切皆为对象的假设。就导致了内存占用居高不下,性能优化的首要问题就是优化内存占用。
提供类似的值类型支持,提供一个新的关键字(inline),让用户可以在不需要向方法外部暴露对象、不需要多态性支持、不需要将对象用作同步锁的场合中,将类标识为值类型。此时编译器就能够绕过对象标识符,以平坦的、紧凑的方式去为对象分配内存。
Java 在诞生 25 周年公布的这些项目就是应对新时代挑战所做出的回应。这里我们从挑战最大的 Leyden 项目入手,了解语言发展的动因与过程。
Leyden
Mark Reinhold 在 2020 年发起 Leyden 项目时开篇明义,Leyden 项目就是为了解决 Java 长期以来饱受诟病的问题,包括启动时间慢、到达最佳性能时间慢、镜像体积大。
解决的思路是引入静态镜像(static image),这是一个可独立运行的程序,甚至不依赖于 JVM,也是一个封闭世界(closed world),所有程序在编译期就是已知的,不能在运行时加载新的字节码。构建方式会依赖 HotSpot JVM、C2 编译器、CDS 等。
我们都知道以前的 Java 生产力工具大量依赖开放的代码空间(open world)这个基础设计,而如果全部封闭就将导致动态代理、反射、CGLib 等动态加载全都无法使用,Spring、ORM 等工具将直接崩溃。
如果使用提前感知并加载,下面这个办法似乎可行:能够让程序先以传统方式运行(启动)一次,自动化地找出程序中的反射、动态代理的代码,代替用户向编译器提供绝大部分所需的信息,并能将允许启动时初始化的 Bean 在编译期就完成初始化,直接绕过 Spring 程序启动最慢的阶段。但将造成大量代码的不兼容或改造工作量,如何平衡还有很多工作要做。
GraalVM
GraalVM 提供了一种部署和运行 Java 应用程序的新方法,本地镜像(native image)是独立的可执行文件,可通过提前处理已编译的 Java 应用程序生成。与 JVM 相比,本地镜像通常内存占用更小,启动速度更快。
而且可以保证应用程序运行更安全,因为不会在运行时加载任何未知程序,主流的 Spring Boot、Micronaut、Quarkus 等微服务框架都已支持。
采用 AOT(Ahead of Time)后,Java 程序相对原来的编译过程就有了根本变化。原来需要 Javac 在编译时将 Java 代码编译成字节码,然后结合 JIT(Just In Time)在运行时翻译成机器码,现在直接在编译时由 Javac 结合 AOT 将 Java 代码转换成了机器码,也就是直接可执行文件。
构建 Spring Boot 本地镜像
Spring Boot 官网提供了两种用于构建本地镜像的方式,一种是使用 Paketo 这样的云原生构建工具,一种是使用 NIK 这样的本地 GraalVM 构建工具,我们这里选择后者完成构建。
安装 SDKMAN
访问安装脚本直接安装:
curl -s "https://get.sdkman.io" | bash
安装完成后的提示是:
All done!
You are subscribed to the STABLE channel.
Please open a new terminal, or run the following in the existing one:
source "/root/.sdkman/bin/sdkman-init.sh"
Then issue the following command:
sdk help
Enjoy!!!
配置环境变量:
source "$HOME/.sdkman/bin/sdkman-init.sh"
验证安装成功:
sdk version
出现如下提示说明安装成功:
SDKMAN!
script: 5.18.2
native: 0.4.6
安装 NIK
Liberica 是 Spring 推荐的 OpenJDK 发行商,他们提供了基于 GraalVM 可制作本地镜像的工具集,也就是 NIK(Native Image Kit),这里选择基于 GraalVM 22.3 和 JDK 17 的 NIK 作为安装对象:
sdk install java 22.3.r17-nik
安装完成后会提示:
Installing: java 22.3.r17-nik
Done installing!
Setting java 22.3.r17-nik as default.
使用 SDKMAN 的一个好处就是可以方便管理多个 JDK,包括不同发行商的不同版本,切换使用刚安装的这个版本:
sdk use java 22.3.r17-nik
GraalVM 部署环境就搭建好了,如果是 Windows 还需要安装下 Visual Studio,用于完成 C++ 构建工具的安装。
创建基于 GraalVM 构建的 Spring Boot 项目
简单地,直接从,选择 GraalVM Native Support 依赖项创建,保证项目使用 spring-boot-starter-parent 作为父 pom,这样可以直接继承 native profile:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
</parent>
并且使用了 GraalVM 构建插件:
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
构建本地镜像
使用命令构建:
mvn -Pnative native:compile
如果没有安装 maven,就用 SDKMAN 安装下 maven:
sdk install maven
因为使用 AOT 编译时间会比较长,稍等一下看到编译完成,编译过程内存和 CPU 占用还是比较高的:
------------------------------------------------------------------------------------------------------------------------
16.5s (6.6% of total time) in 138 GCs | Peak RSS: 3.23GB | CPU load: 2.90
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
/usr/local/src/demo/target/demo (executable)
/usr/local/src/demo/target/demo.build_artifacts.txt (txt)
========================================================================================================================
Finished generating '/usr/local/src/demo/target/demo' in 4m 2s.
运行程序,可以看到启动速度很快:
./demo
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.7)
...... Starting AOT-processed DemoApplication using Java 17.0.5 with PID 24287
......
...... Started DemoApplication in 0.064 seconds (process running for 0.07)
curl 一下 REST 端点,看到控制台有结果返回,说明本地镜像正常运行。