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 RuntimeHints for Spring Boot native support
-
Building native images with Quarkus or Micronaut
Instructions
- 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-* in pom.xml or build.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
- 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
- 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-parent 3.x+ which includes the native profile
-
Register reflection hints via RuntimeHintsRegistrar
-
Run AOT processing with process-aot goal
-
Build with: ./mvnw -Pnative native:compile or ./gradlew nativeCompile
Quarkus and Micronaut: These frameworks are designed native-first and require minimal additional configuration. See the Quarkus & Micronaut Reference.
- 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.
- 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.
- 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
- 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 --from=builder /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 native profile to keep native-specific config separate from standard builds
-
Prefer --no-fallback to ensure a true native build (no JVM fallback)
-
Test with nativeTest to 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.Lookup may 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 @Profile and @ConditionalOnProperty are 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