Part 1: Java to native using GraalVM

Part 1: Java to native using GraalVM

One of the most amazing projects I’ve learned about this year is GraalVM.

I’ve learned about this project during Devoxx Poland (a Polish developer conference) at a talk by Oleg Šelajev. If you’re curious about everything GraalVM has to offer, not just the native Java compilation, please watch his video.

GraalVM is a universal/polyglot virtual machine. This means GraalVM can run programs written in:

  • Javascript
  • Ruby
  • Python 3
  • R
  • JVM-based languages (such as Java, Scala, Kotlin)
  • LLVM-based languages (such as C, C++).

In short: Graal is very powerful.

There is also the possibility to mix-and-match languages using Graal, do you want to make a nice graph in R from your Java code? No problem. Do you want to call some fast C code from Python, go ahead.

Installing GraalVM

In this blogpost though we’ll look at another powerful thing Graal can do: native-image compilation

Instead of explaining what it is, let’s just go ahead, install GraalVM and try it out.

To install GraalVM, download and unpack, update PATH parameters and you’re ready to go. When you look in the /bin directory of Graal you’ll see the following programs:

Here we recognise some usual commands, such as ‘javac’ and ‘java’. And if everything is setup correctly you’ll see:

$ java -version
openjdk version "1.8.0_172"
OpenJDK Runtime Environment (build 1.8.0_172-20180626105433.graaluser.jdk8u-src-tar-g-b11)
GraalVM 1.0.0-rc6 (build 25.71-b01-internal-jvmci-0.48, mixed mode)

Hello World with native-image

Next up, let’s create a “Hello World” application in Java:

public class HelloWorld {
   public static void main(String... args) {
      System.out.println("Hello World");
   }
}

And just like your normal JDK, we can compile and run this code in the Graal virtual machine:

$ javac HelloWorld.java
$ java HelloWorld
Hello World

But the real power of Graal becomes clear when we use a third command: native-image

This command takes your Java class(es) and turns them into an actual program, a standalone binary executable, without any virtual machine! The commands you pass to native-image very similar to what you would pass to java. In this case we have the classpath and the Main class:

$ native-image -cp . HelloWorld
Build on Server(pid: 63941, port: 60051)*
[helloworld:63941]    classlist:   1,236.06 ms
[helloworld:63941]        (cap):   1,885.61 ms
[helloworld:63941]        setup:   2,758.47 ms
[helloworld:63941]   (typeflow):   3,031.39 ms
[helloworld:63941]    (objects):   2,136.63 ms
[helloworld:63941]   (features):      46.04 ms
[helloworld:63941]     analysis:   5,304.17 ms
[helloworld:63941]     universe:     205.46 ms
[helloworld:63941]      (parse):     640.12 ms
[helloworld:63941]     (inline):   1,155.06 ms
[helloworld:63941]    (compile):   3,436.76 ms
[helloworld:63941]      compile:   5,594.76 ms
[helloworld:63941]        image:     749.82 ms
[helloworld:63941]        write:     653.29 ms
[helloworld:63941]      [total]:  16,753.87 ms
$ ls -ltr
-rw-r--r--  1 royvanrijn  wheel  119 Sep 20 09:36 HelloWorld.java
-rw-r--r--  1 royvanrijn  wheel  425 Sep 20 09:38 HelloWorld.class
-rwxr-xr-x  1 royvanrijn  wheel  5596400 Sep 20 09:41 helloworld
$ ./helloworld 
Hello World

Now we have an executable that prints “Hello World”, without any JVM in between, just 5.6mb. Sure, for this example 5mb isn’t that small, but it is much smaller than having to package and install an entire JVM (400+mb)!

Docker and native-image

So what else can we do? Well, because the resulting program is a binary, we can put it into a Docker image without ANY overhead. To do this we’ll need two different Dockerfile’s, the first is used to compile the program against Linux (instead of MacOS or Windows), the second image is the ‘host’ Dockerfile, used to host our program.

Here is the first Dockerfile:

FROM ubuntu

RUN apt-get update && \
    apt-get -y install gcc libc6-dev zlib1g-dev curl bash && \
    rm -rf /var/lib/apt/lists/*

# Latest version of GraalVM (at the time of writing)
ENV GRAAL_VERSION 1.0.0-rc6
ENV GRAAL_FILENAME graalvm-ce-${GRAAL_VERSION}-linux-amd64.tar.gz

# Download GraalVM
RUN curl -4 -L https://github.com/oracle/graal/releases/download/vm-${GRAAL_VERSION}/${GRAAL_FILENAME} -o /tmp/${GRAAL_FILENAME}

# Untar and move the files we need:
RUN tar -zxvf /tmp/${GRAAL_FILENAME} -C /tmp \
    && mv /tmp/graalvm-ce-${GRAAL_VERSION} /usr/lib/graalvm

RUN rm -rf /tmp/*

# Create a volume to which we can mount to build:
VOLUME /project
WORKDIR /project

# And finally, run native-image
ENTRYPOINT ["/usr/lib/graalvm/bin/native-image"]

This image can be created as follows:

$ docker build -t royvanrijn/graal-native-image:latest .

Using this image we can create a different kind of executable. Let’s create our application using the just created docker image:

$ docker run -it \
  -v /Projects/graal-example/helloworld/:/project --rm \
  royvanrijn/graal-native-image:latest \
  --static -cp . HelloWorld -H:Name=app

Build on Server(pid: 11, port: 40905)*
[app:11]    classlist:   3,244.85 ms
[app:11]        (cap):   1,023.94 ms
[app:11]        setup:   1,986.81 ms
[app:11]   (typeflow):   4,285.18 ms
[app:11]    (objects):   2,008.19 ms
[app:11]   (features):      57.07 ms
[app:11]     analysis:   6,446.49 ms
[app:11]     universe:     255.45 ms
[app:11]      (parse):     926.85 ms
[app:11]     (inline):   1,496.69 ms
[app:11]    (compile):   4,953.85 ms
[app:11]      compile:   7,689.47 ms
[app:11]        image:     806.53 ms
[app:11]        write:     573.77 ms
[app:11]      [total]:  21,160.90 ms
$ ls -ltr app
-rwxr-xr-x  1 royvanrijn  wheel  6766144 Sep 20 10:11 app
$ ./app
-bash: ./app: cannot execute binary file

This results in an executable ‘app’, but this is one I can’t start on my MacBook, because it is a statically linked Ubuntu executable. So what do all these commands mean? We’ll let’s break it down:

The first part is just running Docker:
     docker run -it

Next we map my directory containing the class files to the volume /project in the Docker image:
     -v /Projects/graal-example/helloworld/:/project --rm

This is the Docker image we want to run, the one we just created:
     royvanrijn/graal-native-image:latest

And finally we have the commands we pass to native-image inside the Docker image
We start with --static, this causes the created binary to be a statically linked executable
     --static

We have the class path and Main class:
     -cp . HelloWorld

And finally we tell native-image to name the resulting executable 'app'
     -H:Name=app

But we can do something cool with it using the following, surprisingly empty, Dockerfile:

FROM scratch
COPY app /app
CMD ["/app"]

We start with the most empty Docker image you can have, scratch and we copy in our app executable and finally we run it. Now we can build our helloworld image:

$ docker build -t royvanrijn/graal-helloworld:latest .
Sending build context to Docker daemon  34.11MB
Step 1/3 : FROM scratch
 ---> 
Step 2/3 : COPY app /app
 ---> f0894b299e8f
Removing intermediate container 37182de1ef68
 ---> 49ff43413c7a
Step 3/3 : CMD ["/app"]
 ---> Running in ea69a913d243
Removing intermediate container ea69a913d243
 ---> ab33b4d59de3
Successfully built ab33b4d59de3
Successfully tagged royvanrijn/graal-helloworld:latest

$ docker images
REPOSITORY                                                  TAG                         IMAGE ID            CREATED             SIZE
royvanrijn/graal-helloworld                                 latest                      ab33b4d59de3        5 seconds ago       6.77MB

We’ve now turned our Java application into a very small Docker image with a size of just 6.77MB!

In the next blogpost Part 2 we’ll take a look at Java applications larger than just HelloWorld. How will GraalVM’s native-image handle those applications, and what are the limitations we’ll run into?