Fast startup and low memory matter for serverless, scale-to-zero and dense container deployments.
A Spring app normally discovers beans, evaluates conditions and creates proxies at runtime with reflection, which costs startup time. Spring's AOT (ahead-of-time) processing moves that work to build time: it runs the application context in a special mode, then generates Java source code for the bean definitions, plus reflection, resource and proxy hints describing what needs to be reachable at runtime.
GraalVM native image compiles the app and the JDK into a standalone executable with a closed-world assumption: everything reachable must be known at build time. With the GraalVM Native Build Tools plugin, ./mvnw -Pnative native:compile (or ./gradlew nativeCompile, or spring-boot:build-image -Pnative for a container) produces a binary that starts in tens of milliseconds and uses far less memory. The costs: long builds, peak throughput often below a warmed-up JIT, and no runtime dynamism.
Restrictions follow from the closed world: the classpath and @Profile/@ConditionalOnProperty choices are fixed at build time, and reflection, resources, proxies and serialisation that Spring can't infer need hints via RuntimeHintsRegistrar and @ImportRuntimeHints, or @RegisterReflectionForBinding. Run tests natively (-PnativeTest) to catch missing hints.
Staying on the JVM, Class Data Sharing (CDS) cuts startup substantially with no code changes: extract the jar (-Djarmode=tools extract), do a training run with -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh, then start with -XX:SharedArchiveFile=app.jsa. Lazy initialisation (spring.main.lazy-initialization=true) helps too, at the cost of later errors.
Example
// Hints for things AOT can't infer, e.g. reflective access to a library class
class ReportHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader cl) {
hints.reflection().registerType(ReportRow.class,
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
hints.resources().registerPattern("templates/report/*.txt");
}
}
@Configuration
@ImportRuntimeHints(ReportHints.class)
class ReportConfig {}
record ReportRow(String name, int total) {}
// Build and run a native executable (needs GraalVM 21+ and the native build tools plugin):
// ./mvnw -Pnative native:compile
// ./target/shop -> Started ShopApplication in 0.06 seconds
// JVM with CDS instead:
// java -Djarmode=tools -jar shop.jar extract --destination app
// java -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh -jar app/shop.jar
// java -XX:SharedArchiveFile=app.jsa -jar app/shop.jarRemember
- AOT processing generates bean definitions and runtime hints at build time
- Native images start in milliseconds but fix the classpath and conditions at build time
- Register hints for reflection and resources Spring can't infer
- CDS speeds up JVM startup with no code changes
Watch out for
- Relying on a runtime profile or property to switch beans in a native image; conditions were evaluated at build time.