https://it.deepinmind.com/jvm/2019/08/27/graalvm-ten-things.html
GraalVM有许多不同的组件,如果你只是听说过它或有些简单的了解,肯定无法一窥全豹。本文将列举下GraalVM的几大常用功能,看看它们都能做些什么。
本文将要介绍的内容在GraalVM 19.0.0(可以从https://www.graalvm.org/downloads上下载到)上验证通过。这里我用的是MacOS平台的企业版,可以免费使用,里面的命令也可用于Linux平台。下面介绍的大部分功能在社区版上也是支持的。
先从graalvm.org/downloads下载GraalVM 19.0.0,然后将它添加到$PATH中。默认情况下GraalVM可以支持Java和JavaScript。
$ git clone https://github.com/chrisseaton/graalvm-ten-things.git
$ cd foo
$ tar -zxf graalvm-ee-darwin-amd64-19.0.0.tar.gz.tar.gz
# or graalvm-ee-darwin-linux-19.0.0.tar.gz on Linux
$ export PATH=graalvm-ee-19.0.0/Contents/Home/bin:$PATH
# or PATH=graalvm-ee-19.0.0/bin:$PATH on Linux
GraalVM自带了JavaScript的实现,它还有一个包管理工具gu,你可以用它来安装其它语言。我安装了Ruby, Python和R语言,以及native-image工具。这些都可以从github中下载到。
$ gu install native-image
$ gu install ruby
$ gu install python
$ gu install R
现在运行下java或js便能看到它们的运行时的GraalVM版本号。
$ java -version
java version "1.8.0_212"
Java(TM) SE Runtime Environment (build 1.8.0_212-b31)
Java HotSpot(TM) GraalVM EE 19.0.0 (build 25.212-b31-jvmci-19-b01, mixed mode)
$ js --version
GraalVM JavaScript (GraalVM EE Native 19.0.0)
GraalVM中的Graal得名于它的Graal编译器。GraalVM是根据One VM to Rule Them All这篇论文的思想实现的,也就说它只实现了一套编译器库,却能用于不同的场合。比如说我们可以用GraalVM的编译器来进行ahead-of-time或just-in-time(JIT)编译,编译不同的语言,编译到不同的平台上。
最简单的使用方式就是把它当作Java的JIT编译器来使用。
我们用下面这段程序作一个例子,它会输出文档中使用频率前十的单词。里面用到了Java的新特性,包括Stream和Collector。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TopTen {
public static void main(String[] args) {
Arrays.stream(args)
.flatMap(TopTen::fileLines)
.flatMap(line -> Arrays.stream(line.split("\\b")))
.map(word -> word.replaceAll("[^a-zA-Z]", ""))
.filter(word -> word.length() > 0)
.map(word -> word.toLowerCase())
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.sorted((a, b) -> -a.getValue().compareTo(b.getValue()))
.limit(10)
.forEach(e -> System.out.format("%s = %d%n", e.getKey(), e.getValue()));
}
private static Stream<String> fileLines(String path) {
try {
return Files.lines(Paths.get(path));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
可以使用GraalVM自带的javac编译器,不过对这个示例来说无所谓,用系统中原有的javac来编译也是可以的。
$ javac TopTen.java
如果使用GraalVM中的java来运行这个程序的话,就会自动用上Graal的JIT编译器了——不再需要额外的配置。这里我们使用time来计算程序从运行到结束的真实消耗的系统时间,并没有使用更复杂的微基准测试工具,因为这里的输入数据非常大,也没必要纠结这几秒的时间。large.txt文件的大小是150MB。
$ make large.txt
$ time java TopTen large.txt
sed = 502701
ut = 392657
in = 377651
et = 352641
id = 317627
eu = 317627
eget = 302621
vel = 300120
a = 287615
sit = 282613
real 0m12.950s
user 0m17.827s
sys 0m0.622s
GraalVM是用Java编写的,不像其它的JIT编译器那样都是用C++写的。这样相对于传统的编译器来说,更容易对它进行优化,比如像HotSpot中不支持的偏向逃逸分析(partial escape analysis)等新的优化技术都可以很容易在它上面实现。这些技术可以让Java程序运行速度得到显著的提升。
如果想和不启用GraalVM JIT编译器的结果做下对比,可以使用-XX:-UseJVMCICompiler来关掉它。JVMCI是GraalVM与JVM间的接口。可以和标准的JVM的做一下性能对比。
$ time java -XX:-UseJVMCICompiler TopTen large.txt
sed = 502701
ut = 392657
in = 377651
et = 352641
id = 317627
eu = 317627
eget = 302621
vel = 300120
a = 287615
sit = 282613
real 0m19.602s
user 0m20.357s
sys 0m0.498s
结果表明这个Java程序在GraalVM中的运行时间只有传统JVM的2/3。在虚拟机这个领域,哪怕是个位数的性能提升都是很大的进步,所以这个结果真的算是挺了不起了。
即便用社区版来进行测试,结果也要比HotSpot要好,但和企业版相比还是逊色了点。
Twitter是唯一一个已经在生产环境中使用GraalVM的公司,他们声称GraalVM为公司节约了大量的成本。Twitter用它来运行scala程序——GraalVM是基于JVM字节码来工作的,因此JVM上的语言它都能支持。
这是GraalVM的第一个用途,它为我们带来了一个更加强劲的JIT编译器。
在长时间运行,对性能要求较高的领域,Java平台是比较有优势的,但对于那些只需要短暂运行的应用而言,启动时间过长和占用资源过高都是一个问题。
比如说,仍旧是前面这个程序,不过输入文件的大小从150MB变成了1kB,对于一个这么小的文件而言,消耗的时间相对来说就很长了,并且还需要使用接近70MB的内存。我们加上-l参数来同时打印出所消耗内存及时间。
$ make small.txt
$ /usr/bin/time -l java TopTen small.txt
# -v on Linux instead of -l
sed = 6
sit = 6
amet = 6
mauris = 3
volutpat = 3
vitae = 3
dolor = 3
libero = 3
tempor = 2
suscipit = 2
0.17 real 0.28 user 0.04 sys
70737920 maximum resident set size
...
GraalVM有一个工具能解决这个问题。前面说过GraalVM其实更像一个编译器库,有很多不同的用法。其中一种叫作提前编译(ahead-of-time),它可以编译成本地的可执行镜像,而不是在运行时进行just-in-time(JIT)编译。这和传统编译器比如gcc的工作方式类似。
$ native-image --no-server TopTen
[topten:37970] classlist: 1,801.57 ms
[topten:37970] (cap): 1,289.45 ms
[topten:37970] setup: 3,087.67 ms
[topten:37970] (typeflow): 6,704.85 ms
[topten:37970] (objects): 6,448.88 ms
[topten:37970] (features): 820.90 ms
[topten:37970] analysis: 14,271.88 ms
[topten:37970] (clinit): 257.25 ms
[topten:37970] universe: 766.11 ms
[topten:37970] (parse): 1,365.29 ms
[topten:37970] (inline): 3,829.55 ms
[topten:37970] (compile): 34,674.51 ms
[topten:37970] compile: 41,412.71 ms
[topten:37970] image: 2,741.41 ms
[topten:37970] write: 619.13 ms
[topten:37970] [total]: 64,891.52 ms
上述命令会生成一个叫topten的本地可执行文件。它并不是一个JVM启动程序,也没有链接到JVM上,更没有通过任何方式将JVM打包进来。native-image命令会把你的Java代码以及用到的相关库,都编译成本地的机器代码。而一些运行时组件比如垃圾回收器等,用的是一个专属的虚拟机叫SubstrateVM,它和GraalVM一样,也是用Java写的。
如果去看一下topten所使用的库,会发现它只用到了标准的系统库,你可以把这个文件拷贝到一个没有安装JVM的系统上去验证一下,它没有用到JVM或者任何与之相关的东西。执行文件的大小也非常小,只有8MB左右。
$ otool -L topten # ldd topten on Linux
topten:
libSystem.B.dylib (current version 1252.250.1)
CoreFoundation (current version 1575.12.0)
/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.11)
$ du -h topten
7.5M topten
执行下后会发现,与在JVM中运行相比,它的启动时间快了一个数量级,占用的内存也少了一个数量级。速度快到在命令行下执行时都没有意识到花了多少时间——感受不到小程序在JVM中运行时产生的那种停顿感。
$ /usr/bin/time -l ./topten small.txt
sed = 6
sit = 6
amet = 6
mauris = 3
volutpat = 3
vitae = 3
dolor = 3
libero = 3
tempor = 2
suscipit = 2
0.02 real 0.00 user 0.00 sys
3158016 maximum resident set size
...
不过native-image工具有一个局限是所有使用到的类在编译时必须是确定的,在反射的使用上也有一些限制。它在基本的编译之上进行了一些优化,比如类的静态初始化过程在编译期就完成了,因此也可以缩短应用的加载时间。
这是GraalVM的第二个使用场景——将已有的Java程序以一种快速启动、占用资源低的方式进行发布运行。它减少了运行时找不到jar包的烦恼,并且可以生成更小的Docker镜像。
除了Java以外,GraalVM还提供了JavaScript, Ruby, R以及Python语言的实现。它们是基于一款新的语言实现框架Truffle来实现的,用它来实现语言的解释器非常简单,执行性能也很不错。使用Truffle来编写解释器时,它会自动使用GraalVM并为你提供了JIT编译的功能。因此GraalVM不仅仅是Java语言的JIT及ahead-of-time编译器,它也是JavaScript, Ruby, R以及Python等语言的JIT编译器。
GraalVM语言的实现目标是可以很方便地替代现有语言。我们可以安装一个Node.js模块:
$ npm install color
...
+ color@3.1.1
added 6 packages from 6 contributors and audited 7 packages in 6.931s
然后用这个模块写一段小程序,将RGB颜色转换成HSL:
var Color = require('color');
process.argv.slice(2).forEach(function (val) {
print(Color(val).hsl().string());
});
然后正常运行它:
$ node color.js '#42aaf4'
hsl(204.89999999999998, 89%, 60.8%)
GraalVM中的语言可以一起工作——你可以使用它提供的API从一个语言中调用另一个语言。它可以用来编写多语言程序——也就是由多种语言实现的程序。
这样做的好处是,你可能希望主要通过某一门语言来实现某个应用,但是又希望使用一个其它语言实现的工具库。比方说,我们想用Node.js来写一个程序,来将CSS颜色转换成16进制的,但是又想用Ruby的颜色库来完成转换工作。
var express = require('express');
var app = express();
color_rgb = Polyglot.eval('ruby', `
require 'color'
Color::RGB
`);
app.get('/css/:name', function (req, res) {
color = color_rgb.by_name(req.params.name).html()
res.send('<h1 style="color: ' + color + '" >' + color + '</h1>');
});
app.listen(8080, function () {
console.log('serving at http://localhost:8080')
});
这里面运行了一段Ruby代码——但其实我们并没有做太多工作,只是引入了一个库,然后返回了一个ruby对象。这个对象在ruby中正常的使用方式是Color::RGB.byname(name).html。尽管确实是Ruby的对象和方法,但在JavaScript中colorrgb的使用方式,就跟调用JavaScript中的方法是类似的,我们给它们传入JavaScript的字符串对象,然后再将Ruby返回的字符串和JavaScript自己的字符串进行拼接。
我们先安装下Ruby和node的模块:
$ gem install color
Fetching: color-1.8.gem (100%)
Successfully installed color-1.8
1 gem installed
$ npm install express
+ express@4.17.0
added 50 packages from 37 contributors and audited 143 packages in 22.431s
运行node时需要加上几个参数:--polyglot表明希望使用其它语言,加上--jvm是因为默认条件下node是不会引入JavaScript外的其它语言的。
$ node --polyglot --jvm color-server.js
serving at http://localhost:8080
然后在浏览器中打开http://localhost:8080/css/aquamarine,也可以把后面的颜色替换成你想要的。
我们再增加更多的语言和模块,来尝试下更复杂的case。
JavaScript对大整数并没有特别好的支持。我找了几个类似big-integer的库,不过它们把数字当作JavaScript的浮点数来存储,因此性能都不是那么理想。Java的BigInteger性能还不错,我们就用它来完成大整数的运算。
JavaScript在绘图方面也没有特别好的内建支持,而这正是R语言所擅长的。我们就用R的svg模块来绘制一个三角函数的3D投影图。
GraalVM的多语言特性就可以派上用场了,我们可以把其它语言的计算结果,整合到JavaScript中来。
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 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!')
})
打开http://localhost:3000/查看下结果。
这是GraalVM的第三种用法——使用多语言来编写程序,或使用其它语言编写的库。这个过程可以看作是语言或模块的商品化阶段——你觉得哪个语言用来解决手头的问题最顺手,或者你最喜欢哪个库,就随便去用,而不用关心它是拿什么语言写的。
GraalVM还可以支持C语言。GraalVM可以用运行JavaScript和Ruby程序一样的方式来运行C代码。
事实上GraalVM可以运行的是LLVM所产生的中间代码——LLVM bitcode,并非能够直接运行C程序。也就是说你可以使用它来运行C代码,或者是LLVM能够支持的譬如C++, Fortran,甚至未来可能支持的其它语言。这里我通过一个单文件版的gzip程序来简单做下演示,它是Stephen McCamant维护的一个项目。它把gzip的源码和autoconf配置信息整合到一个文件里了。我还给它打了几个补丁以便能在macOS上配合clang运行。
现在我们可以使用标准的clang来编译这个程序,我们希望将它编译成GraalVM可以运行的LLVM bitcode,而不是本地的机器代码。这里我用的是clang4.0.1.
$ clang -c -emit-llvm gzip.c
然后通过lli命令便可以直接使用GraalVM来运行这个程序了。我们先使用系统的gzip命令来压缩一个文件,然后再用GraalVM中的gzip程序对它进行解压缩。
$ cat small.txt
Lorem ipsum dolor sit amet...
$ gzip small.txt
$ lli gzip.bc -d small.txt.gz
$ cat small.txt
Lorem ipsum dolor sit amet...
GraalVM中的Ruby和Python都是使用这项技术来运行C扩展模块的。这意味着我们可以在VM中运行C扩展程序,这样便能在支持这些语言的原生扩展能力的同时还能保证比较高的性能。
这是GraalVM的第四个用途——运行C或C++等原生语言写的程序,运行Python或Ruby等语言的C扩展模块,这是现有的JVM语言JRuby等所无法实现的。
如果你在使用Java编程,肯定会用到一些质量非常高的工具,譬如IDE、调试器、分析工具之类的。但并非所有的语言都有这么好的配套支持,但如果你使用GraalVM中的语言就能免费使用它们。
所有的GraalVM语言(除了Java)都是基于通用的Truffle框架实现的。这样一个功能(比如调试器)只需实现一次,便能应用于所有语言。
为了试验下这个特性,我们编写了一个FizzBuzz程序,它会把结果输出到屏幕上,代码分支也很清晰,每个分支只完成数次的迭代,这样能方便我们打断点。我们从先一个JavaScript的实现开始。
function fizzbuzz(n) {
if ((n % 3 == 0) && (n % 5 == 0)) {
return 'FizzBuzz';
} else if (n % 3 == 0) {
return 'Fizz';
} else if (n % 5 == 0) {
return 'Buzz';
} else {
return n;
}
}
for (var n = 1; n <= 20; n++) {
print(fizzbuzz(n));
}
可以使用GraalVM的js来运行这个JavaScript程序。
$ js fizzbuzz.js
1
2
Fizz
4
Buzz
Fizz
...
运行这个程序时我们加上了一个标记--inspect。它会返回一个链接,用Chrome打开它,然后便会发现程序暂停在调试器中。
$ js --inspect fizzbuzz.js
Debugger listening on port 9229.
To start debugging, open the following URL in Chrome:
chrome-devtools://devtools/bundled/inspector.html?ws=127.0.0.1:9229/6c478d4e-1350b196b409
...
这个时候我们可以在FizzBuzz行处打上一个断点,然后继续执行。当它停止时我们查看下n的值,然后继续执行,你可以体验下这个调试器的功能。
Chrome调试器通常用于调试JavaScript程序,不过在GraalVM里面JavaScript程序并没有什么特别之处。这个功能在Python, Ruby或者R上也同时适用,程序的源码就不一一列举了,不过它们运行的方式都是类似的,也都可以使用同样的Chrome调试器来进行调试。
$ graalpython --jvm --inspect fizzbuzz.py
$ ruby --inspect fizzbuzz.rb
$ Rscript --inspect fizzbuzz.r
使用Java进行开发的话那你一定不会对VisualVM感到陌生。你可以使用它提供的用户界面,通过网络连接到远程或本地机器上运行着的JVM实例,来查看应用的运行状况,比如内存及线程的使用情况等。
GraalVM中也自带了VisualVM工具,可以通过jvisualvm命令来启用它:
$ jvisualvm &> /dev/null &
在运行TopTen程序之前,可以先启动VisualVM,然后便能观察内存的使用情况,或者dump下堆看看里面有哪些对象。
$ java TopTen large.txt
我写了段Ruby程序来定时生成一些垃圾对象。
require 'erb'
x = 42
template = ERB.new <<-EOF
The value of x is: <%= x %>
EOF
loop do
puts template.result(binding)
end
如果运行的是像JRuby这样的标准JVM语言,你一定会对VisualVM感到失望,因为它只能查看Java对象的的使用情况,而不是语言中的实际对象。
但如果我们使用的是GraalVM版的Ruby的话,VisualVM便可以识别出Ruby内部的对象了,但必须加上--jvm选项后运行方能使用VisualVM,原生模式运行的ruby是不支持的。
$ ruby --jvm render.rb
底层的Java对象的堆视图也仍旧支持,而在Summary视图下,我们可以通过Ruby Heap来查看Ruby对象的情况。
Truffle框架是语言和工具之间的一个纽带。如果你使用Truffle来编写自己的语言,并且使用它的工具API来为该语言编写调试器等工具,那么这些工具只需编写一次,便能在不同语言中使用。
因此GraalVM的第五个用途就是,作为一个平台,它能够给那些缺乏好的开发工具支持的语言,提供像Chrome Debugger或者VisualVM这样的高质量工具。
GraalVM除了可以用来实现语言,以及进行多语言编程外,这些语言或工具还可以嵌入到你的Java应用当中。你可以通过新的org.graalvm.polyglot的API,来加载并运行其它语言编写的代码,并获取它们的返回值。
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
public class ExtendJava {
public static void main(String[] args) {
String language = "js";
try (Context context = Context.newBuilder().allowNativeAccess(true).build()) {
for (String arg : args) {
if (arg.startsWith("-")) {
language = arg.substring(1);
} else {
Value v = context.eval(language, arg);
System.out.println(v);
}
}
}
}
}
如果使用GraalVM中的javac和java命令,它的类路径会自动引入org.graalvm包,因此无需增加额外的参数,便可以直接编译及运行。
$ javac ExtendJava.java
$ java ExtendJava '14 + 2'
16
$ java ExtendJava -js 'Math.sqrt(14)'
3.7416573867739413
$ java ExtendJava -python '[2**n for n in range(0, 8)]'
[1, 2, 4, 8, 16, 32, 64, 128]
$ java ExtendJava -ruby '[4, 2, 3].sort'
[2, 3, 4]
这里运行的这些语言,和通过GraalVM的node或ruby命令运行的是一样的,也都有很高的执行性能。
这是GraalVM可以做的第六件事——它可以作为一个接口,在Java程序中嵌入多种不同的语言。你可以使用多语言API去获取客语言(guest language)的对象,并当成Java接口来进行使用,或是完成一些复杂的操作。
GraalVM发布了一个这样的本地库——你可以在原生程序中使用它来运行GraalVM上的任意语言所编写的代码。JavaScript的运行时V8以及Python的解释器CPython,这些都是可嵌入的,可以把它们当作一个库来链接到其它应用程序当中。GraalVM也提供了一个多语言的嵌入库,你可以在嵌入上下文中使用任意语言。
这个库是GraalVM自带的,不过它默认只支持JavaScript。通过下述命令你可以重新编译这个多语言库,以便引入更多语言,不过首先你需要从OTN上下载Oracle GraalVM Enterprise Edition Native Image preview for macOS (19.0.0)。重新编译可能会花上数分钟,因此如果你只想体验下JavaScript的话——就不需要费心去重新编译了。
$ gu install --force --file native-image-installable-svm-svmee-darwin-amd64-19.0.0.jar
$ gu rebuild-images libpolyglot
我们可以写一个C程序,用它来运行命令行传入的GraalVM语言的命令。我们将采用与上述ExtendJava类似的例子,不过这次的主语言是C。
#include <stdlib.h>
#include <stdio.h>
#include <polyglot_api.h>
int main(int argc, char **argv) {
poly_isolate isolate = NULL;
poly_thread thread = NULL;
if (poly_create_isolate(NULL, &isolate, &thread) != poly_ok) {
fprintf(stderr, "poly_create_isolate error\n");
return 1;
}
poly_context context = NULL;
if (poly_create_context(thread, NULL, 0, &context) != poly_ok) {
fprintf(stderr, "poly_create_context error\n");
goto exit_isolate;
}
char* language = "js";
for (int n = 1; n < argc; n++) {
if (argv[n][0] == '-') {
language = &argv[n][1];
} else {
poly_value result = NULL;
if (poly_open_handle_scope(thread) != poly_ok) {
fprintf(stderr, "poly_open_handle_scope error\n");
goto exit_context;
}
if (poly_context_eval(thread, context, language, "eval", argv[n], &result) != poly_ok) {
fprintf(stderr, "poly_context_eval error\n");
const poly_extended_error_info *error;
if (poly_get_last_error_info(thread, &error) != poly_ok) {
fprintf(stderr, "poly_get_last_error_info error\n");
goto exit_scope;
}
fprintf(stderr, "%s\n", error->error_message);
goto exit_scope;
}
char buffer[1024];
size_t