GraalVM native image
An ordinary jar runs on the JVM: the virtual machine loads classes, and the JIT compiler turns hot code into machine code while the program runs. A native image works differently: the native-image tool from GraalVM compiles the application together with the needed part of the JDK ahead of time into a regular executable for a specific OS, like a C program. Such a file starts without an installed Java, in tens of milliseconds, and uses noticeably less memory.
Why Micronaut fits
When building a native image, GraalVM assumes a "closed world": everything the application can call must be known in advance. Reflection, dynamic proxies and loading classes by name break this assumption, and for them you have to write configuration files by hand. Micronaut hardly uses reflection: beans, routes, serialization and Micronaut Data queries are already generated at compile time. That is why most Micronaut applications build into a native image without extra configuration.
What you need
- GraalVM for Java 25 instead of a regular JDK. The easiest way is SDKMAN:
sdk install java 25.4.4+1-graal, thensdk use java 25.4.4+1-graal. - A C toolchain for your OS:
gccand developer libraries on Linux, Visual Studio Build Tools on Windows, Xcode Command Line Tools on macOS. - A few gigabytes of free memory: the build is heavy.
Building
The generated project is already prepared: the native-maven-plugin comes through micronaut-parent. You only change the packaging type:
./mvnw package -Dpackaging=native-imageThe build takes several minutes. The result is the file target/blog (target\blog.exe on Windows). You run it directly, without java:
./target/blogThe application answers on the same addresses and reads the same environment variables as the jar. There is also a container variant: ./mvnw package -Dpackaging=docker-native.
Jar or native file
| Executable jar | Native image | |
|---|---|---|
| Needs Java on the server | yes | no |
| Build time | seconds | minutes |
| Startup | about a second | tens of milliseconds |
| Memory | more | less |
| One file for every OS | yes | no, built for each OS |
| Peak speed over a long run | high thanks to the JIT | usually a little lower |
A native image pays off where the application is started and stopped often: cloud functions (serverless), autoscaling in Kubernetes, command-line tools. For an ordinary long-running service, an executable jar is simpler and perfectly good.
How to check
Before a native build, make sure the tests pass on the JVM. Libraries that use reflection may need hints for GraalVM, which go into src/main/resources/META-INF/native-image/. For your own classes in Micronaut, @Serdeable and @Introspected are usually enough.
Common mistake
If you start a native build on a regular JDK, Maven stops at the native:compile-no-fork step:
[ERROR] Failed to execute goal org.graalvm.buildtools:native-maven-plugin:1.1.12:compile-no-fork (default-compile-no-fork)
on project blog: native-image is not installed in your JAVA_HOME. This probably means that the JDK at '...' is not
a GraalVM distribution. The GraalVM Native Maven Plugin requires GRAALVM_HOME or JAVA_HOME to be a GraalVM distribution.Install GraalVM, point JAVA_HOME or GRAALVM_HOME to it, then check with native-image --version.