下一代JVM:GraalVM的十大特性

Published: 27 Aug 2019 Category: jvm

GraalVM有许多不同的组件,如果你只是听说过它或有些简单的了解,肯定无法一窥全豹。本文将列举下GraalVM的几大常用功能,看看它们都能做些什么。

  1. 高性能的现代Java
  2. 占用资源少,启动速度快
  3. JavaScript, Java, Ruby以及R混合编程
  4. 在JVM上运行原生语言
  5. 跨语言工具
  6. JVM应用扩展
  7. 原生应用扩展
  8. 本地Java库
  9. 数据库支持多语言
  10. 创建自己的语言

本文将要介绍的内容在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)

1.高性能的现代Java

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编译器。

2.资源占用低,启动速度快

在长时间运行,对性能要求较高的领域,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镜像。

3.JavaScript, Java, Ruby以及R混合编程

除了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/查看下结果。

图片2

这是GraalVM的第三种用法——使用多语言来编写程序,或使用其它语言编写的库。这个过程可以看作是语言或模块的商品化阶段——你觉得哪个语言用来解决手头的问题最顺手,或者你最喜欢哪个库,就随便去用,而不用关心它是拿什么语言写的。

4.在JVM中运行原生语言

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等所无法实现的。

5.跨语言工具

如果你在使用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这样的高质量工具。

6.JVM应用扩展

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接口来进行使用,或是完成一些复杂的操作。

7.原生程序扩展

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 length;

      if (poly_value_to_string_utf8(thread, result, buffer, sizeof(buffer), &length) != poly_ok) {
        fprintf(stderr, "poly_value_to_string_utf8 error\n");
        goto exit_scope;
      }

      if (poly_close_handle_scope(thread) != poly_ok) {
        fprintf(stderr, "poly_close_handle_scope error\n");
        goto exit_context;
      }

      buffer[length] = '\0';
      printf("%s\n", buffer);
    }
  }

  if (poly_context_close(thread, context, true) != poly_ok) {
    fprintf(stderr, "poly_context_close error\n");
    goto exit_isolate;
  }

  if (poly_tear_down_isolate(thread) != poly_ok) {
    fprintf(stderr, "poly_tear_down_isolate error\n");
    return 1;
  }

  return 0;

exit_scope:
  poly_close_handle_scope(thread);
exit_context:
  poly_context_close(thread, context, true);
exit_isolate:
  poly_tear_down_isolate(thread);
  return 1;
}

我们先通过系统自带的C编译器来编译并链接到GraalVM的多语言库上,然后运行这段程序。同样的,它也不需要启动JVM。

$ clang -Igraalvm-ee-19.0.0/Contents/Home/jre/lib/polyglot -rpath graalvm-ee-19.0.0/Contents/Home -Lgraalvm-ee-19.0.0/Contents/Home/jre/lib/polyglot -lpolyglot extendc.c -o extendc
$ otool -L extendc
extendc:
  libpolyglot.dylib (current version 0.0.0)
  /usr/lib/libSystem.B.dylib (current version 1252.250.1)
$ ./extendc '14 + 2'
16
$ ./extendc -js 'Math.sqrt(14)'
3.7416573867739413
$ ./extendc -python '[2**n for n in range(0, 8)]'
[1, 2, 4, 8, 16, 32, 64, 128]
$ ./extendc -ruby '(0...8).map { |n| 2 ** n }'
[1, 2, 4, 8, 16, 32, 64, 128]

这是GraalVM能做的第七个用途——在原生应用程序中使用本地库来嵌入GraalVM语言。

8.本地Java库

Java生态中有非常多的高质量的库,这是别的平台包括原生应用或托管应用所没有的。如果你希望在原生程序中使用Java库,这就需要嵌入整个JVM,那整个应用就会迅速膨胀变得复杂。

GraalVM可以解决这一问题,你可以将别人写的或自己实现的Java代码编译成独立的本地库,并用于任意的原生语言。和前面提到的本地编译类似,这个库无需JVM便能直接运行。

我写了一个程序,它使用著名的Apache SIS地理库来计算地球上两点的大圆距离(Great-circle distance)。这里我用的是http://sis.apache.org/上下载的SIS 0.8版本,并从里面提取的jar包。

import org.apache.sis.distance.DistanceUtils;

public class Distance {

    public static void main(String[] args) {
        final double aLat   = Double.parseDouble(args[0]);
        final double aLong  = Double.parseDouble(args[1]);
        final double bLat   = Double.parseDouble(args[2]);
        final double bLong  = Double.parseDouble(args[3]);
        System.out.printf("%f km%n", DistanceUtils.getHaversineDistance(aLat, aLong, bLat, bLong));
    }

}

我们先正常编译这个程序,然后用它来计算伦敦(纬度51.507222, 经度-0.1275)到纽约(40.7127, -74.0059)的距离。

$ javac -cp sis.jar -parameters Distance.java
$ java -cp sis.jar:. Distance 51.507222 -0.1275 40.7127 -74.0059
5570.25 km

然后再像前面的topten程序那样,将它编译成本地可执行文件。

$ native-image --no-server -cp sis.jar:. Distance
...
$ ./distance 51.507222 -0.1275 40.7127 -74.0059
5570.25 km

我们还可以把它编译成本地的共享库,而不是可执行程序。我们需要将其中一个方法打上@CEntryPoint注解。

...
import org.graalvm.nativeimage.IsolateThread;
import org.graalvm.nativeimage.c.function.CEntryPoint;

public class Distance {

    ...

    @CEntryPoint(name = "distance")
    public static double distance(IsolateThread thread,
          double a_lat, double a_long,
          double b_lat, double b_long) {
        return DistanceUtils.getHaversineDistance(a_lat, a_long, b_lat, b_long);
    }

    ...

}

javac命令也不需要作调整,GraalVM会自动把依赖的API的路径给加到类路径里。编译完后会得到一个共享库,以及一个头文件。

$ native-image --no-server -cp sis.jar:. --shared -H:Name=libdistance
$ otool -L libdistance.dylib   # .so on Linux
libdistance.dylib:
  .../graalvm-ten-things/libdistance.dylib (current version 0.0.0)
  /usr/lib/libSystem.B.dylib (current version 1252.250.1)
  CoreFoundation (current version 1575.12.0)
  /usr/lib/libz.1.dylib (current version 1.2.11)
$ du -h libdistance.dylib
1.8M  libdistance.dylib

然后我们写一个C程序来使用这个库。本地库的接口使用起来难免会有一些繁琐的“仪式感“——毕竟虚拟机需要去管理堆,线程,垃圾回收器等服务,我们需要先创建一个系统实例,然后再告诉它主线程是什么。

#include <stdlib.h>
#include <stdio.h>

#include <libdistance.h>

int main(int argc, char **argv) {
  graal_isolate_t *isolate = NULL;
  graal_isolatethread_t *thread = NULL;

  if (graal_create_isolate(NULL, &isolate, &thread) != 0) {
    fprintf(stderr, "graal_create_isolate error\n");
    return 1;
  }

  double a_lat   = strtod(argv[1], NULL);
  double a_long  = strtod(argv[2], NULL);
  double b_lat   = strtod(argv[3], NULL);
  double b_long  = strtod(argv[4], NULL);

  printf("%.2f km\n", distance(thread, a_lat, a_long, b_lat, b_long));

  if (graal_detach_thread(thread) != 0) {
    fprintf(stderr, "graal_detach_thread error\n");
    return 1;
  }

  return 0;
}

然后使用标准的系统工具来编译和运行这个C程序(Linux上需要设置LDLIBRARYPATH=.)。

$ clang -I. -L. -ldistance distance.c -o distance
$ otool -L distance
distance:
 .../graalvm-blog-post/libdistance.dylib (current version 0.0.0)
 libSystem.B.dylib (current version 1252.0.0)
$ ./distance 51.507222 -0.1275 40.7127 -74.0059
5570.25 km

这是GraalVM的第八个用途——将Java代码编译成本地库,然后在原生应用中使用,同时还无需启动完整的JVM实例。

9.数据库支持多语言

Oracle数据库就是多语言库在语言嵌入的一个应用场景。我们用它来创建了Oracle数据库多语言引擎(Database Multilingual Engine,MLE),它能支持在SQL中使用GraalVM中的语言和模块。

假设我们用JavaScript写了一个前端应用,里面使用了它的validator模块来进行一些邮件地址校验的工作。如果用SQL或者PLSQL开发的数据库应用中也有同样的校验需求,我们就能使用现成的校验器,让结果保持一致。

可以从https://oracle.github.io/oracle-db-mle/releases/0.2.7/docker/中下载MLE的Docker镜像。然后加载到Docker容器中。

$ docker load --input mle-docker-0.2.7.tar.gz

我们需要运行下这个镜像,等过了几分钟后它加载完毕,再在容器里面打开一个Bash终端。

$ docker run mle-docker-0.2.7
$ docker ps
$ docker exec -ti <container_id> bash -li

如果我们可以在终端里运行sqlplus,这说明数据库已经启动好并已经在运行了:

$ sqlplus scott/tiger@localhost:1521/ORCLCDB

现在,还是在Docker的bash终端里,先安装validator模块,然后再通过dbjs命令将其部署到数据库中。最后运行sqlplus命令。

$ npm install validator
$ npm install @types/validator
$ dbjs deploy -u scott -p tiger -c localhost:1521/ORCLCDB validator
$ sqlplus scott/tiger@localhost:1521/ORCLCDB

现在我们就可以把validator模块当作SQL表达式来使用了。

SQL> select validator.isEmail('hello.world@oracle.com') from dual;
VALIDATOR.ISEMAIL('HELLO.WORLD@ORACLE.COM')
-------------------------------------------
                                          1
SQL> select validator.isEmail('hello.world') from dual;
VALIDATOR.ISEMAIL('HELLO.WORLD')
--------------------------------
                               0

这是GraalVM的第九个用途——在Oracle数据库中执行GraalVM语言,以便在数据库中复用应用程序的前后端逻辑,而不总是要将它们抽离出来,单独作为一个应用服务器来进行部署。

10.创建自己的语言

Oracle实验室及我们的研究人员可以用一个相对小的团队来快速研发出高性能的JavaScript, R, Ruby, Python 及C语言的实现,这都要归功于我们研发的Truffle框架,让这一切事半功倍。

Truffle是一个Java库,它可以用来为语言编写抽象语法树(AST)的解释器。AST解释器可能是实现一门语言最简单的方法了,因为它能直接使用解析器的输出,而不用引入字节码或传统编译器的技术,但它通常运行得比较慢。因此我们引入了一项叫部分求值(partial evaluation)的技术,这样Truffle就可以在AST解释器的基础之上,自动地使用Graal来为你的语言提供即时编译(just-in-time)。

Truffle可以用来实现一门新的语言,也可以为现有语言提供一个更高性能的版本,或者用来实现领域特定的语言。项目中提到了很多关于Truffle和Graal的细节,不过有的时候我们可能会忘了其实Truffle是语言实现的最简单的一种方式。同时还能自动获得调试器的功能。任何完成了编程语言实现的本科课程的同学都已经具备所需的基本技能。Oracle实验室的一个实习生只花了数月就实现了一个基础版本的Ruby,而且都比现有的版本性能要好。

这里无法再展开介绍如何完整地实现一门语言,哪怕只是很小的语言,不过SimpleLanguage就是个很好的教程,它能教会你如何使用Truffle来创建一门语言,它是一个简化的JavaScript风格的语言。比如说可以看下if语句的实现

除了Oracle实验室实现的这些Truffle语言,外部实现的还有Smalltalk的变体Newspeak的变体,以及Lisp的变体 。Lisp的实现有一个教程可以参考下。

结论

GraalVM带来了许多新的特性——你可以基于这个平台构建更多更强大的语言或工具,并将它应用到更多的平台上。你可以随意选择自己想要的语言或者模块,而不用考虑程序的运行环境和所使用的语言。

可以前往https://www.graalvm.org/快速体验下GraalVM。里面有下载及文档的相关链接,还有更多的使用示例。

英文原文链接