云原生时代的 Java 虚拟机
↓推荐关注↓
转自:李博
链接:cnblogs.com/liboware/p/15518647.html
GraalVM 背景
新、旧编程语言的兴起躁动,说明必然有其需求动力所在,譬如互联网之于JavaScript、人工智能之于Python,微服务风潮之于 Golang 等等。大家都清楚不太可能有哪门语言能在每一个领域都尽占优势,Java已是距离这个目标最接近的选项,但若“天下第一”还要百尺竿头更进一步的话,似乎就只能忘掉Java语言本身,踏入无招胜有招的境界。
更进一步提升 JVM 上运行的程序的性能;
通过预编译(ahead-of-time)编译Java程序为原生可执行程序;
多种编程语言混编在一个程序中(polyglot);
类似于 LLVM,GraalVM 也提供了方便的机制方便开发新的编程语言。
官方网站在:https://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 在接受采访时谈道:“随着 GraalVM 1.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 与 GraalVM 注入更快更强的驱动力。
编译为原生执行程序
编译为原生程序有一定的假设条件,比如:
尽量少的 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 内存就可以了。这样同样的一台机器就可以启动非常多的进程,适合简单的微服务。
参考资料
https://it.deepinmind.com/jvm/2019/08/27/graalvm-ten-things.html
https://www.zhihu.com/question/274042223
https://www.jianshu.com/p/9e578398b108
- EOF -
关注「程序员的那些事」加星标,不错过圈内事
点赞和在看就是最大的支持❤️
微信扫码关注该文公众号作者