This is about writing java software that does not need a JVM in order to execute it. For this the GraalVM can be used. In the end it provides a binary that is ways smaller than a full JVM and can directly be executed on the target operating system.
Why should it be used?
Well, good question. There are multiple use cases:
Small java programm
Write a small java program instead of a script and have the full power of the java ecosystem like libraries, tooling, IDEs and testing capabilities.
Use it in a docker image
Instead of using a docker image that contains a java runtime simply build a binary and then put into the image and then run it.
Deliver binary software
Another use case could be that the software should be delivered as a binary that directly can be executed. So simply put e.g. a full Java EE/Jakarta EE or Spring Boot application into this binary and simply run it.
You want to use another language
It is possible to use GraalVM
with JavaScript, Python, Ruby, R or e.g. Groovy. Even C or C++ would be possible. But that’s another thing. The focus will on Java.
Pros and Cons
+ No JVM required for running java software
+ Small footprint for usage in docker compared to an image with java installed
+ Fancy way of writing scripts
– Compared to a script quite huge binary
– Precompiled and optimized binary -> The main advantage of the JIT of java cannot be used.
– Hard to analyze problems an by default the java tooling for profiling and analysis of running JVMs is not working
How to use it
Install it
Simply download GraalVM and then unpack it. Now there is the assumption there is a folder called graalvm-ce-java11-20.0.0
in the directory /home/user/Downloads
Then add the /home/user/Downloads/graalvm-ce-java11-20.0.0/bin
to the $GRAALVM_HOME
variable. E.g. by using
GRAALVM_HOME=/home/user/Downloads/graalvm-ce-java11-20.0.0"
Then run ${GRAALVM_HOME}/bin/gu install native-image
. This provides the necessary tool for creating a binary.
Alternatively you can add the binaries to your $PATH
and then simply run it.
Write it
Simply download this archive for getting started the fast way.
It contains a java class with the main method. Then there will be jar
having that main class in it’s meta data. This will be built with java 11 by using gradle and using the gradle wrapper.
Of course there can added some libraries as dependencies to the build and then include them directly into the resulting jar
. This program only outputs Hello World!
in order to demonstrate how this works at all.
Build it
Simply build the java with gradle. There is no need to download anything, but of course there must be at least java 11 installed.
./gradlew build
Then build the native image via
> ${GRAALVM_HOME}/bin/native-image -jar build/libs/shell-java.jar Build on Server(pid: 16066, port: 38929)* [shell-java:16066] classlist: 5,545.74 ms, 1.00 GB [shell-java:16066] (cap): 2,914.20 ms, 1.00 GB [shell-java:16066] setup: 8,104.61 ms, 1.00 GB [shell-java:16066] (typeflow): 15,998.90 ms, 1.20 GB [shell-java:16066] (objects): 10,900.20 ms, 1.20 GB [shell-java:16066] (features): 804.55 ms, 1.20 GB [shell-java:16066] analysis: 28,605.08 ms, 1.20 GB [shell-java:16066] (clinit): 638.55 ms, 1.20 GB [shell-java:16066] universe: 2,078.62 ms, 1.20 GB [shell-java:16066] (parse): 4,829.25 ms, 1.20 GB [shell-java:16066] (inline): 4,285.53 ms, 1.20 GB [shell-java:16066] (compile): 27,893.72 ms, 1.40 GB [shell-java:16066] compile: 39,276.90 ms, 1.40 GB [shell-java:16066] image: 4,168.67 ms, 1.40 GB [shell-java:16066] write: 608.59 ms, 1.40 GB [shell-java:16066] [total]: 89,396.08 ms, 1.40 GB
This generates the binary called shell-java
.
Run it
Simply run
./shell-java Hello World!
That’s it!
Something about the performance
In order to measure the runtime of the Hello World
simply run:
date +%N && ./shell-java && date +%N 184142929 Hello World! 193498880
This prints the nanoseconds before and after the execution. In this case the execution time is about 9 ms. Ok this is not a highly precise result but it was about that time every time it was executed. This was with java 11. Trying this with java 8 as well and the result was about the same.
Now doing the same with running the jar
with java
date +%N && java -jar build/libs/shell-java.jar && date +%N 654410089 Hello World! 961069090
This is about 300 ms and thus about 30 times slower than the binary!
And something about the size of the binary
Well the binary is 6867104
bytes in size and therefore 6.6 MiB
. This is with java 11. Repeating the same with java 8 (ok a little bit old fashioned) and there the size was about 3,4 MiB
. The size of the jar
is 730 bytes. Decide yourself whether this is good or not.
Sources
GraalVM
GraalVM Community Edition Release Downloads
GraalVM for java 11 and Linux AMD64 architecture
GraalVM Documentation