Introduction
近期开源的Substrate VM(下称SVM)是一个构建在Graal Compiler上的,支持ahead-of-time (AOT) compilation的编译及运行框架。它的设计初衷是提供一个高startup performance,低memory print,以及能无缝衔接C代码(与JNI相较)的runtime,并能完美适配Truffle语言实现。
这篇笔记将通过一些例子介绍SVM的实现及对应的源代码。如对SVM的架构,算法,性能等感兴趣,可参考如下链接:
- Overview: http://www.oracle.com/technetwork/java/jvmls2015-wimmer-2637907.pdf
- Video: https://www.youtube.com/watch?v=5BMHIeMXTqA
- Sources: https://github.com/graalvm/graal/tree/master/substratevm
- GraalVM: http://www.oracle.com/technetwork/oracle-labs/program-languages/
- Graal tutorial: http://lafo.ssw.uni-linz.ac.at/papers/2017_PLDI_GraalTutorial.pdf
- Publications: https://github.com/graalvm/graal/blob/master/docs/Publications.md
Hello World
从执行时间上来看,SVM可划分为两部分:native image generator及SVM runtime。前者可看成一个普通的Java程序,用以生成可执行文件或动态链接库;后者是一个精简的runtime(与HotSpot相对应),包含异常处理,同步,线程管理,内存管理,Java Native Interface(JNI)等组件。SVM要求所运行的程序是封闭的,即不可动态加载其他类库等。这部分封闭的程序会被native image generator编译,并与SVM runtime相链接,继而形成一个自包含的二进制文件。
我们在Oracle Technology Network(OTN)上迭代发布的GraalVM版本,其中便包含native image generator(native-image
)。GraalVM附载的语言实现,如js
,ruby
,python
,及lli
(LLVM bitcode interpreter),乃至native-image
本身,均是由该工具产生。下面我们将借助这一工具编译一个简单的Hello World程序:
1 | $ export GRAALVM_HOME=/PATH/TO/GRAALVM/HOME |
native-image
工具分为多个步骤,其中最重要的是pointsto analysis及AOT compilation。上述指令最终将在当前目录下生成指定名称(-H:Name=
)的可执行文件,其入口为JavaMainWrapper.run
。严格意义上讲入口是调用该方法的桩程序 —— SVM会为所有被@CEntryPoint
注解的方法生成桩程序,完成由C空间调用至Java空间的准备工作。在JavaMainWrapper.run
方法调用所指定主类(-H:Class=
)的主方法之前并没有出现类似HotSpot中冗杂的初始化操作。实际上,native-image
会维护一个NativeImageHeap
(可看成在运行目标程序前Java堆的一个快照),并在生成二进制文件时直接烧录至其__DATA
段中。因此,相较于HotSpot而言SVM运行Java程序的memory footprint更小。
native-image
工具耗时较长,这是由于其实际工作是由一个运行在HotSpot上的子程序完成,因而受制于其启动性能较差的缺陷(解释执行)。之所以这部分代码没有被AOT编译,是因为native image generator依赖于HotSpot的类加载机制来载入需要编译的Java类。
编译而成的二进制文件的__TEXT
段包含HelloWorld.main
,JDK中所有可能被调用的方法,以及SVM runtime相应的代码。这部分runtime代码提供了简介中提及的各类runtime组件,但代价是生成的二进制文件大小远超于等价的helloworld.c编译而成的二进制文件:
1 | // macOS-specific |
通过追加-H:Debug=
参数,native-image
生成的二进制文件将包含调试信息,有助于我们了解被链接的所有Java方法(下面罗列调用JavaMainWrapper.run
的桩程序的多个别名):
1 | $ $GRAALVM_HOME/bin/native-image -H:Class=HelloWorld -H:Name=helloworld -H:Debug=1 |
除了提供主函数生成可执行文件,我们还可以借助-H:Kind=SHARED_LIBRARY
及@CEntryPoint
来生成动态链接库,如下所示:
1 | $ echo '// HelloWorld.java |
上述指令将在当前目录下生成两个文件:动态链接库helloworld.dylib
及对应的头文件helloworld.h
。后者包含生成的入口函数及一系列用以操作SVM线程的API(暂无文档)。这两者的使用方式和常规动态链接库一致:
1 | $ echo '// helloworld.c |
Build from sources
SVM源代码的编译需借助mx
工具及支持JVMCI
的JDK,如labsjdk(注:请下载页面最下方的labsjdk,直接使用GraalVM可能会导致共有类的编译问题)。下载完成后将JAVA_HOME
指向labsjdk路径。
1 | $ git clone https://github.com/graalvm/graal.git |
指令mx image
除自动将目标文件写入svmbuild
目录外等同于native-image
工具,例如同样需耗费较长的时间。SVM提供了另一种非缺省的解决方案,即让native image generator以server的形式运行(mx image_server_start
),接受请求(mx image -server ...
)并返回编译结果。充分预热的native image generator,可将编译效率提升至三倍以上。
对Truffle语言实现而言,由于运行需加载的Java类仅限于语言解释器及其依赖库,因此可以被SVM完美支持。下述指令将使用SVM编译ruby语言实现TruffleRuby:
1 | $ cd /PATH/TO/GRAAL/REPO/.. |
编译TruffleRuby的指令较为复杂,因此我们提供了mx image -ruby
的封装。它将在包含graal
的git repo的路径下寻找truffleruby
的repo,编译其通过mx build
生成的jar包。
SVM Internals
前段时间碰到一个很难复现的bug,极小概率在graal-js
试图编译js代码时触发。经调试得知:1. 仅在native image中复现;2. 触发原因是System.identityHashCode(Object)
对同一对象返回不同的值。在SVM中同一段代码可能被两个不同的runtime执行,即在生成二进制文件时由HotSpot运行(hosted mode),或在SVM runtime中运行。当时我推测NativeImageHeap
中缓存了某些对象的identityHashCode(例如在HashMap
中),而在SVM runtime中再对这些对象求identityHashCode则会得到不同的结果。但这一猜想被SVM组的开发者否定了,因为SVM已经有机制预防这种哈希值不一致的情况出现。
SVM无法直接使用HotSpot的runtime代码,因此所有的native方法皆需有对应的实现。SVM的解决方案是在Java层提供替代,并在编译过程中将编译内容替换掉。这些替代方法使用@TargetClass
和@Substitute
注解,如System.identityHashCode(Object)
的替代:
1 | // 修复前 |
该替代与HotSpot的实现非常类似,即从每个对象的特定偏移量读取其identityHashCode,如若不存在则随机生成一个。这段代码的问题在于没有对hashcode的写入操作加锁,因此使用CAS指令即可修复。
通过类似的机制native image generator可识别SVM所不支持的组件(类,字段,构造器,或方法)。这些组件需要由@Delete
注解,或者通过@Substitute
注解Java类并且不提供某些方法的替代来实现隐式标记。静态分析会从一系列root method出发,保守地探索所有可被调用到的方法。如若探索到任一不被支持的功能,它将抛出UnsupportedFeatureException
。由于javac
的annotation processor依赖于类加载机制,因此静态分析会探测到对java.lang.ClassLoader.<init>
的调用,由这一替代隐式声明。
1 | $ mx image -cp $JAVA_HOME/lib/tools.jar -H:Class=com.sun.tools.javac.Main -H:Name=javac -H:IncludeResourceBundles=com.sun.tools.javac.resources.compiler,com.sun.tools.javac.resources.javac,com.sun.tools.javac.resources.version |
通过追加-H:+ReportUnsupportedElementsAtRuntime
参数,native image generator会将此类异常推延至运行时,使编译而成的javac
支持不使用annotation processor的Java文件:
1 | $ mx image -H:+ReportUnsupportedElementsAtRuntime -cp $JAVA_HOME/lib/tools.jar -H:Class=com.sun.tools.javac.Main -H:Name=javac -H:IncludeResourceBundles=com.sun.tools.javac.resources.compiler,com.sun.tools.javac.resources.javac,com.sun.tools.javac.resources.version |