そろそろ、1度GraalVMを試してみようかなと思いまして。
Graal or GraalVM?
ちゃんと情報を追っていなかったのですが、GraalとGraalVMは違うもののようです。
詳説GraalVM(1) イントロダクション - Fight the Future
GraalはJavaで書かれたJITコンパイラ、GraalVMは多言語用の仮想マシンを指します、と。
Java開発者にとって、Graalはいくつか別々の、しかし関連のある複数のプロジェクトとみなせる。HotSpotの新しいJITコンパイラであり、また新しいpolyglotな仮想マシンである。以降JITコンパイラはGraal、新しいVMはGraalVMとして言及する。
GraalVMを使うと多言語の他に、ネイティブイメージの作成ができたり、アプリケーションに組み込んで使うこともできるそうな。
また、ドキュメントによるとネイティブイメージを生成する機能もJavaで書かれていて、Substrate VMと呼ばれるようです。
https://github.com/oracle/graal/tree/vm-1.0.0-rc14/substratevm
なるほど?
今回はこのGraalVMを使って、Javaアプリケーションからネイティブイメージを作成して動かしてみたいと思います。
インストール
それでは、GraalVMをインストールしてみましょう。
環境は、Ubuntu Linux 18.04 LTSです。
$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 18.04.2 LTS Release: 18.04 Codename: bionic
GraalVMを動かせる環境は、LinuxおよびMac OS Xで、いずれも64bit OSなようです。
GraalVMにはCommunity EditionとEnterprise Editionがありますが、今回はCEの現時点のバージョンである1.0.0-RC14を使います。
ダウンロードして、今回は「/usr/local」配下に置いてみました。
$ wget https://github.com/oracle/graal/releases/download/vm-1.0.0-rc14/graalvm-ce-1.0.0-rc14-linux-amd64.tar.gz $ tar xf graalvm-ce-1.0.0-rc14-linux-amd64.tar.gz $ mv graalvm-ce-1.0.0-rc14 graalvm-ce $ sudo mv graalvm-ce /usr/local
「bin」ディレクトリの中身を見てみます。
$ ls -1 /usr/local/graalvm-ce/bin appletviewer extcheck gu idlj jar jarsigner java java-rmi.cgi javac javadoc javah javap jcmd jconsole jdb jdeps jhat jinfo jjs jmap jps jrunscript js jsadebugd jstack jstat jstatd jvisualvm keytool lli native-image native2ascii node npm orbd pack200 policytool polyglot rmic rmid rmiregistry schemagen serialver servertool tnameserv unpack200 wsgen wsimport xjc
ざっと、見たことあるコマンドが…。
javaコマンドやjavacコマンドも入っているんですねぇ…。
$ /usr/local/graalvm-ce/bin/java -version openjdk version "1.8.0_202" OpenJDK Runtime Environment (build 1.8.0_202-20190206132807.buildslave.jdk8u-src-tar--b08) OpenJDK GraalVM CE 1.0.0-rc14 (build 25.202-b08-jvmci-0.56, mixed mode) $ /usr/local/graalvm-ce/bin/javac -version javac 1.8.0_202
とりあえず、PATHを通しておきます。
$ PATH=/usr/local/graalvm-ce/bin:$PATH
ネイティブイメージを作成するコマンド、「native-image」のヘルプを見てみます。
$ native-image --help GraalVM native-image building tool This tool can be used to generate an image that contains ahead-of-time compiled Java code. Usage: native-image [options] class [imagename] (to build an image for a class) or native-image [options] -jar jarfile [imagename] (to build an image for a jar file) where options include: -cp <class search path of directories and zip/jar files> -classpath <class search path of directories and zip/jar files> --class-path <class search path of directories and zip/jar files> A : separated list of directories, JAR archives, and ZIP archives to search for class files. -D<name>=<value> set a system property -J<flag> pass <flag> directly to the JVM running the image generator -O<level> 0 - no optimizations, 1 - basic optimizations (default). 〜省略〜
クラスファイルやJARファイルを指定して、ネイティブイメージを作成するような印象を受けますね。
ネイティブイメージを作成してみる
では、早速ネイティブイメージを作成して動かしてみましょう。
とりあえず、簡単なJavaのサンプルプログラムを書いてみます。
HelloGraal.java
public class HelloGraal { public static void main(String... args) { String arg = args[0]; System.out.printf("Hello %s!!%n", arg); } }
コンパイルして、動作確認。
$ javac HelloGraal.java $ java HelloGraal World Hello World!!
では、ドキュメントに従ってネイティブイメージを作成してみます。
「native-image」コマンドに、対象のクラスと生成するイメージ名を渡して、ネイティブイメージを作成します。
$ native-image HelloGraal hello-graal Build on Server(pid: 12397, port: 33175)* [hello-graal:12397] classlist: 1,109.97 ms [hello-graal:12397] (cap): 2,955.18 ms [hello-graal:12397] setup: 3,239.10 ms Error: Error compiling query code (in /tmp/SVM-8860777324169073416/PosixDirectives.c). Compiler command gcc /tmp/SVM-8860777324169073416/PosixDirectives.c -o /tmp/SVM-8860777324169073416/PosixDirectives output included error: /tmp/SVM-8860777324169073416/PosixDirectives.c:77:10: fatal error: zlib.h: そのようなファイルやディレクトリはありません C file contents around line 77: /tmp/SVM-8860777324169073416/PosixDirectives.c:76: #include <unistd.h> /tmp/SVM-8860777324169073416/PosixDirectives.c:77: #include <zlib.h> /tmp/SVM-8860777324169073416/PosixDirectives.c:78: #include <arpa/inet.h> Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception Error: Processing image build request failed
エラーになりました…。
gccが必要だったり、glibcやzlibのヘッダーファイルが要るみたいですね。
For compilation native-image depends on the local toolchain, so make sure: glibc-devel, zlib-devel (header files for the C library and zlib) and gcc are available on your system.
https://github.com/oracle/graal/tree/vm-1.0.0-rc14/substratevm
自分の環境ではzlibのヘッダーファイルが内容だったので、インストール。
$ apt-file search zlib.h | grep ^zlib zlib1g-dev: /usr/include/zlib.h $ sudo apt install zlib1g-dev
気を取り直して、再度実行。
$ native-image HelloGraal hello-graal Build on Server(pid: 12397, port: 33175) [hello-graal:12397] classlist: 204.58 ms [hello-graal:12397] (cap): 928.89 ms [hello-graal:12397] setup: 1,904.88 ms [hello-graal:12397] (typeflow): 3,809.93 ms [hello-graal:12397] (objects): 1,545.75 ms [hello-graal:12397] (features): 212.28 ms [hello-graal:12397] analysis: 5,694.47 ms [hello-graal:12397] universe: 296.10 ms [hello-graal:12397] (parse): 837.37 ms [hello-graal:12397] (inline): 1,072.47 ms [hello-graal:12397] (compile): 6,274.12 ms [hello-graal:12397] compile: 8,595.84 ms [hello-graal:12397] image: 656.15 ms [hello-graal:12397] write: 147.73 ms [hello-graal:12397] [total]: 17,624.32 ms
ちょっと時間がかかりますね…10秒以上…。でも、今度はうまくいきました。
確認。
$ ./hello-graal World Hello World!!
動きました、と。
ファイルの形式を、fileコマンドで確認。
$ file hello-graal hello-graal: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=d754b52a05baa2ad17f9488860dde0cd9b86e54d, with debug_info, not stripped
ELFファイルですね!
Executable and Linkable Format - Wikipedia
実行時間を比較してみましょう。
ネイティブイメージの場合。
$ time ./hello-graal World Hello World!! real 0m0.003s user 0m0.000s sys 0m0.003s
javaコマンドで起動する場合。
$ time java HelloGraal World Hello World!! real 0m0.120s user 0m0.094s sys 0m0.016s
だいぶ速くなりますね。
ちなみに、生成するネイティブイメージの名前の指定はオプションであり、省略した場合はクラス名がLowerCaseになってイメージが
作成されるみたいですね。
$ native-image HelloGraal $ ./hellograal World
で、ネイティブイメージが作成できると、なんか速そうな印象を受けますが、実際にはSubstrate VMにはいろいろと制限があるようなので
その点には注意が必要みたいです。
GraalVM allows you to compile your programs ahead-of-time into a native executable. The resulting program does not run on the Java HotSpot VM, but uses necessary components like memory management, thread scheduling from a different implementation of a virtual machine, called Substrate VM. Substrate VM is written in Java and compiled into the native executable. The resulting program has faster startup time and lower runtime memory overhead compared to a Java VM.
https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md
動的クラスローティングやリフレクションなど、けっこう目立つものがありますね…。
少し視点を替えて、今度は複数のクラスファイルから構成されるアプリケーションについて試してみましょう。
先ほどのサンプルプログラムから、一部処理を切り出します。 Task.java
public class Task { public String format(String message) { return String.format("Hello %s!!!!!", message); } }
こちらを使うように、先ほどのクラスを修正します。
HelloGraal.java
public class HelloGraal { public static void main(String... args) { String arg = args[0]; // System.out.printf("Hello %s!!%n", arg); Task task = new Task(); System.out.println(task.format(arg)); } }
だいぶわざとらしい感じがしますが、分割後のファイルだけlibディレクトリ向けに出力してコンパイルします。
$ rm *.class $ javac Task.java -d lib $ javac -cp lib HelloGraal.java $ find ./ -name '*.class' ./lib/Task.class ./HelloGraal.class
こういう場合は、「-cp」オプションでクラスパスを通してネイティブイメージを作成することになります。
$ native-image -cp .:lib HelloGraal hello-graal
javacな感じがしますね。
確認。
$ ./hello-graal World Hello World!!!!!
実行可能JARファイルから、ネイティブイメージを作成する
最後に、JARファイルからネイティブイメージを作成してみましょう。
ドキュメントによると、MANIFESTにmainメソッドを持ったクラスを指定しておく必要があるようです。
The name of the class containing the main method is the last argument; or you can use -jar and provide a .jar file that specifies the main method in its manifest.
ということは、実行可能JARファイルですね。
今回は、Maven Shade Pluginで簡単なプログラムを作成してみます。
依存関係およびプラグインの定義。ライブラリは、サンプルとしてCommons Lang 3を使うことにします。
<dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.8.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>org.littlewings.graal.App</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build>
プログラム側。
src/main/java/org/littlewings/graal/App.java
package org.littlewings.graal; import org.apache.commons.lang3.StringUtils; public class App { public static void main(String... args) { String arg = args[0]; System.out.println(StringUtils.replace("Hello $place_holder!!", "$place_holder", arg)); } }
パッケージングして、JARファイルを作成します。
$ mvn package
このJARファイルを「-jar」オプションで指定して、ネイティブイメージを作成します。
$ native-image -jar target/hello-assembly-image-0.0.1-SNAPSHOT.jar hello-assembly-image Build on Server(pid: 12397, port: 33175) [hello-assembly-image:12397] classlist: 272.74 ms [hello-assembly-image:12397] (cap): 684.67 ms [hello-assembly-image:12397] setup: 913.24 ms [hello-assembly-image:12397] (typeflow): 1,503.67 ms [hello-assembly-image:12397] (objects): 383.01 ms [hello-assembly-image:12397] (features): 64.26 ms [hello-assembly-image:12397] analysis: 1,984.27 ms [hello-assembly-image:12397] universe: 104.42 ms [hello-assembly-image:12397] (parse): 205.91 ms [hello-assembly-image:12397] (inline): 434.94 ms [hello-assembly-image:12397] (compile): 1,008.91 ms [hello-assembly-image:12397] compile: 1,776.27 ms [hello-assembly-image:12397] image: 144.62 ms [hello-assembly-image:12397] write: 42.16 ms [hello-assembly-image:12397] [total]: 5,275.13 ms
確認。
$ ./hello-assembly-image World Hello World!!
動きましたね。
なお、「-jar」指定時に作成するネイティブイメージの名前を指定しなかった場合は、JARファイルの名前を元にイメージが
作成されるようです。
$ native-image -jar target/hello-assembly-image-0.0.1-SNAPSHOT.jar $ ./hello-assembly-image-0.0.1-SNAPSHOT World Hello World!!
こんなところでしょうか。
とりあえず、インストールしてネイティブイメージを作るところまでは確認できたので、よしとしましょう。