Back to Blog
graalvmnative-imagespring-bootjavaperformancekubernetes

GraalVM Native Image with Spring Boot 3: Fast Startup, Low Memory (2026)

GraalVM Native Image compiles Spring Boot apps to standalone binaries that start in milliseconds and use a fraction of the heap. Here's what works, what doesn't, and whether it's worth it.

J

JOptimize Team

May 29, 2026· 10 min read

A standard Spring Boot application starts in 3-8 seconds. A GraalVM native image of the same application starts in 50-200 milliseconds. The heap usage drops from 300-500MB to 50-100MB. For serverless functions, Kubernetes scale-up, and CLI tools, this difference matters enormously.

But native compilation is not a free lunch. It comes with real trade-offs that affect how you write code, how you build, and what libraries you can use. This guide covers the full picture.


Why Native Images Are Fast

A normal Java application runs on the JVM, which interprets bytecode and progressively JIT-compiles hot methods at runtime. This warmup period is why Spring Boot takes several seconds to start — the JVM is initializing thousands of classes, running reflection-heavy framework code, and waiting for JIT compilation to kick in.

GraalVM's Ahead-of-Time (AOT) compilation eliminates this entirely. It analyzes your entire application at build time, compiles everything to native machine code, performs dead code elimination, and produces a single binary. There's no JVM to start, no bytecode to interpret, no JIT warmup. The binary runs directly on the OS.

The trade-off is the build itself. A native image build for a medium Spring Boot application takes 3-10 minutes and requires significant memory (4-8GB). You also lose JIT optimization at runtime — the native image runs at peak speed from the first request, but that peak may be slightly lower than a warmed-up JVM for CPU-intensive workloads.


Spring Boot 3 + AOT: The Foundation

Spring Boot 3 introduced native image support as a first-class feature. It works through Spring AOT (Ahead-of-Time) processing, which runs at build time to:

  1. Analyze your Spring application context statically
  2. Generate proxy classes, reflection metadata, and serialization hints that GraalVM needs
  3. Eliminate conditional beans that won't be active at runtime

This means most Spring features — dependency injection, auto-configuration, Spring Data, Spring Security — work in native images without any changes to your code.

<!-- pom.xml --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.0</version> </parent> <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> </plugin>

Building a Native Image

# Option 1: Build native image locally (requires GraalVM JDK installed) sdk install java 21.0.3-graal # Using SDKMAN mvn -Pnative native:compile # Option 2: Build inside Docker (no GraalVM required on host machine) mvn spring-boot:build-image -Pnative # Uses Paketo Buildpacks — handles GraalVM installation automatically # Option 3: Buildpack with custom Docker image docker build -f Dockerfile.native -t myapp-native .
# Dockerfile.native — multi-stage build FROM ghcr.io/graalvm/native-image-community:21 AS builder WORKDIR /app COPY . . RUN ./mvnw -Pnative native:compile -DskipTests # Final image is tiny — just the binary FROM debian:bookworm-slim COPY --from=builder /app/target/myapp /app/myapp EXPOSE 8080 ENTRYPOINT ["/app/myapp"]

The resulting Docker image is often 80-120MB — compared to 300-500MB for a JVM-based image — because there's no JDK bundled.


What Works Out of the Box

With Spring Boot 3 and AOT processing, the following work without any additional configuration:

  • Spring MVC / Spring WebFlux — fully supported
  • Spring Data JPA — works with Hibernate 6.x
  • Spring Security — including OAuth2 and JWT
  • Spring Cache — including Caffeine
  • Spring Actuator — health, metrics, Prometheus
  • Jackson — JSON serialization/deserialization
  • Flyway / Liquibase — database migrations
  • HikariCP — connection pool

For these common components, you truly just add the native plugin and build.


What Requires Extra Work: Reflection Hints

GraalVM AOT compilation is a closed-world assumption: it only includes code that can be reached statically. Reflection, dynamic proxies, and serialization of types unknown at compile time require explicit hints.

The most common case is Jackson serializing a class that Spring AOT doesn't know about:

// Your custom class used in JSON responses @RegisterReflectionForBinding(OrderEvent.class) // Tell GraalVM to include this public class OrderEvent { private Long orderId; private String eventType; private Map<String, Object> payload; // Dynamic map — needs hint } // Or register programmatically @Configuration public class NativeHintsConfig implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { // Register for reflection hints.reflection() .registerType(OrderEvent.class, MemberCategory.values()) .registerType(PaymentResult.class, MemberCategory.values()); // Register resources (property files, SQL, etc.) hints.resources().registerPattern("db/migration/*.sql"); } }

Spring Boot's AOT processing handles most of this automatically. You only need manual hints for libraries that don't yet provide their own AOT contributions, or for your own code that uses reflection heavily.


Performance Comparison: JVM vs Native

Here's a realistic comparison for a typical Spring Boot REST API:

MetricJVM (warmed up)Native Image
Startup time3-6 seconds50-200ms
First request latencyHigh (JIT warmup)Low (already compiled)
Peak throughputSlightly higherSlightly lower
Heap at idle200-400MB40-80MB
Docker image size250-500MB80-130MB
Build time30-60 seconds3-10 minutes

The conclusion from this table is that native images win clearly on startup and memory. JVM wins marginally on peak throughput for CPU-bound workloads that run for hours.

For most microservices — which are I/O-bound and benefit more from fast scale-up than raw CPU throughput — native images are the better fit.


When Native Image Is Worth It

Good fit:

  • Kubernetes autoscaling — pods need to start in < 1 second to handle traffic spikes effectively
  • Serverless / AWS Lambda — cold start is a user-visible problem; native images eliminate it
  • CLI tools built with Spring Shell — nobody wants a 6-second startup for a command-line utility
  • Memory-constrained environments — running 20 microservices on a single machine

Not worth it:

  • Long-running monoliths — the startup time advantage is irrelevant if the app runs for weeks
  • Heavy use of dynamic class loading — frameworks like Groovy scripting, some code generation tools
  • Aggressive CI/CD — 10-minute builds slow down developer iteration significantly
  • Apps using libraries with poor native support — some older libraries still require significant reflection hints

Debugging Native Image Issues

# Run AOT processing to see what Spring generates mvn spring-boot:process-aot # Generates: target/spring-aot/main/sources/ (generated code) # target/spring-aot/main/resources/ (reflect-config.json, etc.) # Build with verbose output to see missing hints mvn -Pnative native:compile -Dagent=true # The -Dagent flag runs the tracing agent to detect missing reflection # Test native image in development (faster iteration) mvn -Pnative native:test

The most common error when building a native image is ClassNotFoundException or NullPointerException in code that uses reflection. The fix is always a reflection hint registration for the affected class.


Common Mistakes to Avoid

  • Testing only on JVM — some bugs only appear in native images; run native:test as part of CI
  • Assuming all libraries support native — check GraalVM reachability metadata before choosing a dependency
  • Using GraalVM CE for production — GraalVM CE (Community Edition) is good for development; GraalVM Oracle has better performance for production native images
  • Not setting --initialize-at-build-time correctly — some frameworks initialize static state at build time; incorrect initialization breaks the binary

Summary

GraalVM native images with Spring Boot 3 are production-ready for most use cases. The startup time and memory improvements are dramatic and real. The trade-offs — longer builds, occasional reflection hints, slightly lower peak throughput — are manageable for most teams. If you're running microservices on Kubernetes with autoscaling, or any serverless workload, native compilation is worth evaluating seriously in 2026.


Analyze Your App Before Going Native

GraalVM native images amplify existing performance issues. JOptimize helps you fix N+1 queries, missing indexes, and inefficient fetching before you optimize the binary.

Fast startup means nothing if the queries are slow.

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.