I help you get started with native Java with GraalVM, an open-source Java compiler. It creates native executables that are smaller, start faster, use less memory, and are more secure.
The GraalVM project has three parts:
At least in the Java world, people usually mean “GraalVM Native Image AOT compiler” when they just talk about “GraalVM”.
I wrote a sample application that converts all images in the current directoy into PDFs in the pdf subdirectory. It’s available as a Spring Boot version and a Quarkus one. Both are just a tiny shell around the same class that creates the PDF.
Please see the above section “What is GraalVM?” for what GraalVM is. And see the next section “Frameworks” on how to build native executables and fat JARs for these applications.
./mvnw clean native:compile -DskipTests=true -Pnative
The native executable will be target\[artifactId].
Run this command:
./mvnw clean package -DskipTests=true
The fat JAR will be target\[artifactId]-[version].jar.
Install the CLI, as described here. On the Mac, you should use SDKMAN.
I followed the Quarkus guide for creating command line applications and used this command to create my sample command line application:
quarkus create app --maven --java=17 --wrapper native-java-all-thumbs-quarkus
This will create a new project in the directory native-java-all-thumbs-quarkus.
./mvnw clean install -DskipTests=true -Dnative
The native executable will be target\[artifactId]-[version]-runner.
By default, Native Image can only do global optimizations and will have slower performance than JIT Java. Profile-Guided Optimizations (PGO) can narrow that lead in four steps:
./mvnw clean install -DskipTests=true -Dnative -Dquarkus.native.additional-build-args=--pgo-instrument
default.iprof file with profiling data in the current directory.default.iprof into the directory where you build the executable../mvnw clean install -DskipTests=true -Dnative -Dquarkus.native.additional-build-args=--pgo=[absolut pat]/default.iprof
Add this to the properties section towards the beginning of the pom.xml:
<quarkus.package.type>uber-jar</quarkus.package.type>
Then run this command:
./mvnw clean package
The fat JAR will be target\[artifactId]-[version]-runner.jar.
This is what the standard javac Java compiler from OpenJDK distributions does. It uses Java source code as the input.
The result is platform-independent Java bytecode in JAR/WAR/EAR files.
This is what GraalVM Native Image does. It uses the Java bytecode as the input (see previous section).
The result is a platform-specific, native executable. Native Image does not cross-compile. So on Windows, you can only build a Windows executable, on macOS, only a macOS one, and so on. Now at least on Windows and macOS, we can build a Linux executable by running Native Image in a Linux container.
The starting point is the platform-independent Java bytecode, generated at the build time (see above).
The starting point is the platform-specific, native executable, generated at buildtime (see above). The virtual machine is SubstrateVM.
The JVM saves the internal representation for classes, fields, methods and variables to a file. The JVM then loads this file next time it starts. That saves some time during startup (but no memory).
This is disabled by default. Here’s how to enable it with OpenJDK.
The OpenJDK Project CraC and OpenJ9 CRIU save & load an application snapshot at runtime. That includes the heap, but also JIT metadata. That snapshot is then loaded upon the next application start. That saves some time during startup (but no memory).
The application can control when during its lifetime the snapshot is taken.
This is superior to GraalVM’s heap snapshotting, as it contains the result of all static initializers and the object instances created at runtime (which aren’t part of GraalVM’s heap snapshotting at all).
Amazon’s serverless solution AWS Lambda has a feature called SnapStart that starts serverless functions up to ten times faster. It uses CRaC under the hood.
Please see my InfoQ news item for details on CRaC and an interview with Simon Ritter from Azul, the driving force behind CRaC.
The OpenJDK Project Leyden plans optimizations. We have to wait and see.
The GraalVM Community Edition ships with the serial GC and the epsilon GC. For longer running applications, large heap sizes, or with multiple CPUs, the serial GC is worse than the garbage collectors in JIT Java.
Oracle GraalVM for Java 17 & 21 ships with G1 which is the default garbage collector of most OpenJDK distributions. However, G1 only works on Linux.
Please see my InfoQ news item for the differences between the two GraalVM distributions and an short interview with Alina Yurenko from the GraalVM team.
There are examples where the Enterprise Edition of GraalVM reaches the peak performance of JIT Java (with G1 and PGO). But in many cases, and especially with the open-source version of GraalVM, peak performance of native executables is somewhat worse than JIT Java.
This is bad but getting better.
It also doesn’t impact developers much most of the time. Why? Because the best practice is to develop locally against JIT Java, just like developers normally do. The CI pipeline should then build the native executables. Developers should build native executables locally in two cases:
Developers are used to comfortably debugging Java applications from their IDE. For GraalVM, that only works under Linux now. So macOS and Windows users first have to build their application in a Linux container and then run it one, too. And that’s true for every change. This is impracticable for most circumstances.
Among the IDEs, IntelliJ added experimental debugging support in July 2022.
Observability is worse for native executables, as many solutions rely on Java agents and/or dynamically instrumenting Java code at runtime. Both don’t work in native executables.
Having said that, there is limited JFR support in GraalVM and experimental JMX support. And frameworks like Spring 6/Spring Boot 3 and Quarkus embedded observability into their frameworks so that observability also works in native executables.
This page wouldn’t have been possible without the help from a lot of folks. Thank you!
Quarkus Team, Red Hat
OpenJ9 Team, Red Hat
GraalVM Team, Oracle
Azul