GraalVM与Java静态编译:原理与应用
上QQ阅读APP看书,第一时间看更新

2.1 Oracle GraalVM

Oracle实验室的研究人员最早希望用Java开发出一款新的实时编译器以取代OpenJDK HotSpot中晦涩难懂、无法重构的C1编译器,这款新型的编译器就逐渐发展成为现在的GraalVM编译器。后来Oracle数据库产品又提出了在数据库中直接执行某种程序的代码片段的需求,由此拉开了基于GraalVM编译器的多语言支持的大幕,并逐渐演化成为现在的项目。

2.1.1 GraalVM是什么

GraalVM是Oracle推出的基于Java开发的开源高性能多语言运行时平台,其目标是打破不同语言之间的藩篱,在统一的运行时平台上实现跨语言的程序交互。图2-1展示了GraalVM开放的生态,上方是目前GraalVM支持的所有语言,包括基于JVM的语言Kotlin、Scala和Java(图2-1左上),JavaScript、Ruby、R、Python以及可以通过LLVM中转的C、C++和Rust(图2-1右上)。

029-01

图2-1 GraalVM开放平台示意图[1]

这些语言可以作为客座语言(guest language)运行在主语言(host language)也就是Java的平台上,客座语言程序与主语言程序共享同一个运行时,在同一个内存空间里交互数据。图2-1的下方列出了GraalVM的适用场景,可以看到其既可以作为组件嵌入OpenJDK(用GraalVM编译器代替OpenJDK的C2编译器做JIT编译)和Node.js,也可以在Oracle数据库中支持直接运行内嵌的JavaScript代码(自Oracle 21c版本[2]),或者作为独立的应用程序运行(Java静态编译程序)。

GraalVM分为免费的社区版(Community Edition,CE)和收费的企业版(Enterprise Edition,EE),两者的基本功能是相同的,但是EE版的性能和安全性更高,并为用户提供不间断的24小时支持服务。Oracle云服务的客户可以免费获得EE版。

图2-2展示了GraalVM实现多语言支持的框架结构示意图。Truffle是GraalVM里的解释器实现框架,开发人员可以使用Truffle提供的API快速用Java实现一种语言的解释器,从而实现了在JVM平台上运行其他语言的效果。更进一步,Truffle中还给出了指导JIT编译的Profiling接口和编译优化接口,使得用Truffle实现的解释器还能将频繁执行的热点函数送入JVM的GraalVM编译器执行运行时的实时编译。Truffle为用户提供了在GraalVM上快速实现新语言支持的能力,无论是GraalVM社区还是个人开发者都可以在Truffle框架的支持下为GraalVM平台实现新语言扩展。

030-01

图2-2 GraalVM多语言支持示意图[3]

目前GraalVM已经基于Truffle实现了多种语言的解释器,如WA(即wasm,WebAsse-mbly)、JS、Python、R、Ruby等,见图2-2顶部靠右。图2-2右上的C++和C语言通过LLVM编译为LLVM的中间语言bitcode,然后由GraalVM的Sulong解释器解释执行bitcode。

自GraalVM 21.0开始,JVM类型的语言(图2-2圈中的)既可以通过名为Java on Truffle的组件由Truffle统一执行,也可以按旧有的通过JVM解释器进而JIT编译的方式执行。Java on Truffle是基于Truffle实现的完全遵循JVM 8和11规范的Java字节码解释器。因为Truffle是用Java实现的,所以可以认为它实现了纯Java的自举。Java on Truffle目前的性能还不够好,但它为Java世界带来了更多更有想象力的可能性,例如混用JDK新旧版本的能力。Java on Truffle自己遵循JDK8,但是能够在其上运行遵循JDK11的程序,或者反过来。这样通过Java on Truffle就可以直接在JDK8的旧环境上使用JDK11才有的库或者特性,而不需要承担整体升级的风险和成本。

各种支持语言在GraalVM平台上的性能表现并不一致,图2-3引用自GraalVM官方在2017年程序语言和程序系统研究领域的国际顶级会议PLDI(Programming Language Design and Implementation)发表的学术报告,其中列出了几种语言在GraalVM平台上和原生平台上执行的性能对比。纵坐标是GraalVM对原生的加速比:1表示性能相当;大于1表示GraalVM更好;小于1表示GraalVM更差。从图中可以看到,Java和Scala比原生的略好,这里参与对比的是图2-2最左边的GraalVM支持JVM类型语言的方式,而不是Java on Truffle,因此实际对比的是GraalVM编译器和OpenJDK的C2 JIT编译器的性能。Ruby和R语言的性能有大幅提高,这是因为它们原生只有解释执行而没有JIT编译。Native使用的是LLVM的提前编译(AOT)器,JavaScript是JS V8编译器,GraalVM比它们的性能要差。

031-01

图2-3 GraalVM多语言运行时性能加速比[4]

在目前多语言运行时性能不一的情况下,GraalVM在多语言支持方面体现出的最大优势就是统一的运行时环境。基于GraalVM开发的应用程序可以在Java运行时环境内执行各种其他语言,减小了多语言交互的开销,扩大了Java语言的应用范围。

2.1.2 GraalVM静态编译优点

除多语言支持以外,GraalVM的最大特性就是本书的主角——Java静态编译。GraalVM实现了Java静态编译的编译器、编译框架和运行时等一整套完整的工具链。GraalVM的Java静态编译器就是图2-2中位于GraalVM架构底层的GraalVM JIT Compiler,这意味着GraalVM统一了JIT和AOT编译器,而这个编译器已经在Twitter和Facebook公司的生产环境中用于Java程序的JIT编译了,其可靠性和性能都已经过了实践的检验。GraalVM编译器目前已经支持x86 64位平台和AArch64平台。静态编译框架和运行时是由Substrate VM子项目实现的,目前的运行时兼容OpenJDK的运行时实现。

GraalVM的静态编译方案的基本实现思路是由用户指定编译入口(比如main函数),然后编译框架从入口开始静态分析程序的可达范围,编译器将分析出的可达函数和SubstrateVM中的运行时支持代码一起编译为一个被称为native image的二进制本地代码文件。根据用户的参数设置,这个本地代码文件既可以是ELF可执行文件,也可以是动态共享库文件。

这种方案有效地控制了编译的范围,从而控制了编译后的代码膨胀问题。当语言的抽象程度减弱时,描述同一件事情所需的代码量就会增大,所以当一段Java的字节码被编译为本地代码时,代码行数会大幅增加,造成代码量的膨胀,因此控制代码的膨胀程度是静态编译必须考虑的问题。虽然编译器会通过各种优化技术减少符号数量,降低代码行数,但最有效的方法是从源头上控制拟编译的代码范围。图2-4给出了这种方案的示意图,图中App块代表了应用程序,JDK块代表了JDK的类库,Libs代表了第三方库,一般Java程序用到的代码都可以分为这三个部分。但是Java应用程序其实并不会用到这三者中的全部代码,而只会用到其中的一个子集。如图中所示,从App的入口函数进入后,各个点代表了可达的函数,箭头代表了调用方向,云形图代表了多个函数组成的模块。显而易见,从入口函数可达的代码只是总体的一个子集。GraalVM通过静态分析的指向分析(points-to analysis)、控制流分析(control flow analysis)以及调用图分析(call graph analysis)等技术找到可达的代码范围。图2-4中指向native image框的虚线就代表编译进native image的可达函数。

032-01

图2-4 GraalVM编译内容示意图

GraalVM静态编译方案还实现了多种运行时优化,典型的有对Java静态初始化过程的优化。传统Java模型中的类是在第一次被用到时初始化的,之后每次用到时还要再检查是否已经被初始化过。GraalVM则将其优化为在编译时初始化,只要编译时初始化成功,就无须在运行时做初始化检查,由此可以带来2倍左右的性能提升。但并不是所有的类都可以在编译时初始化,假如一个类的初始化函数里启动了一个线程或者获取了当前的时间,那么这种运行时行为就不能在编译时初始化。GraalVM设计了一套判定类的初始化时机的规则,以指导初始化优化。本书会在后续章节详细介绍这些运行时的实现和优化。

2.1.3 GraalVM静态编译缺点

任何技术优点都有其相应的代价,GraalVM静态编译拥有上述优点的同时,也不可避免地有着多个缺点。

第一,静态分析是资源密集型计算,需要消耗大量的内存、CPU和时间。GraalVM早期版本的静态分析的时间复杂度与大致程序规模的平方成正比。经过不懈改进,现在GraalVM通过并发处理,充分调用硬件的CPU资源,在性能较高的服务器上可以大幅降低静态分析时间。一般来说,人们对离线编译的资源消耗的容忍度较高,但过长的时间会给用户带来开发、测试、验证和部署等过程的效率方面的顾虑,从而限制了可能的应用场景。

第二,静态分析对反射的分析能力非常有限,目前的学术研究领域往往会在静态分析中略过反射,或者通过反射的输入字符串做有限的分析。对于实际中存在的大量反射,GraalVM只能通过额外配置的方式加以解决。但是当代码发生变化时,反射信息就有可能发生变化,配置也需要随之改变,由此增加了维护成本和适配难度。而且配置难以覆盖全部可能性,如有遗漏则会造成运行时错误,这对静态编译的广泛应用也是一大阻碍。

第三,静态编译后程序的运行时性能低于传统Java经过JIT编译后的峰值性能。这主要是由两方面的原因造成的。

1)编译方面:虽然启动性能非常好,而且静态编译器也依据静态分析的结果执行了各种编译优化,但是因为缺少运行时的程序动态执行画像数据,不能执行更有针对性的JIT编译优化,因此虽然静态编译程序的性能表现稳定,但是没有JIT编译后的Java程序的峰值性能高。

2)垃圾回收方面:传统Java有更完善的GC(垃圾回收)机制,可以针对不同的场景选择性能更佳的GC策略,但是GraalVM的垃圾回收机制较简单,其企业版提供了性能较好的G1GC,但是社区版只提供了最简单的顺序GC(serial GC),在运行内存使用情况较复杂的程序时,GC的性能与传统Java有明显差距。

这三点是GraalVM静态编译特有的缺点,此外Java静态编译技术共有的缺点已经在1.3.3节介绍,在此就不再重复。

2.1.4 GraalVM发展分析

总体而言,Oracle GraalVM是目前国际上成熟度最高的Java静态编译技术,已经在行业中产生了较大的影响,包括Facebook、Twitter、RedHat、ARM、Spring、亚马逊和阿里巴巴等在内的国内外大型厂商均对其具有浓厚的兴趣,并从各个方面积极参与并推动其技术发展。比如GraalVM社区成立了用于讨论GraalVM发展方向的顾问委员会,囊括了来自Oracle、亚马逊、Twitter、RedHat、ARM和阿里巴巴的委员代表。

一些主流开源Java框架社区(如Spring、Tomcat等)也纷纷探索向GraalVM静态编译靠拢的静态化支持方案,如开始在它们的代码中尽量支持GraalVM静态编译特性,按GraalVM的规范开发静态编译适配框架等。Spring社区推出了面向GraalVM的静态编译的spring-native项目[5],用于适配Spring中的静态编译不友好内容,以降低用户将Spring项目静态编译的适配难度。与之类似,Tomcat则推出了Tomcat Stuffed组件[6]

就GraalVM静态编译技术的现状而言,该技术非常适合快起快停、不会长时间运行的Java Serverless应用。亚马逊已经在其AWS Java SDK[7]中增加了对GraalVM静态编译技术的支持,AWS用户可以在AWS Java SDK上静态编译自己的Java应用。

可以预见,GraalVM会在开源社区的广泛支持下继续发扬其优点并逐步改正其缺点,进一步实现静态编译技术的实用化。


[1]图片来自GraalVM官方网站:https://www.GraalVM.org/docs/introduction/

[2]参见https://blogs.oracle.com/database/introducing-oracle-database-21c

[3]图片引用自https://medium.com/GraalVM/java-on-truffle-going-fully-metacircular-215531e3f840

[4]图片引用自http://lafo.ssw.uni-linz.ac.at/papers/2017_PLDI_GraalTutorial.pdf

[5]参见https://github.com/spring-projects-experimental/spring-native

[6]参见https://github.com/apache/tomcat/tree/master/modules/stuffed

[7]参见https://aws.amazon.com/cn/sdk-for-java