GraalVM: Native Images for Java Applications
Back to Blog

GraalVM: Native Images for Java Applications

6 min read
Read in Deutsch

Java and the Startup Problem

Java has many strengths: a mature ecosystem, excellent performance after the warm-up phase, stability in production environments. But in the world of containers and serverless functions, a weakness emerges that long seemed insignificant: startup time.

A typical Spring Boot application takes 5 to 15 seconds before it's ready to process requests. In a classic deployment on an application server, that doesn't matter -- the server runs for months at a time. In a Kubernetes environment with autoscaling, where pods are spun up during load spikes and torn down afterward, 15 seconds of startup is an eternity. For serverless functions that are started per invocation, it's simply unacceptable.

Then there's memory consumption. A JVM reserves a substantial heap at startup, regardless of how much the application actually needs. In times when cloud costs are directly tied to memory usage, this adds up quickly.

GraalVM promises to solve exactly these problems.

What GraalVM Native Image Does

GraalVM is a polyglot runtime environment developed by Oracle, available since 2018 and now at version 20.x. The most exciting feature for Java developers is Native Image: a tool that compiles Java bytecode into a standalone, natively executable binary.

The process differs fundamentally from normal Java execution. Instead of translating bytecode into machine code at runtime through the JIT compiler (Just-in-Time), Native Image performs this work entirely at build time. This is known as AOT compilation -- Ahead-of-Time. The resulting binary contains the compiled application code, all required libraries, and a stripped-down runtime environment called SubstrateVM.

The result is impressive. A simple Java application starts as a Native Image in a few milliseconds instead of seconds. Memory consumption drops drastically because no full JVM needs to be booted up. The binary is a single executable file with no external dependencies -- no installed JDK required.

A Simple Example

To get a feel for how the process works, a minimal Java application is enough. First, you need the GraalVM distribution and the native-image tool, which is installed via the GraalVM Updater:

gu install native-image

A simple application:

public class HelloNative {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        System.out.println("Hello from Native Image!");
        System.out.println("Startup: " + (System.currentTimeMillis() - start) + "ms");
    }
}

Compiling to a Native Image:

javac HelloNative.java
native-image HelloNative

The build process takes -- depending on the machine -- one to two minutes. What happens during this is the so-called closed-world analysis: native-image traces all reachable code paths from the entry point and compiles only what is actually used. Everything else is discarded. This explains the small binary size, but also the limitations.

The resulting binary starts in under 5 milliseconds and uses a fraction of the memory of a JVM execution. For this trivial example, the difference is impressive but not practically relevant. It gets interesting with real applications.

The Closed-World Assumption and the Reflection Problem

The closed-world analysis is simultaneously the greatest strength and the greatest limitation of Native Image. The compiler must know at build time which code is reachable. Anything that is only decided at runtime is a problem.

In Java, there is a prominent mechanism for this: Reflection. The ability to load classes, find methods, and invoke them at runtime is deeply embedded in the Java ecosystem. Serialization frameworks use reflection. Dependency injection containers use reflection. And Spring Boot uses reflection to an extent that makes it the most challenging candidate for Native Image.

When native-image encounters a reflection call during the closed-world analysis, it cannot automatically determine which classes are affected. The call Class.forName("com.example.MyService") is a string -- the value is fixed at build time, but the compiler cannot always recognize that.

The solution: manual configuration. Through JSON files, you tell the Native Image compiler which classes must be reachable via reflection, which dynamic proxies are created, and which resources should be embedded. For a small application, this is manageable. For a Spring Boot project with dozens of libraries that all use reflection, it's a monumental task.

GraalVM provides the Tracing Agent as an aid: you start the application on a normal JVM with the attached agent, which logs all reflection accesses and automatically generates the configuration files. This works quite well in practice, but it only captures the code paths that are actually executed during the test run. Rarely used paths are then missing from the configuration.

The State of Play in Mid-2020: Frameworks Compared

Not all frameworks struggle equally with Native Image. Quarkus and Micronaut were designed from the ground up with GraalVM compatibility in mind. They shift as much work as possible from runtime to build time and avoid reflection wherever they can.

A Quarkus project can be built as a Native Image with a single Maven profile:

mvn package -Pnative

The startup time of a typical Quarkus REST application drops from about 1.5 seconds on the JVM to under 20 milliseconds as a Native Image. Memory consumption falls from over 100 MB to roughly 20 MB. These are impressive numbers that make a real difference, especially for serverless scenarios and CLI tools.

Micronaut takes a similar approach and achieves comparable results. Both frameworks have GraalVM support as a strategic goal and invest accordingly.

With Spring Boot, the situation is different. The framework relies heavily on reflection, dynamic proxies, and classpath scanning by design. Compiling an existing Spring Boot project as a Native Image is currently a battle with lengthy configuration files and runtime errors that are hard to diagnose. The Spring team is working on an experimental project called Spring Native that aims to automate the necessary configuration, but it is still at a very early stage. For production Spring Boot applications, Native Image is not a realistic option today.

Where It Already Pays Off

Despite the limitations, there are scenarios where Native Image is already production-ready:

  • CLI tools and utilities: Short-lived programs benefit the most. No JVM startup, instant execution, small binary.
  • Serverless functions: Where startup time directly affects response time, milliseconds instead of seconds is a competitive advantage.
  • Sidecar containers: Small helper processes in Kubernetes that should consume little memory.
  • New projects with Quarkus or Micronaut: If you're starting from scratch with Native Image as a target, you can make the right architectural decisions from the start.

For existing Spring Boot applications, our honest assessment is: wait. The Spring team has the topic on their radar, and the results from the Spring Native project are promising. But the path to seamless integration will still take time.

Honest Assessment

GraalVM Native Image solves a real problem. The startup times and memory consumption of classic Java applications are a disadvantage in a container world that can't be glossed over. Native Image eliminates this disadvantage in an impressive way.

But it's no silver bullet. Build times are long -- several minutes for a simple application, significantly more for complex projects. The build consumes a lot of memory. Debugging becomes harder because the familiar JVM toolbox is unavailable. And the closed-world assumption forces a different mindset in application development.

For teams currently invested in Spring Boot, it makes sense to follow the development of Spring Native and potentially build new, isolated services with Quarkus or Micronaut to gain experience. For new projects without Spring dependencies, Quarkus with Native Image deserves a serious look.

The direction is right. Java will remain competitive in the cloud-native world -- not despite, but thanks to GraalVM.

Patrick Hütter

Written by

Patrick Hütter

Founder & Software Architect

Software architect, engineer and entrepreneur. Patrick has been building products and platforms for over a decade — from enterprise backends and cloud-native infrastructure to AI-powered applications. As founder of encircle360, he combines deep technical expertise with entrepreneurial vision, driving open source projects that create real impact.