向原生迈进

对不需要长时间运行的,或者小型化的应用而言,Java(而不是指 Java ME)天生就带有一些劣势,这里并不光是指跑个 HelloWorld 也需要百多兆的 JRE 之类的问题,而更重要的是指近几年从大型单体应用架构向小型微服务应用架构发展的技术潮流下,Java 表现出来的不适应。

在微服务架构的视角下,应用拆分后,单个微服务很可能就不再需要再面对数十、数百 GB 乃至 TB 的内存,有了高可用的服务集群,也无须追求单个服务要 7×24 小时不可间断地运行,它们随时可以中断和更新;但相应地,Java 的启动时间相对较长、需要预热才能达到最高性能等特点就显得相悖于这样的应用场景。在无服务架构中,矛盾则可能会更加突出,比起服务,一个函数的规模通常会更小,执行时间会更短,当前最热门的无服务运行环境 AWS Lambda 所允许的最长运行时间仅有 15 分钟。

一直把软件服务作为重点领域的 Java 自然不可能对此视而不见,在最新的几个 JDK 版本的功能清单中,已经陆续推出了跨进程的、可以面向用户程序的类型信息共享(Application Class Data Sharing,AppCDS,允许把加载解析后的类型信息缓存起来,从而提升下次启动速度,原本 CDS 只支持 Java 标准库,在 JDK 10 时的 AppCDS 开始支持用户的程序代码)、无操作的垃圾收集器(Epsilon,只做内存分配而不做回收的收集器,对于运行完就退出的应用十分合适)等改善措施。而酝酿中的一个更彻底的解决方案,是逐步开始对提前编译(Ahead of Time Compilation,AOT)提供支持。

提前编译是相对于即时编译的概念,提前编译能带来的最大好处是 Java 虚拟机加载这些已经预编译成二进制库之后就能够直接调用,而无须再等待即时编译器在运行时将其编译成二进制机器码。理论上,提前编译可以减少即时编译带来的预热时间,减少 Java 应用长期给人带来的“第一次运行慢”不良体验,可以放心地进行很多全程序的分析行为,可以使用时间压力更大的优化措施。

但是提前编译的坏处也很明显,它破坏了 Java“一次编写,到处运行”的承诺,必须为每个不同的硬件、操作系统去编译对应的发行包。也显著降低了 Java 链接过程的动态性,必须要求加载的代码在编译期就是全部已知的,而不能再是运行期才确定,否则就只能舍弃掉已经提前编译好的版本,退回到原来的即时编译执行状态。

早在 JDK 9 时期,Java 就提供了实验性的 Jaotc 命令来进行提前编译,不过多数人试用过后都颇感失望,大家原本期望的是类似于 Excelsior JET 那样的编译过后能生成本地代码完全脱离 Java 虚拟机运行的解决方案,但 Jaotc 其实仅仅是代替掉即时编译的一部分作用而已,仍需要运行于 HotSpot 之上。

直到Substrate VM出现,才算是满足了人们心中对 Java 提前编译的全部期待。Substrate VM 是在 Graal VM 0.20 版本里新出现的一个极小型的运行时环境,包括了独立的异常处理、同步调度、线程管理、内存管理(垃圾收集)和 JNI 访问等组件,目标是代替 HotSpot 用来支持提前编译后的程序执行。它还包含了一个本地镜像的构造器(Native Image Generator)用于为用户程序建立基于 Substrate VM 的本地运行时镜像。这个构造器采用指针分析(Points-To Analysis)技术,从用户提供的程序入口出发,搜索所有可达的代码。在搜索的同时,它还将执行初始化代码,并在最终生成可执行文件时,将已初始化的堆保存至一个堆快照之中。这样一来,Substrate VM 就可以直接从目标程序开始运行,而无须重复进行 Java 虚拟机的初始化过程。但相应地,原理上也决定了 Substrate VM 必须要求目标程序是完全封闭的,即不能动态加载其他编译期不可知的代码和类库。基于这个假设,Substrate VM 才能探索整个编译空间,并通过静态分析推算出所有虚方法调用的目标方法。

Substrate VM 带来的好处是能显著降低了内存占用及启动时间,由于 HotSpot 本身就会有一定的内存消耗(通常约几十 MB),这对最低也从几 GB 内存起步的大型单体应用来说并不算什么,但在微服务下就是一笔不可忽视的成本。根据 Oracle 官方给出的测试数据,运行在 Substrate VM 上的小规模应用,其内存占用和启动时间与运行在 HotSpot 相比有了 5 倍到 50 倍的下降,具体结果如下图所示:

内存占用对比内存占用对比

启动时间对比启动时间对比

Substrate VM 补全了 Graal VM“Run Programs Faster Anywhere”愿景蓝图里最后的一块拼图,让 Graal VM 支持其他语言时不会有重量级的运行负担。譬如运行 JavaScript 代码,Node.js 的 V8 引擎执行效率非常高,但即使是最简单的 HelloWorld,它也要使用约 20MB 的内存,而运行在 Substrate VM 上的 Graal.js,跑一个 HelloWorld 则只需要 4.2MB 内存而已,且运行速度与 V8 持平。Substrate VM 的轻量特性,使得它十分适合于嵌入至其他系统之中,譬如Oracle 自家的数据库就已经开始使用这种方式支持用不同的语言代替 PL/SQL 来编写存储过程。