graalvm-native-image
GraalVM Native Image for Java Applications
Expert skill for building high-performance native executables from Java applications using GraalVM Native Image, dramatically reducing startup time and memory consumption.
Overview
GraalVM Native Image compiles Java applications ahead-of-time (AOT) into standalone native executables. These executables start in milliseconds, require significantly less memory than JVM-based deployments, and are ideal for serverless functions, CLI tools, and microservices where fast startup and low resource usage are critical.
This skill provides a structured workflow to migrate JVM applications to native binaries, covering build tool configuration, framework-specific patterns, reflection metadata management, and an iterative approach to resolving native build failures.
When to Use
Use this skill when:
- Converting a JVM-based Java application to a GraalVM native executable
- Optimizing cold start times for serverless or containerized deployments
- Reducing memory footprint (RSS) of Java microservices
- Configuring Maven or Gradle with GraalVM Native Build Tools
- Resolving
ClassNotFoundException,NoSuchMethodException, or missing resource errors in native builds - Generating or editing
reflect-config.json,resource-config.json, or other GraalVM metadata files - Using the GraalVM tracing agent to collect reachability metadata
- Implementing
RuntimeHintsfor Spring Boot native support - Building native images with Quarkus or Micronaut
Instructions
1. Contextual Project Analysis
Before any configuration, analyze the project to determine the build tool, framework, and dependencies:
Detect the build tool:
# Check for Maven
if [ -f "pom.xml" ]; then
echo "Build tool: Maven"
# Check for Maven wrapper
[ -f "mvnw" ] && echo "Maven wrapper available"
fi
# Check for Gradle
if [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
echo "Build tool: Gradle"
[ -f "build.gradle.kts" ] && echo "Kotlin DSL"
[ -f "gradlew" ] && echo "Gradle wrapper available"
fi
Detect the framework by analyzing dependencies:
- Spring Boot: Look for
spring-boot-starter-*inpom.xmlorbuild.gradle - Quarkus: Look for
quarkus-*dependencies - Micronaut: Look for
micronaut-*dependencies - Plain Java: No framework dependencies detected
Check the Java version:
java -version 2>&1
# GraalVM Native Image requires Java 17+ (recommended: Java 21+)
Identify potential native image challenges:
- Reflection-heavy libraries (Jackson, Hibernate, JAXB)
- Dynamic proxy usage (JDK proxies, CGLIB)
- Resource bundles and classpath resources
- JNI or native library dependencies
- Serialization requirements
2. Build Tool Configuration
Configure the appropriate build tool plugin based on the detected environment.
For Maven projects, add a dedicated native profile to keep the standard build clean. See the Maven Native Profile Reference for full configuration.
Key Maven setup:
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.6</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<imageName>${project.artifactId}</imageName>
<buildArgs>
<buildArg>--no-fallback</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
Build with: ./mvnw -Pnative package
For Gradle projects, apply the org.graalvm.buildtools.native plugin. See the Gradle Native Plugin Reference for full configuration.
Key Gradle setup (Kotlin DSL):
plugins {
id("org.graalvm.buildtools.native") version "0.10.6"
}
graalvmNative {
binaries {
named("main") {
imageName.set(project.name)
buildArgs.add("--no-fallback")
}
}
}
Build with: ./gradlew nativeCompile
3. Framework-Specific Configuration
Each framework has its own AOT strategy. Apply the correct configuration based on the detected framework.
Spring Boot (3.x+): Spring Boot has built-in GraalVM support with AOT processing. See the Spring Boot Native Reference for patterns including RuntimeHints, @RegisterReflectionForBinding, and test support.
Key points:
- Use
spring-boot-starter-parent3.x+ which includes the native profile - Register reflection hints via
RuntimeHintsRegistrar - Run AOT processing with
process-aotgoal - Build with:
./mvnw -Pnative native:compileor./gradlew nativeCompile
Quarkus and Micronaut: These frameworks are designed native-first and require minimal additional configuration. See the Quarkus & Micronaut Reference.
4. GraalVM Reachability Metadata
Native Image uses a closed-world assumption — all code paths must be known at build time. Dynamic features like reflection, resources, and proxies require explicit metadata configuration.
Metadata files are placed in META-INF/native-image/<group.id>/<artifact.id>/:
| File | Purpose |
|---|---|
reachability-metadata.json |
Unified metadata (reflection, resources, JNI, proxies, bundles, serialization) |
reflect-config.json |
Legacy: Reflection registration |
resource-config.json |
Legacy: Resource inclusion patterns |
proxy-config.json |
Legacy: Dynamic proxy interfaces |
serialization-config.json |
Legacy: Serialization registration |
jni-config.json |
Legacy: JNI access registration |
See the Reflection & Resource Config Reference for complete format and examples.
5. The Iterative Fix Engine
Native image builds often fail due to missing metadata. Follow this iterative approach:
Step 1 — Execute the native build:
# Maven
./mvnw -Pnative package 2>&1 | tee native-build.log
# Gradle
./gradlew nativeCompile 2>&1 | tee native-build.log
Step 2 — Parse build errors and identify the root cause:
Common error patterns and their fixes:
| Error Pattern | Cause | Fix |
|---|---|---|
ClassNotFoundException: com.example.MyClass |
Missing reflection metadata | Add to reflect-config.json or use @RegisterReflectionForBinding |
NoSuchMethodException |
Method not registered for reflection | Add method to reflection config |
MissingResourceException |
Resource not included in native image | Add to resource-config.json |
Proxy class not found |
Dynamic proxy not registered | Add interface list to proxy-config.json |
UnsupportedFeatureException: Serialization |
Missing serialization metadata | Add to serialization-config.json |
Step 3 — Apply fixes by updating the appropriate metadata file or using framework annotations.
Step 4 — Rebuild and verify. Repeat until the build succeeds.
Step 5 — If manual fixes are insufficient, use the GraalVM tracing agent to collect reachability metadata automatically. See the Tracing Agent Reference.
6. Validation and Benchmarking
Once the native build succeeds:
Verify the executable runs correctly:
# Run the native executable
./target/<app-name>
# For Spring Boot, verify the application context loads
curl http://localhost:8080/actuator/health
Measure startup time:
# Time the startup
time ./target/<app-name>
# For Spring Boot, check the startup log
./target/<app-name> 2>&1 | grep "Started .* in"
Measure memory footprint (RSS):
# On Linux
ps -o rss,vsz,comm -p $(pgrep <app-name>)
# On macOS
ps -o rss,vsz,comm -p $(pgrep <app-name>)
Compare with JVM baseline:
| Metric | JVM | Native | Improvement |
|---|---|---|---|
| Startup time | ~2-5s | ~50-200ms | 10-100x |
| Memory (RSS) | ~200-500MB | ~30-80MB | 3-10x |
| Binary size | JRE + JARs | Single binary | Simplified |
7. Docker Integration
Build minimal container images with native executables:
# Multi-stage build
FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /app
COPY . .
RUN ./mvnw -Pnative package -DskipTests
# Minimal runtime image
FROM debian:bookworm-slim
COPY /app/target/<app-name> /app/<app-name>
EXPOSE 8080
ENTRYPOINT ["/app/<app-name>"]
For Spring Boot applications, use paketobuildpacks/builder-jammy-tiny with Cloud Native Buildpacks:
./mvnw -Pnative spring-boot:build-image
Best Practices
- Start with the tracing agent on complex projects to generate an initial metadata baseline
- Use the
nativeprofile to keep native-specific config separate from standard builds - Prefer
--no-fallbackto ensure a true native build (no JVM fallback) - Test with
nativeTestto run JUnit tests in native mode - Use GraalVM Reachability Metadata Repository for third-party library metadata
- Minimize reflection — prefer constructor injection and compile-time DI where possible
- Include resource patterns explicitly rather than relying on classpath scanning
- Profile before and after — always measure startup and memory improvements
- Use Java 21+ for the best GraalVM compatibility and performance
- Keep GraalVM and Native Build Tools versions aligned
Examples
Example 1: Adding Native Support to a Spring Boot Maven Project
Scenario: You have a Spring Boot 3.x REST API and want to compile it to a native executable.
Step 1 — Add the native profile to pom.xml:
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>process-aot</id>
<goals>
<goal>process-aot</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</profile>
</profiles>
Step 2 — Register reflection hints for DTOs:
@RestController
@RegisterReflectionForBinding({UserDto.class, OrderDto.class})
public class UserController {
@GetMapping("/users/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
Step 3 — Build and run:
./mvnw -Pnative native:compile
./target/myapp
# Started MyApplication in 0.089 seconds
Example 2: Resolving a Reflection Error in Native Build
Scenario: Native build fails with ClassNotFoundException for a Jackson-serialized DTO.
Error output:
com.oracle.svm.core.jdk.UnsupportedFeatureError:
Reflection registration missing for class com.example.dto.PaymentResponse
Fix — Add to src/main/resources/META-INF/native-image/reachability-metadata.json:
{
"reflection": [
{
"type": "com.example.dto.PaymentResponse",
"allDeclaredConstructors": true,
"allDeclaredMethods": true,
"allDeclaredFields": true
}
]
}
Or use the Spring Boot annotation approach:
@RegisterReflectionForBinding(PaymentResponse.class)
@Service
public class PaymentService { /* ... */ }
Example 3: Using the Tracing Agent for a Complex Project
Scenario: A project with many third-party libraries needs comprehensive reachability metadata.
# 1. Build the JAR
./mvnw package -DskipTests
# 2. Run with the tracing agent
java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image \
-jar target/myapp.jar
# 3. Exercise all endpoints
curl http://localhost:8080/api/users
curl -X POST http://localhost:8080/api/orders -H 'Content-Type: application/json' -d '{"item":"test"}'
curl http://localhost:8080/actuator/health
# 4. Stop the application (Ctrl+C), then build native
./mvnw -Pnative native:compile
# 5. Verify
./target/myapp
Constraints and Warnings
Critical Constraints
- GraalVM Native Image requires Java 17+ (Java 21+ recommended for best compatibility)
- Closed-world assumption: All code paths must be known at build time — dynamic class loading, runtime bytecode generation, and
MethodHandles.Lookupmay not work - Build time and memory: Native compilation is resource-intensive — expect 2-10 minutes and 4-8 GB RAM for typical projects
- Not all libraries are compatible: Libraries relying heavily on reflection, dynamic proxies, or CGLIB may require extensive metadata configuration
- AOT profiles are fixed at build time: Spring Boot
@Profileand@ConditionalOnPropertyare evaluated during AOT processing, not at runtime
Common Pitfalls
- Forgetting
--no-fallback: Without this flag, the build may silently produce a JVM fallback image instead of a true native executable - Incomplete tracing agent coverage: The agent only captures code paths exercised during the run — ensure all features are tested
- Version mismatches: Keep GraalVM JDK, Native Build Tools plugin, and framework versions aligned to avoid incompatibilities
- Classpath differences: The classpath at AOT/build time must match runtime — adding/removing JARs after native compilation causes failures
Security Considerations
- Native executables are harder to decompile than JARs, but are not tamper-proof
- Ensure secrets are not embedded in the native image at build time
- Use environment variables or external config for sensitive data
Troubleshooting
| Issue | Solution |
|---|---|
| Build runs out of memory | Increase build memory: -J-Xmx8g in buildArgs |
| Build takes too long | Use build cache, reduce classpath, enable quick build mode for dev |
| Application crashes at runtime | Missing reflection/resource metadata — run tracing agent |
| Spring Boot context fails to load | Check @Conditional beans and profile-dependent config |
| Third-party library not compatible | Check GraalVM Reachability Metadata repo or add manual hints |