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?