Krzysztof Mucha: Backend Developer at co.brick
Introduction to GraalVM Compiler and Native Image
Since co.brick is dedicated to cloud-native solutions, we are constantly looking for new opportunities to limit the number of resources consumed by our services and speed up their startup times. That is why I decided to take a closer look at GraalVM, which for some time now is a production-ready solution.
GraalVM is a Java VM and JDK fully implemented in Java. It consists of three main components:
- GraalVM Compiler, a new JIT compiler for Java applications,
- GraalVM Native Image, which provides ahead-of-time compilation for Java applications,
- Truffle language Implementation framework for running other than JVM based languages on GraalVM.
In this article, I will cover the basics of the first two components.
1. GraalVM Compiler
From the perspective of people working with JVM-based languages, the most important part of GraalVM is a new JIT compiler fully written in Java. Current implementations of C1 and C2 compilers are very complex and Oracle had troubles with their development. That is why nowadays GraalVM is gaining popularity. In its present version, GraalVM is a standalone JDK supporting LTS language versions (currently 8 and 11).
The new compiler is optimized especially for modern Java code, in which we are using more high-level abstractions and declarative approach. What does this mean for us developers? We no longer have to be afraid that creating immutable objects and collections will slow down our applications on production.
2. GraalVM Native Image
Native Image is an add-on to GraalVM and needs additional installation, which is quite straightforward. After installing it, you will be able to create platform-specific, self-contained executable binaries from Java bytecode. During the binary creation process, JIT is used for ahead-of-time (AOT) compilation.
Native Image Limitations
There are some limitations when it comes to building a native image. Some of the dynamic language aspects need to be configured before compilation and some of them are not supported.
Static analysis will reach out to every part of your application. Even libraries that are not used explicitly by your program will be checked. So do not be surprised if you see weird exception messages during compilation.
Supported (with extra configuration):
- Dynamic Class Loading,
- Dynamic Proxy,
- Security Manager,
- Class Initializers,
- Threads (deprecated methods),
- Debugging and Monitoring – since the bytecode is not available when working with the native image, there is no support for JVM debugging and monitoring tools.
Be aware that this is only a shortcut. A full limitations list can be found in GraalVM documentation.
Native Image binary is not completely free from the virtual machine. Compiled binary has a built-in micro virtual machine called SubstrateVM. It contains necessary components, such as memory management, thread scheduling, garbage collector, etc.
For this test, I decided to start with a very basic POC with an application written in Java using Micronaut framework. The application makes object transformations in a stream. Each transformation produces a new immutable object.
For load tests, the K6 tool was used. All tests were made on a local machine (MacBook Pro i7 16GB 2019). First of all, the application startup time was measured. Directly after startup, I made one request. I calculated the average from a series of ten measurements. Following that I run a 6-minute load test simulating 150vus. After that time I made one additional request.
Results of those tests are gathered in the table below.
Load Tests Summary
- OpenJDK 11
- GraalVM Compiler
- Native Image
When building and running applications we have to consider five major factors based on customer requirements:
- Startup Speed,
- Peak Throughput,
- Max Latency,
- Small Packaging,
- Low Memory Footprint.
The sample POC showed that there is no holy grail and we always have to make compromises. Luckily, with GraalVM we can choose one of two approaches to run a Java application. When Peak Throughput and Max Latency is critical, JIT will still do the best job. But when Startup Speed and Low Memory Footprint is critical, AOT and Native Image will be the best option. So, from now on it is reasonable to build Cloud Functions with Java and compile them into binary. When it comes to fast scaling of multiple instances of service, Native Image should also be your first preference.
How do you like this article? Feel free to share it with your colleagues and friends! Make sure to follow our social media for more insights and knowledge.