近些年来, 云原生(Cloud Native) 的概念席卷业界,其核心元素之一就是如何提高云上业务的响应速度。随着应用微服务化、容器化程度的不断提高,应用的执行响应速度也在不断提升,逐渐触及到了 Java 程序速度提升的天花板——Java 自身的启动运行开销。
虽然 Java 从最初的单独解释执行演进到 JIT(实时编译)和 AOT(提前编译)后,在运行时 peak performance 和冷启动性能方面都取得了大幅进步,但如虚拟机启动、代码解释执行、JNI 调用、反射开销、静态初始化检查开销等 Java 虚拟机和运行时本身的耗时点始终存在,并且耗时占比随着应用程序运行时间的降低而愈加突出。要进一步提高云原生应用的启动执行速度,突破 Java 自身的启动运行开销性能瓶颈,就必须在传统的 Java 程序编译启动执行过程之外,另辟蹊径。
Java 静态编译技术是一种激进的 AOT 技术,通过单独的编译阶段将 Java 程序编译为本地代码,在运行时无需传统 Java 虚拟机和运行时环境,只需操作系统类库支持即可。目前较成熟、受到业界广泛关注的开源 Java 静态编译器是由 Oracle 开发的 GraalVM SubstrateVM (以下简称 SVM),图 1 展示了目前基于 SVM 的阿里巴巴静态编译项目的基本编译过程。
SVM 遵循封闭性假设(closed-world assumption, 即运行时所需的所有内容必须在编译时提供),将应用程序代码及其所依赖的三方 jar 文件和 Alibaba Dragonwell 代码编译为本地机器码,同时也将用 Java 编写的 Runtime 编译为机器码,两者被编到一个 elf 可执行文件或 so 共享库文件中,实现代码的自举。运行阶段只需直接执行 elf 文件,或者在其他 C/C++ 程序中调用 so 文件中发布的 API 即可。
可以认为静态编译技术实现了 Java 语言与原生 Native 程序的“合体”,将原本的 Java 程序编译成为了一个自举的具有 Java 行为的原生 Native 程序,由此兼有 Java 程序和原生 Native 程序的优点。
从 Java 程序角度看,经过静态编译后 Java 程序具有更快的启动速度、更低的内存占用、更小的发布体积,在云上部署时从服务拉起到响应用户的请求所需的时间更短,非常适合云原生的需求。
以蚂蚁开源中间件项目 sofastack 的服务注册中心 Meta 节点应用 为例,该项目验证了静态编译在 Serverless 应用上的可行性。图 2 展示了以传统 Java 方式运行和静态编译程序运行的几项数据的比较。
左上图的服务启动时间指服务完全启动到可以接受用户请求的状态所需的时间;右上图的可执行文件大小指包含了运行 Java 程序所需的所有依赖资源的 fat jar 包和静态编译出的 elf 可执行文件的大小比较,虚线框代表没有显式打入 fat jar 的 Alibaba Dragonwell;底图的运行时内存消耗则是以相同压力请求测试时,两个版本应用各自的内存使用情况。从图中可见静态编译的代码在这些指标上较传统 Java 程序有质的提升,服务启动时间降低了 17 倍,可执行文件大小降低了 3.4 倍,运行时内存降低了一半。
从 Native 原生代码角度看,在静态编译的帮助下开发人员可以用 Java 编写出 Native 代码,较以往的 C/C++ 开发效率更高、为同一业务维护多个语言版本的成本更低。
在这方面的探索中,阿里巴巴发布了静态编译版本的 RocketMQ 客户端,为 RocketMQ 进一步 Serverless 化提供了语言无关的运行时保障,实现了一套内核的多语言支撑。图 3 展示了 RocketMQ 客户端的多语言维护的示意。
在引入静态编译技术之前,同一需求需要用两种语言分别实现不同的 SDK,最终运行在不同的环境中,开发和维护成本高。在静态编译的支持下,同一需求只用 Java 单一语言开发,然后沿图中绿色路径静态编译得到 Native Code。当然此外还需要一部分适配工作,将拟暴露的 API 用 SVM 定义的 C-Java 转化协议包装起来。
与先前单独编写 C++ 客户端相比,静态编译方案给多语言客户端的快速开发和后续管理带来了诸多优势:
目前静态编译版本的 RocketMQ 客户端已稳定可靠地服务阿里巴巴集团内部以及阿里云超过 10+ 业务场景,包括网络延迟在内的客户端启动时间相比 Java 原生客户端提升 30%,达到与 C++ 版应用同等的启动性能。
然而世界上没有银弹,静态编译技术也不例外。兼有 Java 和原生 Native 代码两者的优势是以牺牲部分 Java 动态特性为代价换取来的,表格 1 列出了在 SVM 中受限制的 Java 特性。封闭性假设要求在编译时必须获取运行时所需的全部信息,这与 Java 的动态性相矛盾,是静态编译对传统 Java 限制最大的部分。代码 Native 化是指由于丢弃了 Bytecode,传统上基于 Bytecode 的特性便不再被支持。缺少工具指缺少调试静态编译后代码的工具,如 Heap Dump、Thread Dump、代码调试等功能在 SVM 社区版中都没有提供。
原因分类 | 存在限制的特性 | 具体解释 |
---|---|---|
封闭性 | 反射、动态类加载、动态代理、JNI 反射、序列化反序列化、MethodHandler | 编译时需要完全掌握运行时的信息 |
代码 native 化 | 插桩、JVMTI、agent | 编译为本地代码后已不存在 bytecode |
缺少工具 | Heap dump,Thread dump、调试 | 部分存在于企业版中,社区版为提供 |
可以看到 SVM 静态编译仅支持了传统 Java 特性的子集,而且对生产级的应用还缺少良好的工具支持。阿里巴巴正在结合自己的业务场景与 SVM 开源社区紧密合作,以 Alibaba Dragonwell 和 SVM 为基础,积极探索如何缩小静态编译和传统 Java 之间的鸿沟。
一方面逐步扩展静态编译能够支持的特性子集,使静态编译能够适用于更多的业务场景。比如针对 JDK 原生序列化反序列化的需求,通过预执行输出动态加载的类,将其加入静态编译的代码范围中,并自动生成需要的反射配置信息提供给编译器,最终实现对序列化反序列化特性的支持。另一方面探索适合面向静态编译的 Java 编程模型,帮助开发人员便捷地开发出适合静态编译的 Java 程序。
阿里巴巴对 Alibaba Dragonwell 进行静态化剪裁,一边定义了使用反射、动态类加载等不完全支持特性时的规则,并通过 javac 做强制检查。一边将静态编译完全不能支持的特性从 Alibaba Dragonwell 中去除,帮助开发人员以静态编译的思维开发新的 Java 程序。最后增加了对 Thread dump,Heap dump 支持,并开发了针对阿里巴巴业务场景的更有效的 GC 算法。这些对业务场景的不断实验和打磨,在成熟后也会贡献反哺到社区。
静态编译技术是一种兼具传统 Java 程序和本地原生代码程序二者优点的技术。以静态编译的原生代码助力云原生应用,既保持了传统 Java 的开发流程和效率,又消除了 Java 自身固有的性能瓶颈,显著改善服务拉起响应请求的速度,阿里巴巴也通过自身实践,证明了静态编译技术在实际生产中的可行性。
在长期的 Java 开发实践中,阿里巴巴对 OpenJDK 不断优化总结开发出了其下游 Alibaba Dragonwell JDK,现已运行在 100,000+ 服务器上,为阿里巴巴的各项业务提供基础服务。相信随着 Alibaba Dragonwell 静态编译版本进一步成熟,将能够支持更多原生代码应用部署上云。
作者介绍:
林子熠,阿里巴巴集团 JVM 团队技术专家。上海交通大学软件工程专业工学博士,毕业后加入华为编译器与编程语言实验室参与方舟编译器的研发工作,现于阿里巴巴 JVM 团队主要负责 Java 静态编译技术在 Java 服务端的开发与应用。