Speed Up Your Java App: Compile to Standalone Binaries with AOT
Java is known for its portability and write-once-run-anywhere philosophy. However, traditional Java applications rely on the Java Runtime Environment (JRE) being present on the target system. This can add complexity to deployment and slow down startup times. In short, your Java app might be as bloated and slow as your mom.
I mean, look at this:
Ahead-of-Time (AOT) compilation offers a solution. By pre-compiling your Java bytecode into a native executable, you can create standalone binaries that run without the JRE. This brings several advantages:
- Faster Startup Times: No need for the JRE to load, leading to quicker application launches. Your app is fast like a “fuckboi” and starts in milliseconds, delivering peak performance immediately, with no warmup.
- Smaller Footprint: Standalone binaries are typically smaller than their JAR counterparts with all dependencies.
- Simplified Deployment: No need to worry about JRE compatibility on target machines. Plus, native executables can be packaged into lightweight container images for fast and efficient deployment.
- Sustainability: Business bros can brag about sustainability while sipping their ethically-sourced almond milk latte. Native executables contribute to a greener environment by using fewer resources.
Side-by-side comparison for my small university project
Here’s a picture of me running same my university project code, one in JRE and the other in Native.
Running with JRE:
Running with Native:
Wa lao eh!
GraalVM and the native-image Tool
While AOT compilation has been around for a while, GraalVM’s native-image tool has made it a more accessible option for Java developers. native-image analyzes your application’s code and dependencies to determine what needs to be included in the final binary. This static analysis ensures a self-contained executable.
There are some things to keep in mind with AOT compilation:
- Reflection and Dynamic Class Loading: AOT compilation works best with code with well-defined dependencies. Techniques like reflection and dynamic class loading can make it more challenging. (whatever that means).
- Increased Build Time: AOT compilation can take longer than traditional Java compilation. Yes it takes fuck ton of time to compile and i really mean it.
- Limited Platform Support: The generated binary will be specific to the target operating system.
Getting Started with AOT Compilation
Prerequisites
The native-image
tool, available in the bin
directory of your GraalVM installation, depends on the local toolchain (header files for the C library, glibc-devel
, zlib
, gcc
, and/or libstdc++-static
). These dependencies can be installed (if not yet installed) using a package manager on your machine.
Build a Native Executable
The native-image
tool takes Java bytecode as its input. You can build a native executable from a class file, from a JAR file, or from a module (with Java 9 and higher).
From a Class
To build a native executable from a Java class file in the current working directory, use the following command:
1native-image [options] class [imagename] [options]
For example, build a native executable for a HelloWorld application.
- Save this code into file named HelloWorld.java:
1public class HelloWorld {2 public static void main(String[] args) {3 System.out.println("Yo!, What's up?");4 }5 }
- Compile it and build a native executable from the Java class:
1javac HelloWorld.java2native-image HelloWorld
It will create a native executable, helloworld, in the current working directory.
- Run the application:
1./helloworld
You can time it to see the resources used:
1time -f 'Elapsed Time: %e s Max RSS: %M KB' ./helloworld23# Yo!, What's up?45# Elapsed Time: 0.00 s Max RSS: 7620 KB
From a JAR file
To build a native executable from a JAR file in the current working directory, use the following command:
1native-image [options] -jar jarfile [imagename]
The default behavior of native-image
is aligned with the java command which means you can pass the -jar
, -cp
, -m
options to build with Native Image as you would normally do with java. For example, java -jar App.jar someArgument
becomes native-image -jar App.jar
and ./App someArgument
.
Follow this guide to build a native executable from a JAR file.
From a Module
You can also convert a modularized Java application into a native executable.
The command to build a native executable from a Java module is:
1native-image [options] --module <module>[/<mainclass>] [options]
For more information about how to produce a native executable from a modular Java application, see Building a HelloWorld Java Module into a Native Executable.
Gradle Plugin
If you’re using Gradle to build your Java application, you can use the Gradle Native Image Plugin to simplify the process of building native executables. The plugin provides tasks to build and run native images from your Java application.
Add following to plugins section of your project’s build.gradle/build.gradle.kts
:
Groovy:
1plugins {2 // ...34 // Apply GraalVM Native Image plugin5 id 'org.graalvm.buildtools.native' version '0.10.1'6}
Kotlin:
1plugins {2 // ...34 // Apply GraalVM Native Image plugin5 id("org.graalvm.buildtools.native") version "0.10.1"6}
Configuration
This plugin works with the application plugin and will register a number of tasks and extensions for you to configure.
Available tasks The main tasks that you will want to execute are:
nativeCompile
, which will trigger the generation of a native executable of your applicationnativeRun
, which executes the generated native executablenativeTestCompile
, which will build a native image with tests found in the test source setnativeTest
, which will execute tests found in the test source set in native mode
Those tasks are configured with reasonable defaults using the graalvmNative extension binaries container of type NativeImageOptions.
The main executable is configured by the image named main, while the test executable is configured via the image named test.
Native image options
The NativeImageOptions allows you to tweak how the native image is going to be built. The plugin allows configuring the final binary, the tests one, as well as apply options to both.
Groovy:
1graalvmNative {2 binaries {3 main {4 imageName = "my-app"5 mainClass = "net.bontal.Runner"6 buildArgs.add("-O4")7 }8 test {9 buildArgs.add("-O0")10 }11 }12 binaries.all {13 buildArgs.add("--verbose")14 }15}
Kotlin:
1graalvmNative {2 binaries {3 named("main") {4 imageName.set("my-app")5 mainClass.set("net.bontal.Runner")6 buildArgs.add("-O4")7 }8 named("test") {9 buildArgs.add("-O0")10 }11 }12 binaries.all {13 buildArgs.add("--verbose")14 }15}
Now you can run the nativeCompile
task to build the native executable:
1./gradlew nativeCompile
and binary will be generated in the build
directory.
For more information on the Gradle Native Image Plugin, check out the official documentation.
Additional Resources
If you’re interested in trying AOT compilation for your Java application, here are some resources to get you started:
- GraalVM Native Image: The official documentation for native-image, including usage instructions and troubleshooting tips https://www.graalvm.org/latest/reference-manual/native-image/.
- Spring Native: A project from the Spring team that simplifies AOT compilation for Spring Boot applications https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html
- Gradle Native Image Plugin: A Gradle plugin that simplifies the process of building native images https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html