GraalVM 背景
新、旧编程语言的兴起躁动,说明必然有其需求动力所在,譬如互联网之于JavaScript、人工智能之于Python,微服务风潮之于Golang等等。大家都清楚不太可能有哪门语言能在每一个领域都尽占优势,Java已是距离这个目标最接近的选项,但若“天下第一”还要百尺竿头更进一步的话,似乎就只能忘掉Java语言本身,踏入无招胜有招的境界。
- 更进一步提升JVM上运行的程序的性能
- 通过预编译(ahead-of-time)编译Java程序为原生可执行程序
- 多种编程语言混编在一个程序中(polyglot)
- 类似于LLVM,GraalVM也提供了方便的机制方便开发新的编程语言
官方网站在: www.graalvm.org/
当前痛点
在云原生时代,Java程序是有很大的劣势的,为什么这么说呢?一般的Java应用程序都要几十兆的内存,启动也不不快。
最流行的SpringBoot/SpringCloud微服务框架为例,启动一个已经优化好,很多bean需要lazy load的application至少需要3-4秒时间,内存需要几百兆,业务逻辑稍微复杂一点点,没有1G以上的内存是很难满足业务的需要呢?
那么在云原生时代,一个充满黑科技的JVM介绍给大家,它能帮助我们让Java程序的启动速度加快100倍,内存只需要原来的五分之一,甚至更少。
Graalvm的介绍
- GraalVM是2018年Oracle开发的下一代JVM实现,被官方称为“Universal VM”和“Polyglot VM”,这是一个在HotSpot虚拟机基础上增强而成的跨语言全栈虚拟机,可以作为“任何语言”的运行平台使用。
- 这里“任何语言”包括了Java、Scala、Groovy、Kotlin等基于Java虚拟机之上的语言,还包括了C、C++、Rust等基于LLVM的语言,同时支持其他像JavaScript、Ruby、Python和R语言等等。
GraalVM可以无额外开销地混合使用这些编程语言,支持不同语言中混用对方的接口和对 象,也能够支持这些语言使用已经编写好的本地库文件。
它的口号“Run Programs Faster Anywhere”就能感觉到一颗蓬勃的野心
Graalvm性能的对比
GraalVM的性能真的不错。以JDK 8为例
- OpenJDK
- Oracle JDK
其中OpenJDK是通过“GPL v2 with CE”协议开源的,可以免费商用的。
之前在用Apache Spark测试性能时, 对比一下两者性能, 稍微数据量大点的查询,会发现Oracle JDK一般都会比OpenJDK快30%以上。
- 而GraalVM分为社区版和商业版,其中GraalVM的社区版也是采用了和OpenJDK一样的“GPL v2 with CE”协议开源的。
- 对于GraalVM的社区版,非常惊喜的发现其比Oracle JDK也会快10%以上。
- 没有试过GraalVM的商业版, 官方报道,其商业版比社区版提升的性能更多
Graalvm主要特性
- 高性能的现代Java
- 占用资源少,启动速度快
- JavaScript,Java, Ruby以及R混合编程
- 在JVM上运行原生语言
- 跨语言工具
- JVM应用扩展
- 原生应用扩展
- 本地Java库
- 数据库支持多语言
- 创建自己的语言
Graalvm工作原理
GraalVM的基本工作原理是将这些语言的源代码(例如,JavaScript)或源代码编译后的中间格式(例如,LLVM字节码、Class字节码)通过解释器转换为能被GraalVM接受的中间表示(Intermediate Representation,IR),譬如设计一个解释器专门对LLVM输出的字节码进行转换来支持C和C++语言,这个过程称为“程序特化”(Specialized,也常称为Partial Evaluation)。
GraalVM提供了Truffle工具集来快速构建面向一种新语言的解释器,并用它构建了一个称为Sulong的高性能LLVM字节码解释器。
从某个角度来看,GraalVM才是真正意义上与物理计算机相对应的高级语言虚拟机,因为它与物理硬件的指令集一样,做到了只与机器特性相关而不与某种高级语言特性相关。
Graalvm的高等优化能力
Oracle Labs的研究总监Thomas Wuerthinger在接受采访时谈到:“随着GraalVM1.0的发布,已经证明了拥有高性能的多语言虚拟机是可能的,并且实现这个目标的最佳方式不是通过类似Java虚拟机和微软CLR那样带有语言特性的字节码”。
本来就不以速度见长的语言运行环境,由于GraalVM本身能够对输入的中间表示进行自动优化,在运行时还能进行即时编译优化,往往使用GraalVM实现能够获得比原生编译器更优秀的执行效率,譬如Graal.js要优于Node.js、Graal.Python要优于CPtyhon,TruffleRuby要优于Ruby MRI,FastR要优于R语言等等。
Graalvm与Hotspot的对比
GraalVM本来就是在HotSpot基础上诞生的,天生就可作为一套完整的符合Java SE8标准Java虚拟机来使用。
它和标准的HotSpot差异主要在即时编译器上,其执行效率、编译质量目前与标准版的HotSpot相比也是互有胜负。
Oracle Labs和美国大学里面的研究院所做的最新即时编译技术的研究全部都迁移至基于GraalVM之上进行了,其发展潜力令人期待。
如果Java语言或者HotSpot虚拟机真的有被取代的一天,那从现在看来GraalVM是希望最大的一个候选项,这场革命很可能会在Java使用者没有明显感觉的情况下悄然而来,Java世界所有的软件生态都没有发生丝毫变化,但天下第一的位置已经悄然更迭。
Graalvm即时编译器
自JDK 10起,HotSpot中又加入了一个全新的即时编译器:Graal编译器,看名字就可以联想到它是来自于Graal VM。
C1/C2即时编译器
对需要长时间运行的应用来说,由于经过充分预热,热点代码会被HotSpot的探测机制准确定位捕获,并将其编译为物理硬件可直接执行的机器码,在这类应用中Java的运行效率很大程度上是取决于即时编译器所输出的代码质量。
HotSpot虚拟机中包含有两个即时编译器:
- 编译时间较短但输出代码优化程度较低的客户端编译器(简称为C1)
- 编译耗时长但输出代码优化质量也更高的服务端编译器(简称为C2)
通常它们会在分层编译机制下与解释器互相配合来共同构成HotSpot虚拟机的执行子系统的。
C2即时编译器
Graal编译器是作为C2编译器替代者的角色登场的。C2的历史已经非常长了,可以追溯到Cliff Click大神读博士期间的作品,这个由C++写成的编译器尽管目前依然效果拔群,但已经复杂到连Cliff Click本人都不愿意继续维护的程度。
Graal编译器
而Graal编译器本身就是由Java语言写成,实现时又刻意与C2采用了同一种名为“Sea-of-Nodes”的高级中间表示(High IR)形式,使其能够更容易借鉴C2的优点。
Graal编译器比C2编译器晚了足足二十年面世,有着极其充沛的后发优势,在保持能输出相近质量的编译代码的同时,开发效率和扩展性上都要显著优于C2编译器,这决定了C2编译器中优秀的代码优化技术可以轻易地移植到Graal编译器上,但是反过来Graal编译器中行之有效的优化在C2编译器里实现起来则异常艰难。
这种情况下,Graal的编译效果短短几年间迅速追平了C2,甚至某些测试项中开始逐渐反超C2编译器。
Graal能够做比C2更加复杂的优化:
- “部分逃逸分析”(Partial Escape Analysis)
- 比C2更容易使用“激进预测性优化”(Aggressive Speculative Optimization)的策略
- 支持自定义的预测性假设
未来可期
Graal编译器尚且年幼,还未经过足够多的实践验证,所以仍然带着“实验状态”的标签,需要用开关参数去激活,这让笔者不禁联想起JDK 1.3时代,HotSpot虚拟机刚刚横空出世时的场景,同样也是需要用开关激活,也是作为Classic虚拟机的替代品的一段历史。
Graal编译器未来的前途可期,作为Java虚拟机执行代码的最新引擎,它的持续改进,会同时为HotSpot与Graal VM注入更快更强的驱动力。
编译为原生执行程序
编译为原生程序有一定的假设条件,比如:
- 尽量少的JNI调用
- 尽量少的使用反射
- 尽量少的class loader隔离等
当没有用这些复杂功能的时候,很容易可以使用GraalVM提供的 native image 编译Jar为可执行程序。
当然即使当程序使用了 JNI、反射时,也没关系,我们可以使用一些配置文件告诉GraalVM单独处理这些信息,比如:
- 通过参数 -H:JNIConfigurationFiles 告诉JNI相关配置JSON文件
- 通过参数 -H:ReflectionConfigurationFiles 告诉反射相关配置JSON文件
稍微会复杂一些,但是只要有足够的耐心,理论上也是可以编译成功的!
不过我们可以使用一些原生支持GraalVM native image的框架, 比如:Quarkus。
GraalVM的原生编译非常适合微服务和 Serverless
当可以把Java程序也编译为原生的可执行程序后 (目前GraalVM已经支持编译为Windows, MacOS, Linux上的原生程序),最主要的两个变化:
- 启动时间变短了,之前启动一个有“依赖注入”的Java程序,可能启动时间要2秒以上。如果Java程序是要长期运行的,那启动时间稍慢一点是没问题的,但是对于 Serverless 应用,这就变为冷启动(cold start)了,影响比较大。
- 程序运行的内存需求变小了,之前启动一个Java程序,控制的好的话(heap设置的比较小),也要100M以上的内存,但是编译为原生程序后,只需要4M内存就可以了。 这样同样的一台机器就可以启动非常多的进程,适合简单的微服务。