Introduction to GraalVM Native Image

Abstract

GraalVM is a high-performance, universal polyglot VM that can run applications written in various programming languages. It can generate native images using ahead-of-time (AOT) compilation. Native images can improve startup time and reduce memory footprint of applications. This article shows how to use GraalVM to build a native image of a simple program written in Java and then compares file space usage, startup time and memory usage of the native application versus its the managed version.

1. What is GraalVM?

GraalVM is a high-performance, universal polyglot VM that can run applications written in dynamic languages (e.g. Python, JavaScript, R, Ruby), LLVM-based languages (e.g. C, C++) or JVM-based languages (e.g. Scala, Kotlin, Clojure, Java).

GraalVM can be embedded in both managed and native applications. It can run either standalone or in the context of OpenJDK, Node.js, Oracle Database, or MySQL.

GraalVM can generate native images using ahead-of-time (AOT) compilation. Native images can improve startup time and reduce memory footprint of applications.

GraalVM is available in two editions - Community Edition (CE) and Enterprise Edition (EE). The Community Edition (CE) 19.0.0 has open-source license and is free for production use.

GraalVM 19.0.0 is based on JDK version 8u212. Work is in progress [11] to add support for Java 11+.

2. What is GraalVM Native Image?

GraalVM Native Image allows you to ahead-of-time compile Java code to a standalone executable, called a native image. This executable includes the application, the libraries, the JDK and does not run on the Java VM, but includes necessary components like memory management and thread scheduling from a different virtual machine, called “Substrate VM”. …​ The resulting program has faster startup time and lower runtime memory overhead compared to a Java VM.

— www.graalvm.org

A native image is preferred over the JVM when,

  1. startup time and memory footprint is important.

  2. Java code needs to be embedded with other native applications.

Due to fast startup time and low memory footprint, native images are well-suited for CLI and serverless applications.

Frameworks such as Fn Project, Micronaut and Helidon [4] already support GraalVM native images.

There is work in progress to add support for GraalVM in Netty [5],[9], Spring Framework [6], Spring Boot [10], Vert.x [7],[8] and more.

GraalVM Native Image for Java has limitations.

GraalVM Native Image is available as an early adopter plugin. This means its functionality is subject to change while its specification is being developed by the Java community.

3. Preparations

3.1. Platform and tools used in this experiment

  1. GraalVM Community Edition 19.0.0

  2. GraalVM native-image plugin

  3. Maven plugin com.oracle.substratevm:native-image-maven-plugin

  4. macOS Mojave

  5. Xcode command line tools

  6. jpackage tool

  7. GNU Time

3.2. Install GraalVM

  1. Download GraalVM Community Edition 19.0.0.

  2. Unpack the archive

    $ tar xf graalvm-ce-darwin-amd64-19.0.0.tar.gz -C <GRAALVM_INSTALL_DIR>
  3. Set GRAALVM_HOME environment variable.

    The exact path of $GRAALVM_HOME will depend on the operating system.
    On macOS
    $ export GRAALVM_HOME="<GRAALVM_INSTALL_DIR>/Contents/Home"

3.3. Install Xcode command line tools

This step is required only on macOS.

In the absence of Xcode command line tools, native image creation may fail with error xcrun: error: invalid active developer path

If Xcode command line tools are not install on your macOS machine, install them.

$ xcode-select --install

4. Native image

4.1. Create a simple program

  1. Create a Maven-based hello-world module.

  2. Configure this module to use Java 8

    pom.xml
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
  3. Create a Hello World class

    package rahulb.experiments.graalvm.helloworld;
    
    public class Main {
    
        public static void main(String[] args) {
            System.out.println("Hello World!");
        }
    }

4.2. Build a native image

  1. Add native-image plugin to GraalVM using GraalVM Updater tool.

    $ ${GRAALVM_HOME}/bin/gu install native-image
  2. Add com.oracle.substratevm:native-image-maven-plugin to the build

    pom.xml
    <properties>
        <graalvm.version>19.0.0</graalvm.version>
        ...
    </properties>
    
    <build>
        <plugins>
            <plugin>
                <groupId>com.oracle.substratevm</groupId>
                <artifactId>native-image-maven-plugin</artifactId>
                <version>${graalvm.version}</version>
                <executions>
                    <execution>
                        <id>build-native-image</id>
                        <phase>package</phase>
                        <goals>
                            <goal>native-image</goal>
                        </goals>
                        <configuration>
                            <mainClass>rahulb.experiments.graalvm.helloworld.Main</mainClass>
                            <imageName>hello-world</imageName>
                            <buildArgs>--no-server --no-fallback</buildArgs> (1)
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    1 --no-server means 'do not use image-build server'. --no-fallback means 'build standalone image or report failure'
  3. Set JAVA_HOME environment variable equal to $GRAALVM_HOME

    $ export JAVA_HOME="${GRAALVM_HOME}"
  4. Build the package

    $ mvn clean package

    This will generate a native image named hello-world in target directory of the Maven project.

    On my machine, it took 87 seconds to build the native image.

The GraalVM version used to build a native image can be determined from the native image file.

$ strings target/hello-world | grep -A 1 com.oracle.svm.core.VM

com.oracle.svm.core.VM
GraalVM 19.0.0 CE

4.3. Comparisons - native application vs. managed application

Managed application means application running on the Java Virtual Machine (JVM).

4.3.1. File space usage

To determine the file space usage of the managed program, package the program and the Java Runtime Environment (JRE) using jpackage tool. See Using jpackage tool for instructions to generate the package.

Combined size of the application and the JRE
$ du --summarize --dereference --human-readable ./hello-world.app/

123M	./hello-world.app/ (1)
1 The application and the JRE together occupy 123 MB of file space.

Now, let us determine the size of the native image.

$ du --dereference --human-readable <PROJECT_HOME>/target/hello-world

2.4M	./target/hello-world (1)
1 The native image occupies 2.4 MB of file space.

Native images can help reduce the size of container (e.g. Docker) images.

4.3.2. Startup time

Native application
$ time ./target/hello-world

Hello World!

real    0m0.005s (1)
user    0m0.002s
sys     0m0.002s
1 The native application finished in 5 milliseconds
Managed application
$ time ${JDK11_HOME}/bin/java -cp ./target/hello-world-1.0.0.jar rahulb.experiments.graalvm.helloworld.Main

Hello World!

real    0m0.103s (1)
user    0m0.103s
sys     0m0.024s
1 The managed application finished in 103 milliseconds

Native application starts up faster than the managed application.

4.3.3. Memory usage

Native application
$ gtime -f "\nMax resident set size: %M KB" target/hello-world

Hello World!

Max resident set size: 1704 KB (1)
1 Maximum RSS was about 1.7 MB
Managed application
$ gtime -f "\nMax resident set size: %M KB" java -cp target/hello-world-1.0.0.jar rahulb.experiments.graalvm.helloworld.Main

Hello World!

Max resident set size: 32840 KB (1)
1 Maximum RSS was about 32 MB

Native application has lower memory footprint than the managed application.

5. Summary

This article showed how to use GraalVM to build a native image of a simple program written in Java. It also showed how a native application occupies lesser file space, starts up faster and has lower memory footprint than its managed version.

Appendix A: Using jpackage tool

A.1. Install jpackage tool

  1. Download jpackage tool

  2. Unpack the archive

    $ tar xzf openjdk-13-jpackage+49_osx-x64_bin.tar.gz

A.2. Create a package using jpackage tool

  1. Prepare for packaging

    Example
    $ mkdir -p /tmp/packaging-workspace/input
    $ mkdir -p /tmp/packaging-workspace/output
    $ cd /tmp/packaging-workspace
    
    $ cp <PROJECT_HOME>/target/hello-world-1.0.0.jar ./input
  2. Generate a package

    Example
    $ cd /tmp/packaging-workspace
    
    $ jpackage create-app-image \
        --output ./output \
        --input ./input \
        --name hello-world \
        --main-class rahulb.experiments.graalvm.helloworld.Main \
        --main-jar hello-world-1.0.0.jar

    This generates a directory named hello-world.app in the output directory.

    Package contents
    $ cd /tmp/packaging-workspace/output
    
    
    $ find . -maxdepth 4 -mindepth 3 -type d
    
    ./hello-world.app/Contents/MacOS (1)
    ./hello-world.app/Contents/Resources
    ./hello-world.app/Contents/runtime
    ./hello-world.app/Contents/runtime/Contents (2)
    ./hello-world.app/Contents/Java (3)
    1 Contains the application launcher
    2 Contains Java Runtime Environment (JRE)
    3 Contains application JARs
  3. Test the package by launching the application

    Example
    $ cd /tmp/packaging-workspace/output
    
    
    $ ./hello-world.app/Contents/MacOS/hello-world (1)
    
    Hello World! (2)
    1 Application launcher
    2 Application output