今天我们来讲讲整个Java生态下相当有分量的一位角儿——GraalVM。
要想搞清楚一个技术的主要适用场景,通常都是看官网。但是GraalVM
的官网,怎么说呢,稍微有点营销号的感觉,不利于技术人理解它。咱们还是看维基百科上怎么说吧。
GraalVM
是用Java实现的基于HotSpot/OpenJDK的JVM和JDK。它支持额外的编程语言和运行范式,例如对Java应用程序AOT,从而实现快速启动和低内存占用。
这段描述透露了三个信息:
GraalVM
可以代替JDK、JVM之前的工作。GraalVM
除了支持Java,也支持多种语言。GraalVM
可以对应用AOT,也就是把程序直接编译成二进制,从而提升启动速度、改进内存使用。
了解完这些背景知识,我们再看看官网怎么说。
Run Programs Faster Anywhere
- Increase application throughput and reduce latency
- Compile applications into small self-contained native binaries
- Seamlessly use multiple languages and libraries
翻译一下就是:在各种地方跑起来都更快。提升应用的吞吐并减少延迟、把应用编译成独立本机的二进制程序、无缝使用多种语言和库。
对于上述特点,官网还有几个好看的图示:
不知道大家看了官网后有什么想法,反正我是感觉,这明显跟维基百科的画风不一样嘛,整个首页都在特意弱化Java的存在,应该是想让其受众更广泛,吸纳更多的用户。由此可见Oracle的野心不小。
不过野心归野心,是骡子是马还得遛遛才知道。下面我们分三个方面来捋捋:
- 性能有多强
- 多语言互相调用好使不
- 编译二进制香不香
一、性能有多强
这里我偷个懒啊,网上找了些使用了GraalVM
框架的测试表现供大家参考,数据有点夸张哈,但是大趋势肯定是不会错的。
这个图片来自Quarkus官网,横轴表示时间,可以明显看出在GraalVM
的加持下,Quarkus的启动后首次响应时间大概提升了50倍左右(0.016vs0.943),这算是一个相当恐怖的数据了。还有一个参考线“Traditional Cloud-Native Stack”,数据更夸张,但是并没有指名道姓,这个就稍微有点不老实了,大家就忽略它吧。
上面的数据证明通过GraalVM
编译后,启动及响应速度是大幅跃进了。那么内存使用呢,这个也是云原生比较关注的点。请看下图:
大概有个5倍左右的改进吧,应该说相当不错了。如果说前面的响应速度影响的还是用户体验,那8G内存的服务器跟40G内存的服务器,那省下来的可是真金白银啊。
聪明的小伙伴可能会说了,你上面的数据强是强,但是万物互联时代,主要看吞吐,对别的技术栈来说,上面的两个维度数据即使不理想,也不能算硬伤。吞吐强,才是真的强。我们一起来看下图:
来自TechEmpower的数据测试。我只截取了使用JavaScript
语言相关技术(都是后端哦)的成绩。先交代下背景,TechEmpower准备了同样的硬件环境,然后用不同的语言和框架来做同一件事情(提供后端HTTP API),并对它们进行压力测试记录下来成绩。所有参与测试的代码都是开源的,可以在GitHub上找到。当然这种“跑分”化的场景跟我们实际项目运行的情况肯定是千差万别的,TechEmpower考虑到实际情况的复杂性,准备了不同的测试场景,如图:
其中Fortunes
要从数据库取数然后在服务端进行数据排序,我觉得还是比较有代表性,所以上面的“跑分”截图就用的Fortunes
的成绩。
现在我们来具体看下成绩,TechEmpower的规则是以第一名的得分值作为基准,也就是100%
。第一名es4x
框架实际得分为237751,这个数字的含义截图中有提到——responses per second
,也就是每秒响应数高达23万多,应该是个非常恐怖的数字了。我们可以对比看下nodejs
的得分,也有91,799的每秒响应数,其实也很惊人,但是成绩也才有es4x
的38.6%
。如果你仔细看的话,会发现es4x
的100%
以及nodejs
的38.6%
数字后面还分别有两个百分比,这个数字代表其得分跟所有本次参与的技术及框架中最强者的比较。至于最强者是谁,我就卖个关子,有兴趣的朋友可以自己去官方网站查看。
好了,现在你应该同意es4x是一个相当有性能优势的技术了。而es4x
之所以能取得如此不俗的成绩,就是因为它用到了我们今天谈论的主角——GraalVM
。TechEmpower的GitHub上能看到说明,指出了这次测试是基于GraalVM的。略显遗憾的就是,TechEmpower并没有测试一版不依赖GraalVM
的es4x
,其实es4x
是一种跑在JDK
上的JavaScript
技术(如果你对JDK
上跑JavaScript
感到惊奇,可以查看你不知道的Java),所以es4x
跟GraalVM
不是强关联的,只是es4x
官方认为GraalVM
性能更好,所以不论es4x
的官网还是TechEmpower的跑分,都是把GraalVM
作为了第一选择。
至此,我已经花了不少篇幅来介绍GraalVM
的性能优势,相信你对它的性能水平有了一个大概的认识。前面的内容基本是拿来主义,重度依赖了互联网上已有的数据成果,下面我们开始第二个议题,真刀真枪的写点代码试一试。
二、多语言互相调用好使不
先来看看GraalVM
支持哪些语言:
应该说有点意思,支持的语言还真不少。那我们就来动手试试吧。
安装GraalVM
我是通过sdkman来管理GraalVM
的,建议你也这样做。
安装sdkman
curl -s "https://get.sdkman.io" | bash
使用sdkman
查看可用的GraalVM
sdk list java
结果如图:
找一个最新版本安装,此处是20.3.0.r11-grl
sdk install java 20.3.0.r11-grl
这里提一句,执行完安装后,sdkman
会询问你是否把刚安装的环境作为默认环境,我建议你选择否,这样就不会对你电脑之前安装好的Java、Node环境造成影响。当您想使用20.3.0.r11-grl
的时候,只需要执行sdk use java 20.3.0.r11-grl
,它只会影响当前终端上下文的环境。
基本的环境准备好后,你可以试试node --version
,应该可以看到v12.18.4
的输出,而如果你执行which node
,应当会看到类似下面的内容:
有意思吧?GraalVM
居然自带了一个node
命令,一个不需要你安装https://nodejs.org
就可以使用的node
🤓。
安装完GraalVM
,还需要安装多语言扩展包
# 根据个人口味,请适量添加😜
# js/node是一等公民,不需要特意安装,这个在上一节已经见识了😋
gu install python
gu install ruby
gu install r
经过漫长的等待,GraalVM
及多语言环境就算安装完成,终于可以进入代码环节了。我们直接采用官方GitHub中的一个例子——polyglot-javascript-java-r,一个演示了JavaScript
、R
、Java
混合使用的精彩demo。
你只需要准备两个文件就可以了,先来看看package.json
,内容非常简单:
{
"dependencies": {
"express": "^4.16.3"
}
}
重头戏都在server.js
,内容如下:
const express = require('express')
const app = express()
const BigInteger = Java.type('java.math.BigInteger')
app.get('/', function (req, res) {
var text = 'Hello World from Graal.js!<br> '
// Using Java standard library classes
text += BigInteger.valueOf(10).pow(100)
.add(BigInteger.valueOf(43)).toString() + '<br>'
// Using R methods to return arrays
text += Polyglot.eval('R',
'ifelse(1 > 2, "no", paste(1:42, c="|"))') + '<br>'
// Using R interoperability to create graphs
text += Polyglot.eval('R',
`svg();
require(lattice);
x <- 1:100
y <- sin(x/10)
z <- cos(x^1.3/(runif(1)*5+10))
print(cloud(x~y*z, main="cloud plot"))
grDevices:::svg.off()
`);
res.send(text)
})
app.listen(3000, function () {
console.log('Example app listening on port 3000!')
})
除了常规的node
和express
使用外,比较有意思的就是对Java
和R
的访问了。咱们一个个来看。
const BigInteger = Java.type('java.math.BigInteger')
使得我们直接在JavaScript
拿到了一个Java Class
。后续就可以在JavaScript
无缝使用Java
中的功能,比如静态方法valueOf
、pow
、add
。一通操作后,我们获得了一个10的100次方的超大整数,并让其和43相加。Polyglot.eval('R','ifelse(1 > 2, "no", paste(1:42, c="|"))')
演示了JavaScript
中通过Polyglot.eval
动态执行其他语言表达式。这里显然是执行了一个R
的表达式。其他语言都是支持的,比如python
你可以使用这样的代码var array = Polyglot.eval("python", "[1,2,42,4]")
,就可以获得一个在python
中制造的数组了。- 另一段比较长的
Polyglot.eval
就是本次demo的核心了, 其实也是R
语言的专属技能。关于R
语言的使用,已经超出了本文的讨论范围,有兴趣的小伙伴可以查看官方文档。
让我们来看看结果吧,先执行npm install
把依赖安装好,然后你要用GraalVM
的node
命令启动项目,请一定注意,不是用你曾经熟悉的那个node
哦。执行node --jvm --polyglot server.js
,然后你会看到如下输出:
赶紧打开浏览器访问一下http://127.0.0.1:3000看看效果吧:
非常精彩,你不费吹灰之力就在html
中构建了一个基于svg
的三维坐标系,仿佛开启了在html
展示数学技艺的大门——当然,如果你想进门转转的话,还得回头恶补一下数学和R
语言,至少我是这么打算的😉。
现在你理解多语言的程序环境是一件多么激动人心事了吧。这里没有口水战,有的只是百家争鸣,取长补短,共建繁荣。所以我才说Oracle的野心真的很大,通过GraalVM
拿出一个多语言支持的环境,并且性能还更强,还可以预编译成二进制文件,直接分发高效运行。如果真有这样的东西,谁不喜欢呢。
三、编译二进制香不香
下面终于到最后一个环节了,把程序打包成二进制程序。按照官网的指点,首先我们要安装native-image
来解锁编译二进制文件的技能。你需要执行:
gu install native-image
安装完native-image
后,当我满心欢喜的执行native-image --language:js server.js
试图把上面演示的server.js
编译成二进制的时候,居然报错了:
去官网查阅了native-image的文档才知道,native-image
只支持JVM-based
的语言,比如Java
、Scala
、Clojure
、Kotlin
这些JVM
的亲缘语言。
没办法,那我们还是拿Java
开刀吧,这里我准备了一个简单的Java
类:
import org.graalvm.polyglot.*;
public class PolyglotJavaJS {
public static void main(String[] args) {
Context polyglot = Context.create();
Value array = polyglot.eval("js", "eval('1+1')");
int result = array.asInt();
System.out.println(result);
}
}
可以看到中间我用GraalVM
提供的工具执行了一小段JavaScript
脚本,通过JavaScript
中的eval
函数计算了1+1
。eval
这种开挂的函数在JavaScript
这种动态语言里挺常见的,但是在Java
这种静态语言就特别少见,也制约了好多编程场景,我个人感觉GraalVM
能通过这种非主流的方式给Java
带来eval
的特性还挺香的。下面我们来把这个Class打包成二进制文件:
- 第一步,编译
.java
为.class
:
javac PolyglotJavaJS.java
- 第二步,有了
.class
文件后我们可以验证一下代码:
Java PolyglotJavaJS
这时控制台打印了2
,看起来一切正常。
- 第三步,用
native-image
把.class
编译可执行文件(也就是二进制文件)。
native-image --language:js PolyglotJavaJS
由于代码里我们用到了JavaScript
相关内容,所以编译的时候需要提供参数--language:js
。
经过一个漫长的等待(我这边是3分多钟),还有恐怖的内存消耗(被native-image
进程消耗掉了6个多G),我们终于得到了如图的执行结果:
当然还有我们想要的可执行文件polyglotjavaJS
,一个惊人的97M的文件。
好在这个文件执行起来还不赖,直接运行./polyglotjavajs
就可以看到我们想要的结果了。如果在执行的命令前面加上time
,我们就能和JVM
解释执行的版本做个对比了:
可以看到,二进制版本确实有着更快的执行速度和更低的CPU使用率,跟JVM
版本对比优势还是比较明显的。就是97M的文件大小,我觉得有点大了,不利于分发。下面尝试把JavaScript
的相关内容移除,只实现最简单的逻辑:
public class PolyglotJavaJS {
public static void main(String[] args) {
System.out.println("Hello GraalVM!");
}
}
然后我们再次执行javac
和native-image PolyglotJavaJS
,又得到了一组新的编译日志:
可喜可贺,编译过程对内存和时间的消耗都只有之前的三分之一。再来看下编译后的文件大小:
只有7.7M了,瞬间觉得香了,有木有。同样的,我们再把二进制和JVM
版的执行速度对比一下:
二进制版本依然保持了不俗的优势。
这么看来GraalVM
打包二进制可执行文件的功能,在JVM
系语言上还是有一定优势的。除了打包过程中耗时比较长以及对机器的CPU、内存有不小的占用,二进制程序执行速度还是挺快的,明显感觉比解释型执行的语言要快。打包出来的文件Size,确实不小,与主流的硬编译语言C
、C++
、Go
、Rust
比差太远了,我随便测试了一个Rust
的Hello World
程序,编译后的可执行文件才370K。这方面的差距,我觉得在很长一段时间GraalVM
是不可能追赶上了。好在5G时代带宽可能不是个大问题,磁盘也越来越白菜价了,可执行文件的大小应该不是我们要考量的主要因素。
总结
GraalVM
在整个JVM
领域无疑是个异类,有众多激进的特性。并且由Oracle
做背书,同时提供社区版和商业版,可持续性不需担心,未来的发展肯定是要越来越好的。但是Java
作为一个有年代感的语言,早也不复舞台C位的荣光。最近几年JVM
系的好东西涌现了不少,无奈国内接受度真的不高,这可能是我辈JVM
系程序员最尴尬的境地了。
我个人感觉,那些没有历史包袱的公司,甚至有了包袱但已经在想办法甩掉包袱的公司,它们在做技术选型时,Java
往往不是第一考量。相反,那些还在坚持Java
路线的公司,往往也不是因为Java
有多好,而是受制于公司软件资产规模,不好调头,或者压根不关心技术,本着能用就行的原则惯性地使用着Java
(这里主要指Web领域)。
这么看,GraalVM
能不能得到市场的认可,未可知也。说实话,我自己甚至觉得,有同样的时间,不如学学go
、rust
,难道不香么?
不过对于那些已经在JVM
扎了根的公司,GraalVM
的出现无疑是有建设意义的,它的多语言环境,能让Java
充当主持人的身份,舞台不再属于Java
一个人,而是“你方唱罢我登场”,大家一起唱好这出戏。至于编译成二进制可执行文件,我个人感觉不是很有必要,至少大部分Java
之前已经做的很好的场景其实是不需要这个二进制文件的。这个功能更像是对Java
的一些补充,用于应对某些特殊的情况。
当然我们也不要忘了node
的程序员,GraalVM
给了他们不同的世界,一个可以享受到JVM
资产,可以访问R
、Python
语言的机会。毕竟Oracle下了一盘大棋,前所未有的让之前割裂的技术有机会协同工作,而这种高维度的演进,也许会在某个不经意的地方擦出你未想象过的火花🤓。
全文完。