You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

10 KiB

36 | SubstrateVMAOT编译框架

今天我们来聊聊GraalVM中的Ahead-Of-TimeAOT编译框架SubstrateVM。

先来介绍一下AOT编译所谓AOT编译是与即时编译相对立的一个概念。我们知道即时编译指的是在程序的运行过程中将字节码转换为可在硬件上直接运行的机器码并部署至托管环境中的过程。

而AOT编译指的则是程序运行之前,便将字节码转换为机器码的过程。它的成果可以是需要链接至托管环境中的动态共享库,也可以是独立运行的可执行文件。

狭义的AOT编译针对的目标代码需要与即时编译的一致也就是针对那些原本可以被即时编译的代码。不过我们也可以简单地将AOT编译理解为类似于GCC的静态编译器。

AOT编译的优点显而易见我们无须在运行过程中耗费CPU资源来进行即时编译而程序也能够在启动伊始就达到理想的性能。

然而与即时编译相比AOT编译无法得知程序运行时的信息因此也无法进行基于类层次分析的完全虚方法内联或者基于程序profile的投机性优化并非硬性限制我们可以通过限制运行范围或者利用上一次运行的程序profile来绕开这两个限制。这两者都会影响程序的峰值性能。

Java 9引入了实验性AOT编译工具jaotc。它借助了Graal编译器将所输入的Java类文件转换为机器码并存放至生成的动态共享库之中。

在启动过程中Java虚拟机将加载参数-XX:AOTLibrary所指定的动态共享库,并部署其中的机器码。这些机器码的作用机理和即时编译生成的机器码作用机理一样,都是在方法调用时切入,并能够去优化至解释执行。

由于Java虚拟机可能通过Java agent或者C agent改动所加载的字节码或者这份AOT编译生成的机器码针对的是旧版本的Java类因此它需要额外的验证机制来保证即将链接的机器码的语义与对应的Java类的语义是一致的。

jaotc使用的机制便是类指纹class fingerprinting。它会在动态共享库中保存被AOT编译的Java类的摘要信息。在运行过程中Java虚拟机负责将该摘要信息与已加载的Java类相比较一旦不匹配则直接舍弃这份AOT编译的机器码。

jaotc的一大应用便是编译java.base module也就是Java核心类库中最为基础的类。这些类很有可能会被应用程序所调用但调用频率未必高到能够触发即时编译。

因此如果Java虚拟机能够使用AOT编译技术将它们提前编译为机器码那么将避免在执行即时编译生成的机器码时因为“不小心”调用到这些基础类而需要切换至解释执行的性能惩罚。

不过今天要介绍的主角并非jaotc而是同样使用了Graal编译器的AOT编译框架SubstrateVM。

SubstrateVM的设计与实现

SubstrateVM的设计初衷是提供一个高启动性能、低内存开销并且能够无缝衔接C代码的Java运行时。它与jaotc的区别主要有两处。

第一SubstrateVM脱离了HotSpot虚拟机并拥有独立的运行时包含异常处理同步线程管理内存管理垃圾回收和JNI等组件。

第二SubstrateVM要求目标程序是封闭的即不能动态加载其他类库等。基于这个假设SubstrateVM将探索整个编译空间并通过静态分析推算出所有虚方法调用的目标方法。最终SubstrateVM会将所有可能执行到的方法都纳入编译范围之中从而免于实现额外的解释执行器。

有关SubstrateVM的其他限制你可以参考这篇文档

从执行时间上来划分SubstrateVM可分为两部分native image generator以及SubstrateVM运行时。后者SubstrateVM运行时便是前面提到的精简运行时经过AOT编译的目标程序将跑在该运行时之上。

native image generator则包含了真正的AOT编译逻辑。它本身是一个Java程序将使用Graal编译器将Java类文件编译为可执行文件或者动态链接库。

在进行编译之前native image generator将采用指针分析points-to analysis从用户提供的程序入口出发探索所有可达的代码。在探索的同时它还将执行初始化代码并在最终生成可执行文件时将已初始化的堆保存至一个堆快照之中。这样一来SubstrateVM将直接从目标程序开始运行而无须重复进行Java虚拟机的初始化。

SubstrateVM主要用于Java虚拟机语言的AOT编译例如Java、Scala以及Kotlin。Truffle语言实现本质上就是Java程序而且它所有用到的类都是编译时已知的因此也适合在SubstrateVM上运行。不过它并不会AOT编译用Truffle语言写就的程序。

SubstrateVM的启动时间与内存开销

SubstrateVM的启动时间和内存开销非常少。我们曾比较过用C和用Java两种语言写就的Hello World程序。C程序的执行时间在10ms以下内存开销在500KB以下。在HotSpot虚拟机上运行的Java程序则需要40ms内存开销为24MB。

使用SubstrateVM的Java程序的执行时间则与C程序持平内存开销在850KB左右。这得益于SubstrateVM所保存的堆快照以及无须额外初始化直接执行目标代码的特性。

同样我们还比较了用JavaScript编写的Hello World程序。这里的测试对象是Google的V8以及基于Truffle的Graal.js。这两个执行引擎都涉及了大量的解析代码以及执行代码因此可以当作大型应用程序来看待。

V8的执行效率非常高能够与C程序的Hello World相媲美但是它使用了约18MB的内存。运行在HotSpot虚拟机上的Graal.js则需要650ms方能执行完这段JavaScript的Hello World程序而且内存开销在120MB左右。

运行在SubstrateVM上的Graal.js无论是执行时间还是内存开销都十分优越分别为10ms以下以及4.2MB。我们可以看到它在运行时间与V8持平的情况下内存开销远小于V8。

由于SubstrateVM的轻量特性它十分适合于嵌入至其他系统之中。Oracle Labs的另一个团队便是将Truffle语言实现嵌入至Oracle数据库之中这样就可以在数据库中运行任意语言的预储程序stored procedure。如果你感兴趣的话可以搜索Oracle Database Multilingual EngineMLE或者参阅这个网址。我们团队也在与MySQL合作开发MySQL MLE详情可留意我们在今年Oracle Code One的讲座

Metropolis项目

去年OpenJDK推出了Metropolis项目他们希望可以实现“Java-on-Java”的远大目标。

我们知道目前HotSpot虚拟机的绝大部分代码都是用C++写的。这也造就了一个非常有趣的现象那便是对Java语言本身的贡献需要精通C++。此外随着HotSpot项目日渐庞大维护难度也逐渐上升。

由于上述种种原因使用Java来开发Java虚拟机的呼声越来越高。Oracle的架构师John Rose便提出了使用Java开发Java虚拟机的四大好处

  1. 能够完全控制编译Java虚拟机时所使用的优化技术
  2. 能够与C++语言的更新解耦合;
  3. 能够减轻开发人员以及维护人员的负担;
  4. 能够以更为敏捷的方式实现Java的新功能。

当然Metropolis项目并非第一个提出Java-on-Java概念的项目。实际上JikesRVM项目Maxine VM项目都已用Java完整地实现了一套Java虚拟机后者的即时编译器C1X便是Graal编译器的前身

然而Java-on-Java技术通常会干扰应用程序的垃圾回收、即时编译优化从而严重影响Java虚拟机的启动性能。

举例来说目前使用了Graal编译器的HotSpot虚拟机会在即时编译过程中生成大量的Java对象这些Java对象同样会占据应用程序的堆空间从而使得垃圾回收更加频繁。

另外Graal编译器本身也会触发即时编译并与应用程序的即时编译竞争编译线程的CPU资源。这将造成应用程序从解释执行切换至即时编译生成的机器码的时间大大地增长从而降低应用程序的启动性能。

Metropolis项目的第一个子项目便是探索部署已AOT编译的Graal编译器的可能性。这个子项目将借助SubstrateVM技术把整个Graal编译器AOT编译为机器码。

这样一来在运行过程中Graal编译器不再需要被即时编译因此也不会再占据可用于即时编译应用程序的CPU资源使用Graal编译器的HotSpot虚拟机的启动性能将得到大幅度地提升。

此外由于SubstrateVM编译得到的Graal编译器将使用独立的堆空间因此Graal编译器在即时编译过程中生成的Java对象将不再干扰应用程序所使用的堆空间。

目前Metropolis项目仍处于前期验证阶段如果你感兴趣的话可以关注之后的发展情况。

总结与实践

今天我介绍了GraalVM中的AOT编译框架SubstrateVM。

SubstrateVM的设计初衷是提供一个高启动性能、低内存开销和能够无缝衔接C代码的Java运行时。它是一个独立的运行时拥有自己的内存管理等组件。

SubstrateVM要求所要AOT编译的目标程序是封闭的即不能动态加载其他类库等。在进行AOT编译时它会探索所有可能运行到的方法并全部纳入编译范围之内。

SubstrateVM的启动时间和内存开销都非常少这主要得益于在AOT编译时便已保存了已初始化好的堆快照并支持从程序入口直接开始运行。作为对比HotSpot虚拟机在执行main方法前需要执行一系列的初始化操作因此启动时间和内存开销都要远大于运行在SubstrateVM上的程序。

Metropolis项目将运用SubstrateVM项目逐步地将HotSpot虚拟机中的C++代码替换成Java代码从而提升HotSpot虚拟机的可维护性也加快新Java功能的开发效率。


今天的实践环节,请你参考我们官网的SubstrateVM教程AOT编译一段Java-Kotlin代码。