Spring 官宣,换掉 JVM
Spring 团队日前发布了 Spring Native Beta 版。通过 Spring Native,Spring 应用将有机会与 GraalVM 原生镜像的方式运行。为了更好地支持原生运行,Spring Native 提供了 Maven 和 Gradle 插件,并且提供了优化原生配置的注解。
Spring 发布了 Spring Native 的 beta 版本,并在http://start.spring.io上运行它
实际上,这意味着自Spring成立以来,除了Spring支持的常规Java虚拟机之外,我们还将添加Beta支持,以使用GraalVM将Spring应用程序编译到本机映像中,从而提供一种部署Spring应用程序的新方法。支持Java和Kotlin。
这些本机Spring应用程序可以部署为独立的可执行文件(无需安装JVM),并提供有趣的特性,包括几乎即时启动(通常<100ms),即时峰值性能和较低的内存消耗,但所需的构建时间和运行时优化次数少于JVM。
使用简单mvn spring-boot:build-image
或gradle bootBuildImage
命令,您可以生成一个优化的容器映像,该映像将包含一个最小的OS层和一个小的本机可执行文件,该映像仅随附JDK,Spring以及您在应用程序中使用的依赖项中的必需位。
请参阅下面的示例,其中包含50MB可执行文件的最小容器映像,其中包含Spring Boot,Spring MVC,Jackson,Tomcat,JDK和应用程序。
这种原生方式,在很多场景下都会对 Spring 应用产生价值:
具有Spring Cloud功能的无服务器 以更便宜和更可持续的方式托管Spring微服务 非常适合VMware Tanzu等Kubernetes平台 想要创建最佳的容器映像来打包您的Spring应用程序和服务
在使用场景上,比如 Piotr Mińkowski 提供了一个非常棒的指南,介绍了如何在 Knative 上使用 Spring Boot 和 GraalVM 构建原生微服务。
其实这个技术在阿里,他们也探索了两种不同维度上的冷启动加速技术,经过双 11 大促的检验都取得了良好的效果:AppCDS 技术在传统 Java 环境的维度上,利用类数据共享特性改进启动速度和减少内存开销;静态编译技术则在更为激进的维度上,将 Java 程序提前编译为二进制机器码,实现以 Native Code 的速度启动 Java 程序,带来最多两个数量级的启动性能提升。
阿里JVM 团队技术专家林子熠博士在最新出版的《GraalVM与Java静态编译:原理与应用》一书中,揭秘Oracle GraalVM中Java静态编译技术的特性、实现原理、应用与调试技巧,以突破Java“冷启动”桎梏,实现启动性能“质”的飞跃。
作者介绍 :林子熠 博士
《GraalVM与Java静态编译:原理与应用》作者
上海交通大学软件工程博士
美国伊利诺伊大学香槟分校(UIUC)访问学者
中国计算机学会系统软件专委会委员
曾任华为方舟编译器前端核心
现就职于阿里巴巴集团JVM团队,负责GraalVM的Java静态编译和静态分析技术在阿里的落地应用
Graal开源社区主要贡献:
序列化、动态类加载、独立静态分析工具
以下是来自林老师QCon北京大会的分享,以期帮你实现云原生场景下 Java 快速冷启动(下文以林子熠老师第一人称叙述)
Java 诞生至今的 25 年里,凭借其峰值性能高、语言功能强、生态支持好等特点赢得了语言市场的霸主地位。但 Java 冷启动开销大,而云原生时代下的应用程序短小,启动频繁,冷启动问题的解决机不容发。
下图为典型 Java 应用的生命周期:
如图,Java 应用生命周期分为 5 个阶段:VM 初始化阶段、APP 初始化阶段、APP 初活跃阶段、APP 稳定执行期、结束阶段。
VM 初始化(图中红色)和 Class loading(图中蓝色)的开销为冷启动的根因。阿里巴巴实现了两类改造:一类为改良型技术,调整优化现有 Java 的框架和运行模型,另外一类为革新型的技术,摆脱原有 Java 框架另起炉灶。
改良型技术中,阿里巴巴主要实现了基于传统 CDS(Class Data Sharing)的 EagerAppCDS。传统 CDS 包括 mark、Klass*、fields 三部分,如下图所示:
Klass* 指针指向内存中 class 实例 InstanceKlass,该实例为 bytecode Class Loading 解析后生成的,但在多次解析时内容均不变,因此可固定存储磁盘文件 Shared Archive 中,下次运行时从文件中可省略解析直接读取,实现提速。
面对 system class,CDS 可根据 name 快速匹配,但面对 customized class 时,JVM 无法辨认 customized class loader 的身份,因此需在 classpath 上扫描 jar 包,根据 name、crc 校验后方可完成匹配。jar 包即为 libs 包,包含大量 I/O 操作,开销大。EagerAppCDS 用 identity 固定了 customized class loader 的名字,可直接通过 identity+name 匹配找到所需 class。
下图为 EagerAppCDS 在阿里巴巴内部实践的脱敏数据,如图所示性能提升效果从 12%~95% 不等。
EagerAppCDS 虽未开源但已在阿里云 SAE(Serverless 微服务 PaaS 平台)上线。线上可公开实测数据中应用启动耗时降低 5%~45%,提升效果与启动时加载类数量成正比。
除此之外,我们还实现了以下改进型技术:
JWarmup:共享预热后的 code cache,减小 JIT 开销;
PGO AOT:提前准备 runtime 信息,指导改进 AOT 代码质量;
Class Preinit:类预先初始化,降低运行时初始化类的开销。
革新型技术中,阿里巴巴采用了基于 Graal VM 的静态编译技术。Graal VM 为 Oracle 主导的基于 Java 的开源高性能多语言平台:C++、Kotlin、python 等多种语言可通过 Truffle 框架运行在 GraalVM 上,Java 和其他 JVM 语言(如 Groovy、Kotlin 和 Scala 等)编译成 bytecode 后可直接运行。
Substrate VM(SVM)为 Graal VM 的静态编译组件,可将 Java 程序静态编译为可执行文件或共享库文件 Native Image,实现直接编译 Java 代码。
Java 最初依靠解释器实现无需编译实时执行;该解释器性能较差,因此引入了 JIT(Just In Time)实时编译技术,将高热度函数送到编译器中编译;为解决编译器开销大的问题,引入了 AOT(Ahead Of Time)编译,提前编译部分代码;AOT 缺乏 runtime 数据,运行后即丧失转为 JIT 的机会,运行速度慢;静态编译技术将 AOT 扩大,彻底摒弃 JVM,由 SVM 提供运行环境。
传统 Java 执行模型如下图所示:Application(应用本身)在 libs 的支持下运行在 JDK 上在 JVM 中执行。
静态编译在 Graal Compiler 编译器中编译 Application、libs、JDK,同时编译 Substrate VM Runtime,获得 Native Image。Native Image 包含 code(编译后的代码)和 Image heap(存储数据)两部分。Image heap 为运行时 heap 的起点,直接读取 Image heap 可以提高运行时的性能。
静态编译必须遵循封闭性原则 (the closed-world assumption),即所有运行时信息均需在编译时可见。该原则带来两个基本问题:如何确定封闭的边界?如何处理 Java 的动态特性?
Java bytecode 编译为 Native code 时,代码抽象性降低体积增大,如若编译所有代码,Native Image 体积将过于庞大,因此需确定封闭边界。SVM 通过静态分析上实现了从给定入口开始确定程序可达范围的功能。
该技术应用广泛,例如 main 函数调用 Virtue call 必须先明确其 type,type 和 Virtue call 有时可唯一绑定,但通常不能唯一绑定。此时使用静态分析技术,可明确 Virtue call type 的可能范围,实现封闭。
受静态分析本身的特性和能力所限,静态分析得到的可达代码集合(蓝色)略大于实际执行代码集合(绿色)。静态分析精度越高、冗余越少、image 越小。
静态分析无法分析出 Java 的许多动态特性运行时的行为,如反射、动态代理、JNI、序列化(阿里巴巴贡献,从 21.0 开始支持)、动态类加载(阿里巴巴贡献,patch 已经通过评审)等。此时需提前获取所需信息,方可封闭此类动态特性的触达范围——即需基于配置进行动态特性支持。
以反射为例。SVM 提供了 native-image-agent,可记录 APP 运行时所有的反射。编译时只需解析配置文件,即可注册反射目标,扩大编译范围;同时获取反射信息后可放入 ReflectionData 缓存中,将反射调用替换为直接调用。运行时如遇反射可查找 ReflectionData,获取目标值,通过 Method.invoke 直接调用目标函数。
下图为通过静态编译和传统 Java 两种方式,分别用反射调用空函数 30 次性能对比测试结果:
由于峰值过高,该图进行了对数修正。传统 Java 编译空函数耗时(深蓝色)为 3000ns,峰值由于反射开销为 4000ns,静态编译后(深蓝色)稳定在 150ns 内。
SVM 的静态编译实现的编译优化包括标准优化如:Method inlining, constant folding and arithmetic optimization, loop optimization, partial escape analysis 等。
此外还有因为静态分析而引入的新的编译优化,例如未被标为 final 的 field 通过静态分析发现只读不写,即可当作常量处理,做常量折叠等优化;又如,静态分析出某虚函数 type 唯一绑定,即可优化为直接调用,进而实现 inline;再如,消除部分编译时已知变量类型状态的类型检查和空指针检查。
静态编译由于所有的类均已被编译因此只有一个类加载器,实际只执行类查找功能。
传统 Java 一边检查异常一边运行,如遇异常直接处理即可。SVM 考虑到在不同平台兼容性,异常处理采用非信号处理机制:检测无错方可正常运行。该检测对性能影响小。
此外,静态编译的 GC 为 Oracle 开源版本中的单线程 stop-and-copy 顺序 GC,性能一般。
下图为 Graal VM 官方的实验数据:
如上图所示,在只执行 Hello world 程序时,Native Image 性能次于 C,与 Go 相当,远快于传统 JDK;内存使用次于 C,只有 Go 的一半,远低于传统 JDK,具有高性能低内存占用的优点。图中红色数据为受测语言数据除以 Native Image 数据所得比值。
Javac 为 Java 编写的编译器:可以在 Java 程序中来调用 API 编译,也可用 stand alone 工具编译。通过 API 调用,实际上已完成 VM 启动,因此两者对比可观察冷启动带来的性能差异。
通过 API 调用 Javac 耗时 250ms,使用 Native Image 后耗时达到 35ms,实现了 1 个数量级的飞跃。
Javac 中使用的反射、Serverless 较少,其他项目静态分析性能提升效果更加显著。下图为基于 spring boot 的应用 greeting-service 部署在阿里云函数计算平台上的数据。greeting-service 收到请求会返回“ hallo”,功能简单但需要 spring boot 全流程支持。
如图所示,Native Image 静态编译(橙色)相比传统 Java(蓝色):内存占用从 128MB 降至 21MB;实际第一次调用耗时从 454 ms 降至 4.27 ms,提升了两个数量级;阿里云服务计费从 500ms 降至 100ms,事半功倍。
静态编译的局限性如上表所示:
为实现封闭性,反射、动态代理、JNI、序列化、动态类加载均需要通过配置支持;
不支持 InvokeDynamic(开发人员使用)、Method Handles(开发人员使用)、Security Manager、多 classloader、Finalizers、过时 Thread 函数(如 Thread.stop())等;
Java 程序被静态编译后不再保留 bytecode,因此存在监控、调试方面的问题:不支持 JVMTI、JMX、agent,只能使用 GDB 调试,无法通过 Eclipse IDE、IntelliJ IDEA 等调试。
GraalVM 静态编译目前生态如下:
阿里云:通过阿里云函数计算平台进行支持部署 serverless Native Image 应用,通过 Apache RocketMQ 为 C++ 客户端提供使用静态编译的 Java 共享库;
Spring 社区:发布了针对于静态编译 Spring-Native beta 版本,完全支持 Spring 的运算;
MICRONAUT:实现了支持 Native Image 的去反射微服务框架;
Facebook & Twitter:均在生产环境下使用 Graal 编译器代替 C2 编译器。
总之,在 Serverless 场景下 Java 的冷启动问题与应用对快速响应、实时扩展的需求形成突出矛盾。阿里巴巴一方面在现有技术上不断改进,最终形成突破:EagerAppCDS 提升最多 45% 的启动速度;另一方面积极参与开源社区探索创新型的前沿技术,打磨成熟用于实践:GraalVM 静态编译技术最多提升百倍启动速度。但 GraalVM 存在兼容性和改造成本的问题,适合新项目。