【干货】云原生时代,阿里如何让Java冷启动提速两个数量级?
来源 | QCon全球软件开发大会 嘉宾 | 林子熠 整理 | 李慧文
Java 语言自从诞生以来就被打上了“慢”的标签。经过 25 年的发展,Java 程序的峰值性能在实时编译(JIT)技术的支持下已能超越 C/C++ 程序,但实时编译仍无法解决冷启动速度慢的问题。在云原生的场景下,往往需要快速拉起新的服务以响应用户持续增长的请求,此时服务的启动时间就显得至关重要。
阿里探索了两种不同维度上的冷启动加速技术,经过双 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 存在兼容性和改造成本的问题,适合新项目。
423 · 世界读书日之际,我们邀请到了《GraalVM与Java静态编译:原理与应用》作者林子熠分享Java冷启动性能飞跃之道:Java静态编译技术,为你探究其中的奥秘。
▌直播间福利:
邀请好友观看「Java冷启动性能飞跃之道:Java静态编译技术」直播,将有机会免费领取
《GraalVM与Java静态编译:原理与应用》/《Java核心技术(原书第11版》等纸书
免费获得华章千余种有声书、电子书VIP季卡
荐
▊《Java核心技术(Core Java)》
这本书就像一张高精地图,展示了Java编程语言这座“热门城市”的知识体系全貌,从Java语言的核心概念、基础语法、学习路线,到各个重要特性、异常处理、开发方法等都有全面和详细的介绍。
对于学习者最重要的是动手实践,实践是最好的老师,本书随内容提供了大量示例代码,可以跟着这些示例由浅入深地进行实践。书中还给出了大量注释、提示和警告,帮助你写出高质量代码。作者凯.霍斯特曼亲自为本书录制了学习视频,对书里的重点和难点一一作了讲解,相关资源,在B站“Java技术核心技术站”可免费观看。
▊《Spring Data JPA:入门、实战与进阶》
▊《Java并发编程的艺术》
好啦,接下来就是本次送书环节啦~上面给出的书单,小伙伴们留言说说你想要哪本以及理由,松哥会从中选出六位幸运小伙伴,满足你的愿望,周三晚上开奖~。