Learn Quarkus - Native Image & Startup Optimization
Episode 16 of 24

Learn Quarkus - Native Image & Startup Optimization

This episode covers native images in Quarkus: compilation with GraalVM or Mandrel, build-time augmentation and AOT compilation, startup time and memory footprint optimization, as well as debugging native images and compatibility tradeoffs.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

In episode 1 you heard Quarkus' promise: tens of milliseconds of startup and a small memory footprint. That promise is fulfilled through native images — ahead-of-time compilation that turns a Java application into a self-contained executable without a JVM.

Episode 16 covers native images thoroughly: compilation with GraalVM or Mandrel, build-time augmentation and AOT compilation, startup and memory optimization, as well as debugging native images and the compatibility tradeoffs you need to understand.

Quarkus Native Images with GraalVM or Mandrel

GraalVM and Mandrel

GraalVM is a JDK with an ahead-of-time compiler. Mandrel is a GraalVM distribution optimized for Quarkus by Red Hat. Both can compile an application into a native executable.

Building a Native Image

Make sure GraalVM is installed, or use a container build:

Building a native image
./mvnw package -Pnative
./mvnw package -Pnative -Dquarkus.native.container-build=true

The flag -Dquarkus.native.container-build=true runs the compilation inside a container, so you don't need to install GraalVM locally — just have Docker running.

Running the Build Output

After the build, run the executable directly:

Running the native executable
./target/belajar-quarkus-1.0.0-SNAPSHOT-runner

There's no java -jar — the -runner file is a self-contained executable. The command curl http://localhost:8080/hello shows the application responding almost instantly.

Build-Time Augmentation and AOT Compilation

Why It Can Be So Fast

The key is the combination of build-time augmentation (episode 2) and AOT compilation. Quarkus processes metadata in the build phase, then GraalVM compiles the application with static context: all used classes are already known, reflection is explicitly allowed, and unused code is discarded.

Build-Time Processing

Configuration that involves build-time values (like quarkus.native.*) is processed at compilation time, not runtime. That's why some properties can't be changed via environment variables — they're already frozen into the executable.

Optimizing Startup Time, Memory Footprint, and Size

Startup Time

A typical comparison:

Startup comparison
JVM mode       : 0.7-1.2 seconds,  memory 150-300MB
Native image   : 0.02-0.08 seconds, memory 50-80MB

Native images are perfect for serverless and Kubernetes autoscaling, where pods are created and destroyed frequently.

Reducing Executable Size

A few configuration options:

Native build optimization
quarkus.native.remove-unused-symbols=true
quarkus.native.enable-fallback-images=false
quarkus.native.strip-debug-symbols=true
quarkus.native.gc=epsilon
  • remove-unused-symbols: discards unused symbols.
  • gc=epsilon: a garbage collector without deallocation — suited for short workloads with minimal latency.

Profiling Startup

Measure and verify the optimization:

Measuring startup time
time ./target/belajar-quarkus-1.0.0-SNAPSHOT-runner &
curl -s http://localhost:8080/q/health

time measures execution duration. Compare JVM vs native for concrete data before setting performance targets.

Debugging Native Images and Compatibility Tradeoffs

Tradeoffs to Understand

Native images don't come for free:

  • Reflection must be registered: dynamic reflection usage must be declared via @RegisterForReflection or configuration.
  • Dynamic proxies are limited: JDK proxies need configuration.
  • Longer build times: native compilation can take several minutes.
  • Some libraries are incompatible: libraries that heavily rely on dynamic reflection can be problematic.

Registering Reflection

JavaRegistering a class for reflection
import io.quarkus.runtime.annotations.RegisterForReflection;
 
@RegisterForReflection
public class DtoPesan {
    public String isi;
    public long timestamp;
}

@RegisterForReflection tells GraalVM to keep reflection metadata for DtoPesan in the native image. Always audit code that uses reflection before building native.

Debugging Native

JVM mode remains available for debugging; use native in production. For native-specific issues, run with detailed reports:

Native runtime logging
quarkus.native.enable-reports=true

quarkus.native.enable-reports=true produces detailed reports about the features GraalVM uses — useful when an error only appears in native.

Wrap-Up

Episode 16 gives you Quarkus' ultimate weapon: understanding native images with GraalVM and Mandrel, how build-time augmentation and AOT work together, startup, memory, and size optimization strategies, as well as the tradeoffs and debugging approaches you need to know before deploying native in production.

Key takeaways:

  • Native images produce a self-contained executable without a JVM.
  • -Dquarkus.native.container-build=true builds native without a local GraalVM.
  • Native startup can be 10-50 times faster than JVM mode.
  • Dynamic reflection must be registered with @RegisterForReflection.
  • Native builds take longer; dynamic-reflection-based libraries can be problematic.
  • JVM mode remains the choice for development and debugging.
  • quarkus.native.enable-reports=true helps with native-specific debugging.

In episode 17 we'll cover testing and quality assurance — unit testing with JUnit 5 and the Quarkus Test Framework, integration testing with @QuarkusTest, mocking and REST client testing, as well as Testcontainers and end-to-end testing.

Learn Quarkus - Native Image & Startup Optimization | Learn Quarkus