Navigate back to the homepage

Speed Up Your Java App by Compiling into AOT Standalone Binaries

Pewrie Bontal
April 1st, 2024 · 4 min read

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:

Eww JAVA SOO BLOATED

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 JRE

Running with Native:

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.

  1. 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 }
  1. Compile it and build a native executable from the Java class:
1javac HelloWorld.java
2native-image HelloWorld

It will create a native executable, helloworld, in the current working directory.

  1. Run the application:
1./helloworld

You can time it to see the resources used:

1time -f 'Elapsed Time: %e s Max RSS: %M KB' ./helloworld
2
3# Yo!, What's up?
4
5# 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 // ...
3
4 // Apply GraalVM Native Image plugin
5 id 'org.graalvm.buildtools.native' version '0.10.1'
6}

Kotlin:

1plugins {
2 // ...
3
4 // Apply GraalVM Native Image plugin
5 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 application

  • nativeRun, which executes the generated native executable

  • nativeTestCompile, which will build a native image with tests found in the test source set

  • nativeTest, 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:

More articles from Pewrie Bontal

Beginner guide to Authenticating SSH with YubiKey

Beginner guide to Authenticating SSH with YubiKey

March 19th, 2024 · 4 min read

Casually dropping some Unix aesthetic

r/unixporn

November 17th, 2023 · 1 min read
© 2021–2024 Pewrie Bontal
Link to $https://bontal.netLink to $https://twitter.com/pewriebontalLink to $https://github.com/pewriebontalLink to $https://instagram.com/pewriebontalLink to $https://linkedin.com/in/pewriebontal/