Getting Started with GraalVM and Java Native

09 Mar 2021  Sergio Martin Rubio  6 mins read.

GraalVM is one of the virtual machines to run applications written in multiple languages like JavaScript, Python or Java. One of the main features of GraalVM is Native Images. GraalVM allows you to generate a native image of your Java code, so it does not need to run on the JVM, and includes all the necessary componentes.

Build a Native Java Application


  • Install GraalVM. For Mac OS users SdkMan provides GraalVM images.

    sdk install java
  • Install the GraalVM native tool.

    gu install native-image
  • For Mac OS users install Xcode tools if you don’t have them yet.

    xcode-select --install

Hello World Application

Now you can write your first GraalVM application.

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Hello, World!");

and run:

native-image HelloWorld

During the compilation you will see something like this:

[helloworld:65139]    classlist:   3,096.34 ms,  0.96 GB
[helloworld:65139]        (cap):   3,308.79 ms,  0.96 GB
[helloworld:65139]        setup:   6,654.19 ms,  0.96 GB
[helloworld:65139]     (clinit):     233.40 ms,  1.20 GB
[helloworld:65139]   (typeflow):  14,796.92 ms,  1.20 GB
[helloworld:65139]    (objects):   6,133.03 ms,  1.20 GB
[helloworld:65139]   (features):     308.55 ms,  1.20 GB
[helloworld:65139]     analysis:  21,670.32 ms,  1.20 GB
[helloworld:65139]     universe:     782.71 ms,  1.20 GB
[helloworld:65139]      (parse):   4,390.33 ms,  1.20 GB
[helloworld:65139]     (inline):   1,680.80 ms,  1.44 GB
[helloworld:65139]    (compile):  17,090.49 ms,  1.89 GB
[helloworld:65139]      compile:  23,661.70 ms,  1.89 GB
[helloworld:65139]        image:   1,185.79 ms,  1.89 GB
[helloworld:65139]        write:     437.82 ms,  1.89 GB
[helloworld:65139]      [total]:  57,892.54 ms,  1.89 GB

The generation of the executable file will took around 1 minute on my machine (MacBook Air (M1, 2020) - 16 GB) in the first execution. Once it is finished it generates a helloworld file that you can run from your terminal or by simply double clicking on it.

The generated file is 7.7MB, which is quite impressive for a Java application since this executable does need a JVM.

In the real world you might want to use something like Kubernetes or EC2 to deploy your native Java application and you will see how to do it in the next section.

Dockerized Java Native Application


  • You need to have Docker installed on your machine to create Docker images.
  • Bytecode class file HelloWorld.class: javac

Getting Started

  1. Create a Java native executable in a Linux machine. If you built the native image on a system other than Linux and try to use that image in a docker container with a Linux base image, it will not work! and you will get something like standard_init_linux.go:219: exec user process caused: exec format error, since the binaries generated by GraalVM are only compatible with the system where they are generated, this means if you are running macOS, binaries built on macOS won’t work on a Linux distribution. The workourd for this is to generate the binaries from a Linux host with Docker. Steps:

    • Create a Dockerfile with GraalVM as the base image with native-image installed.

      WORKDIR /opt/graalvm
      RUN gu install native-image
      ENTRYPOINT ["native-image"]
    • Run the GraalVM image to generate a static native Java executable with Linux binaries.

      docker run -it -v <PATH_TO_JAVA_BYTECODE_CLASS_FILE>:/opt/cp -v <PATH_TO_OUTPUT_FOLDER>:/opt/graalvm graalvm-native-image HelloWorld

      where we are mounting two directories, one for the path where the Java bytecode class file is (HelloWorld.class in this case) and another one for the output executable file; and HelloWorld is the name of the entry class.

      After running this command you will get a helloworld file in the specified output folder.

      You can also specify the output file name with -H:Name=<name>

  2. Build a Docker image for your Java executable file.


    FROM debian:buster-slim
    COPY helloworld /opt/helloworld
    CMD ["/opt/helloworld"]

    and run:

    docker build . -t graalvm-hello-world

    This will create a Docker image with a size of ~72MB, but we can do better than this! We can compile a static native image that contains all the necessary resources, so we can use a smaller Docker base image.

    If we run the Docker command from the previous step with a --static flag it will bundle all the required binaries in the native image so the Docker container can run without any additional dependencies.

    docker run -it -v <PATH_TO_JAVA_BYTECODE_CLASS_FILE>:/opt/cp -v <PATH_TO_OUTPUT_FOLDER>:/opt/graalvm graalvm-native-image --static HelloWorld

    As a result the output native image will went up from 8.6MB to 9.8MB.

    Now we can use a smaller base Docker image:

    FROM scratch
    COPY helloworld /opt/helloworld
    CMD ["/opt/helloworld"]

    and after running again:

    docker build . -t graalvm-hello-world

    you will get an image of size ~10MB

  3. Finally you can run the Docker image!

    time docker run --rm graalvm-hello-world


    Hello, World!
    docker run graalvm-hello-world  0.17s user 0.08s system 14% cpu 1.758 total

    As you can see it takes only 0.17s to run the native image, and 1.7s in total (this is including the Docker overhead).

Native Image Execution vs JVM Execution

Native images provide some benefits like a reduced executable size compared to a traditional Java application running in the JVM.

However, there is no difference in terms of execution time:


Native images can provide benefits in terms of executable size, however realtime bytecode manipulation or reflection is problematic and it is a tradeoff that you need to consider before using GraalVM with Java.