CLOVER🍀

That was when it all began.

GraalVMをインストールして、Javaアプリケーションからネイティブイメージを作って遊ぶ

そろそろ、1度GraalVMを試してみようかなと思いまして。

GraalVM

Graal or GraalVM?

ちゃんと情報を追っていなかったのですが、GraalとGraalVMは違うもののようです。

Javaの新JITコンパイラ、Graalを解説

詳説GraalVM(1) イントロダクション - Fight the Future

GraalはJavaで書かれたJITコンパイラ、GraalVMは多言語用の仮想マシンを指します、と。

Java開発者にとって、Graalはいくつか別々の、しかし関連のある複数のプロジェクトとみなせる。HotSpotの新しいJITコンパイラであり、また新しいpolyglotな仮想マシンである。以降JITコンパイラはGraal、新しいVMはGraalVMとして言及する。

Javaの新JITコンパイラ、Graalを解説

GraalVMを使うと多言語の他に、ネイティブイメージの作成ができたり、アプリケーションに組み込んで使うこともできるそうな。

また、ドキュメントによるとネイティブイメージを生成する機能もJavaで書かれていて、Substrate VMと呼ばれるようです。

Ahead-of-time Compilation

https://github.com/oracle/graal/tree/vm-1.0.0-rc14/substratevm

なるほど?

今回はこのGraalVMを使って、Javaアプリケーションからネイティブイメージを作成して動かしてみたいと思います。

インストール

それでは、GraalVMをインストールしてみましょう。

Install 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を使います。

Downloads

ダウンロードして、今回は「/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 Images

「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.

Image Generation Options

ということは、実行可能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!!

こんなところでしょうか。

とりあえず、インストールしてネイティブイメージを作るところまでは確認できたので、よしとしましょう。