GraalVM Native Images: Millisecond Startup
A regular Spring Boot application takes 2–10 seconds to start. A GraalVM native image of the same application starts in under 100 milliseconds. For serverless functions, batch jobs, and CLI tools, this is the difference between viable and unusable.
What Is a Native Image?
GraalVM’s native image compiler performs ahead-of-time (AOT) compilation. Instead of shipping a JAR that the JVM interprets at runtime, you ship a standalone executable that:
- Contains only the code your application actually uses
- Has no JVM startup overhead
- Uses much less memory (no JIT compiler, no class metadata)
- Starts in milliseconds
The tradeoff: compile time increases from seconds to minutes. And the JVM’s dynamic features (reflection, dynamic proxies, runtime class loading) must be declared at compile time.
Spring Boot AOT Support
Spring Boot 3+ includes built-in AOT support that handles most of the native image compatibility automatically:
<!-- pom.xml -->
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
Spring Boot’s parent POM includes the native plugin. You just need to activate it.
Setup
Install GraalVM:
# Install via SDKMAN (recommended)
sdk install java 21.0.3-graalce
sdk use java 21.0.3-graalce
# Verify
java -version
# java version "21.0.3" 2024-04-16 LTS
# Java(TM) SE Runtime Environment GraalVM CE 21.0.3+7.1
Or use Docker (no local installation needed):
# Build native image in Docker
docker build -f Dockerfile.native -t order-service:native .
Building the Native Image
# Maven — build native executable
./mvnw native:compile -Pnative
# Output: target/order-service (single executable, ~60–100MB)
Or build a native Docker image using Cloud Native Buildpacks:
./mvnw spring-boot:build-image -Pnative
# Creates: docker.io/library/order-service:0.0.1-SNAPSHOT (based on paketobuildpacks/native-image)
Running the native executable:
./target/order-service
# Started OrderServiceApplication in 0.081 seconds (process running for 0.106)
Spring Boot AOT Processing
When you build natively, Spring runs AOT processing at build time:
Build time:
Source code → AOT processor → Generated Java/Kotlin sources → native-image compiler → executable
Runtime:
executable → application (no Spring context initialization overhead)
The AOT processor:
- Analyzes your bean definitions and configuration
- Generates explicit bean factories (no more reflection)
- Detects required reflection metadata (JPA entities, Jackson DTOs)
- Generates
reflect-config.json,proxy-config.json,resource-config.json
This handles ~90% of native image compatibility automatically. The remaining 10% requires manual configuration.
Common Compatibility Issues
Reflection
GraalVM cannot see runtime reflection unless declared:
// Spring Boot AOT handles @Component, @Service, @Entity automatically.
// But third-party libraries that use reflection may need hints.
@Configuration
public class NativeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// Register a class for reflection
hints.reflection()
.registerType(PaymentGatewayResponse.class,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.DECLARED_FIELDS);
// Register resources
hints.resources()
.registerPattern("payment-schema.json");
// Register JDK proxies
hints.proxies()
.registerJdkProxy(PaymentGateway.class);
// Register serialization
hints.serialization()
.registerType(OrderEvent.class);
}
}
// Register the registrar
@ImportRuntimeHints(NativeHints.class)
@Configuration
public class AppConfig { }
@NativeHint Annotations
Spring Boot provides annotation-based hints:
@Entity
@Table(name = "orders")
public class Order {
// Spring AOT registers JPA entities automatically
}
// For custom serialization:
@JsonSerialize
@Reflective // Spring Boot 3 — marks for reflection registration
public record OrderEvent(UUID orderId, String type, Instant timestamp) {}
Testing Native Compatibility
Run a simulation of native image behavior without building the native image:
@SpringBootTest
@TestPropertySource(properties = "spring.aot.enabled=true")
class NativeCompatibilityTest {
// Tests run in AOT mode — catches most compatibility issues
}
Or run actual native tests:
./mvnw test -PnativeTest
# Builds native test executable and runs it
Application Properties for Native
# application.yml
spring:
aot:
enabled: true # enable AOT processing (automatic in native builds)
jpa:
# Use 'create-drop' in test, 'validate' in prod — same as JVM mode
hibernate:
ddl-auto: validate
# Hibernate needs reflection hints for entity classes
# Spring Boot handles this automatically for @Entity classes
JPA with Native Images
JPA works natively with Spring Boot 3+, but requires Hibernate 6.2+ (included in Spring Boot 3.2+):
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
// Lombok @Getter, @Setter work in native images
// JPA proxies for lazy loading are generated at build time
}
Lazy loading works in native images — Spring Boot generates the necessary Hibernate bytecode enhancement at build time instead of runtime.
Memory Comparison
| Mode | Startup | Heap (idle) | Heap (under load) | Container size |
|---|---|---|---|---|
| JVM | 3–8s | 200–400MB | 500MB–2GB | 200MB JRE + JAR |
| Native | 50–100ms | 30–80MB | 100–400MB | 60–100MB executable |
Native images use significantly less memory at idle — critical for scale-to-zero serverless workloads.
Dockerfile for Native Images
# Dockerfile.native
FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /app
COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline -q
COPY src/ src/
RUN ./mvnw native:compile -Pnative -q
# Minimal runtime image — no JVM needed
FROM debian:bookworm-slim
WORKDIR /app
COPY --from=builder /app/target/order-service .
EXPOSE 8080
ENTRYPOINT ["./order-service"]
Or use distroless for an even smaller image:
FROM gcr.io/distroless/base-debian12
COPY --from=builder /app/target/order-service /order-service
ENTRYPOINT ["/order-service"]
Final image size: ~80MB vs ~250MB for a JVM-based image.
Lambda / Serverless
Native images are ideal for AWS Lambda (cold start is the main pain point with JVM):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency>
@SpringBootApplication
public class OrderFunctionApplication {
public static void main(String[] args) {
SpringApplication.run(OrderFunctionApplication.class, args);
}
@Bean
public Function<OrderRequest, OrderResponse> processOrder(OrderService service) {
return request -> service.process(request);
}
}
./mvnw native:compile -Pnative
# Deploy target/order-function to Lambda (no JVM layer needed)
# Cold start: ~100ms vs 8-15s for JVM
When to Use Native Images
Good candidates:
- CLI tools and batch jobs (millisecond startup, low memory)
- Serverless functions (Lambda, Cloud Run, Azure Functions)
- Microservices with scale-to-zero requirements
- Container workloads with tight memory limits
Not ideal for:
- Long-running services with significant warm-up time (JVM JIT eventually beats native throughput)
- Applications with heavy dynamic class loading or reflection (e.g., Groovy scripts, dynamic plugins)
- Rapid development cycles (3–5 minute native compile vs 10s JVM compile)
For most production services: JVM with virtual threads is the better default. Consider native when cold start or memory matters specifically.
What You’ve Learned
- GraalVM native images start in under 100ms and use half the memory of JVM mode
- Spring Boot 3+ AOT processing handles reflection, proxies, and JPA entities automatically
- Use
RuntimeHintsRegistrarto declare reflection or resources for third-party libraries - Test native compatibility with
@TestPropertySource(properties = "spring.aot.enabled=true") ./mvnw native:compile -Pnativebuilds the executable;spring-boot:build-image -Pnativebuilds a container- Native images are ideal for serverless and CLI tools; JVM is better for long-running, throughput-optimized services
This completes Part 7: Performance. You now have the tools to make Spring Boot applications fast — both response-time optimizations (JPA, caching, async) and deployment optimizations (native images, virtual threads).
Next: Part 8 — Messaging and Event-Driven Architecture starts with Article 42: Introduction to Messaging with Apache Kafka.