GraalVM Native Image can be a compelling platform for your Java cloud applications. Native Image precompiles your applications ahead of time (AOT). This obviously removes the need to compile at the start of runtime, so you get apps that start almost instantly and have a lower memory footprint. This saves resources for the just-in-time (JIT) compiler infrastructure, class metadata, and so on.
Beyond the fast startup, there are different reasons why developers use native images with their applications. There’s the cloud friendliness of such deployments and obfuscating the code to improve security.
Sometimes better performance is about throughput and how many clients one instance of a service can handle; sometimes it’s about serving individual responses as fast as possible, memory usage, startup, or even about comparing the size of the deployment because in certain scenarios, that can influence, for example, cold-start performance.
What’s important is that with a few relatively simple tricks, plus using advanced GraalVM Native Image features, you can leverage all of these advantages for your applications.
Creating a sample application
Imagine that you have a simple sample application, which is a Micronaut microservice that responds to HTTP queries and computes prime numbers. It’s a simple one-controller application that simulates the business logic by generating garbage collector pressure with some temporary objects by conveniently using the Java Stream API and the CPU to compute sequences of prime numbers very inefficiently: by trying all numbers as factors, including even numbers larger than 2.
Better memory management
A reduced memory footprint at runtime is one important metric where Native Image offers an improvement for running your application using a generic JDK.
The savings are mostly a one-time advantage because executables built with Native Image contain all the code in the application already compiled and all the classes analyzed. This allows you to leave out the class metadata and JIT compiler infrastructure.
However, the data set your application operates on takes a similar amount of memory, because the object layouts are similar on the JVM and in the native image. So if an application holds a few gigabytes of data in memory, Native Image will take a similar amount minus the 200 MB to 300 MB slice I talked about above.
Native Image obviously includes a runtime to support the application, which operates under the assumption that memory is managed and garbage is collected when needed. The implementation of the runtime used in native images, including the garbage collection, is from the GraalVM project.
The services mentioned above are written in Java, and since during the build of your application classes, dependencies and JDK class library classes must be compiled anyway, the runtime is compiled together with your application.
The garbage collector exposes the same memory configuration options for specifying heap sizes as the JDK exposes, for example: * –Xmx – for maximum heap size and * -Xmn – for young generation size. The –XX:+PrintGC and –XX:+VerboseGC options are also available if you feel the need to look behind the curtain or fine-tune the garbage collector for your particular workload.
If configuring the generation size is not your first preference, you could build the native image with the multithreaded G1 GC garbage collector. G1 GC is a performance-oriented feature included in GraalVM Enterprise, and it has a very straightforward configuration.
To enable G1 GC, pass the –gc=G1 parameter to the Native Image build process. Since you’re working with a Micronaut application and relying on its Gradle plugin for configuring and running the Native Image builder, specify the option in the build.gradle file.
The code part. This part is easiest to grasp. This part contains all the classes and methods that needed to be included in the image because the static analysis found a possible code path to them or their inclusion was preconfigured with the explicit configuration.
The code part includes your classes, their dependencies, the dependencies’ dependencies, and so on up to the JDK class library classes and classes generated at build time. In other words, it’s all the Java bytecode that will be executed in the resulting executable.