skip to content

Startup, Footprint & JVM Alternatives

Weighing native against the alternatives: the startup and footprint trade-offs, Class Data Sharing, CRaC checkpoint and restore, Spring's CRaC lifecycle integration, and tuning native throughput. Interviewers want a reasoned choice, not a preference.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

24

What do you gain by compiling a Spring Boot app to a GraalVM native image instead of running it on the JVM, in terms of startup and memory?

level: juniorimportance: must knowfreq 70%

answer

  1. AOT machine code, no JVM boot
  2. tens of ms startup vs seconds
  3. low RSS: no JIT, small heap
  4. nativeCompile / bootBuildImage
  5. great for serverless / scale-to-zero

basics

~10 s

A native image starts in tens of milliseconds instead of seconds, and uses much less memory (RSS), because the app is compiled ahead-of-time to a native executable with no JVM warm-up.

solid answer

~40 s

GraalVM native image compiles the app ahead-of-time (AOT) into a standalone machine-code executable. Because there is no JVM to boot, no bytecode to load and verify, and no JIT warm-up, startup drops from seconds to tens of milliseconds. Resident memory (RSS) is also much lower — often well under half — since there is no JIT compiler, less class metadata, and a small default heap. Spring Boot enables this via its AOT engine plus the GraalVM Native Build Tools (`./gradlew nativeCompile` or `bootBuildImage`). This makes native images attractive for serverless/functions, CLIs, and horizontally scaled services where fast scale-to-zero and cold-start matter, and where per-instance memory cost dominates.

code

kotlin · 11 lines
kotlin
// build.gradle.kts — enable GraalVM Native Build Tools
plugins {
    id("org.springframework.boot") version "3.3.0"
    id("org.graalvm.buildtools.native") version "0.10.2" // native image support
}

// Build a native executable (requires a GraalVM JDK):
//   ./gradlew nativeCompile   -> build/native/nativeCompile/<app>
// Or build a native container image (no local GraalVM needed):
//   ./gradlew bootBuildImage
// Spring Boot's AOT engine runs automatically during the native build.

go deeper

for a junior

Must know the headline: fast startup (tens of ms) and low memory, because it's AOT-compiled with no JVM warm-up.

for a middle

Should connect low RSS to 'no JIT + small heap + less metadata' and name the build commands (nativeCompile / bootBuildImage).

for a senior

Should frame it as a trade: gains startup/footprint, pays in peak throughput and build complexity; pick per workload.

for a principal

Should tie startup/footprint to cost models (serverless cold-start SLAs, per-pod RAM * replica count) and fleet economics.

## AOT vs JIT - A normal Spring Boot app runs on the JVM: the JVM boots, loads and verifies bytecode, and initially *interprets* it. A Just-In-Time (JIT) compiler then watches which methods run often ('hot') and compiles them to optimized machine code at runtime — this is 'warm-up'. - **GraalVM native image** does the opposite: at build time it runs Ahead-Of-Time (AOT) compilation, producing a single self-contained native executable that already contains machine code plus a minimal runtime. ## Why startup is fast There is: - no JVM process to start, - no classpath scanning of bytecode, - no class verification, - and no interpretation phase. Spring Boot additionally moves work to build time: the **Spring AOT engine** pre-computes the bean definitions and generates code, so the context 'refresh' does far less at runtime. Result: startup measured in *tens of milliseconds* rather than the ~1–5 seconds a typical JVM Boot app needs. ## Why memory (RSS) is low **RSS** = Resident Set Size, the physical RAM a process actually uses. On the JVM, RAM is consumed by: - the JIT compiler, - compiled-code caches, - extensive class metadata, - and a heap sized generously for throughput. A native image ships no JIT compiler, stores far less metadata, and defaults to a low-footprint garbage collector (Serial GC) with a small heap — so RSS is typically less than half of the equivalent JVM process. ## How you build it Add the GraalVM `org.graalvm.buildtools.native` plugin (Spring Initializr adds it). Then: - `./gradlew nativeCompile` (needs a GraalVM JDK installed) or - `./gradlew bootBuildImage` (builds inside a container, no local GraalVM needed). Spring Boot's AOT processing runs automatically as part of the native build. ## When to use it Best for: - serverless/FaaS where cold-start latency is user-visible; - CLI tools; - and fleets of small horizontally-scaled instances where per-pod RAM cost dominates. **Trade-offs (covered in sibling questions):** lower peak throughput because there is no runtime JIT, a closed-world model that breaks unregistered reflection, and slow, memory-hungry builds.

  • Does a native image use less memory only at startup, or throughout its life?
    Throughout. There is no JIT compiler or compiled-code cache, less class metadata, and a small default heap, so steady-state RSS stays lower than an equivalent JVM process — though a JVM can eventually GC down too.
  • Name one workload where native's startup advantage is essentially wasted.
    A long-running, high-throughput backend that starts once and runs for days. It starts once, so tens-of-ms startup barely matters, while it loses the JIT's peak-throughput advantage — the JVM is usually the better fit there.

context

open as a page

Walk through the Profile-Guided Optimization (PGO) workflow for a Spring Boot native image. What are the steps and what file is produced?

level: middleimportance: must knowfreq 50%

basics

~10 s

Three steps: build an instrumented binary with --pgo-instrument, run it under a realistic workload to produce a profile file (default.iprof), then rebuild passing --pgo=default.iprof so the compiler optimizes the hot paths. Requires Oracle GraalVM.

open as a page

Why can a warmed-up JVM out-perform a GraalVM native image on peak throughput, and what mitigates that gap?

level: middleimportance: must knowfreq 60%

basics

~20 s

The JVM's JIT compiler profiles running code and re-optimizes hot paths, so after warm-up it can run faster. A native image is compiled once ahead-of-time with no runtime profiling, so its peak throughput is often lower. Profile-Guided Optimization (PGO) narrows the gap.

open as a page

How do you handle open resources (DB connections, sockets) across a CRaC checkpoint/restore in a Spring app?

level: seniorimportance: must knowfreq 38%

basics

~10 s

Implement org.crac.Resource: in beforeCheckpoint() close/drain the resource (e.g. evict a connection pool); in afterRestore() re-open it. Register with Core.getGlobalContext().register(this). Spring already does this for its managed Lifecycle beans and many pools support it.

open as a page

Explain the 'closed-world assumption' of native image and how it constrains a Spring application at build and run time.

level: seniorimportance: must knowfreq 55%

basics

~20 s

Native image assumes a closed world: every class, reflective call, resource, and proxy that will ever run must be known at build time. Anything discovered dynamically at runtime (unregistered reflection, runtime classpath additions, dynamic proxies) simply isn't there and fails.

open as a page

What is Class Data Sharing (CDS), and how does it speed up JVM / Spring Boot startup without going native?

level: juniorimportance: should knowfreq 25%

basics

~20 s

CDS stores parsed class metadata in an archive file that the JVM memory-maps at startup instead of re-parsing and re-verifying each class. A Spring Boot app starts roughly twice as fast while staying a normal JVM app.

open as a page

What is CRaC, and what does Spring do to your beans when a checkpoint is taken and when the app is restored?

level: juniorimportance: should knowfreq 20%

basics

~20 s

CRaC (Coordinated Restore at Checkpoint) snapshots a running JVM to disk and restores it later for near-instant startup. On checkpoint Spring calls stop() on its Lifecycle beans (closing pools/connections); on restore it calls start() to reopen them.

open as a page

What is Project CRaC and what problem does it solve for JVM startup?

level: juniorimportance: should knowfreq 45%

basics

~10 s

CRaC (Coordinated Restore at Checkpoint) snapshots a fully warmed-up, running JVM to disk, then restores it later so the app starts in milliseconds instead of seconds — while keeping normal JVM speed.

open as a page

Why does a GraalVM native-image Spring Boot app often have lower peak throughput than the same app on the JVM, and what is the headline lever to close that gap?

level: juniorimportance: should knowfreq 45%

basics

~20 s

A native image is compiled ahead-of-time and has no JIT, so it can't re-optimize hot code from runtime profiles. Peak throughput lags the JVM. Profile-Guided Optimization (PGO) feeds a runtime profile back into the build to close most of the gap.

open as a page

Walk through the dynamic CDS flags: what do -XX:ArchiveClassesAtExit and -XX:SharedArchiveFile each do, and in what order?

level: middleimportance: should knowfreq 22%

basics

~10 s

First a training run with -XX:ArchiveClassesAtExit=app.jsa dumps an archive of the classes loaded, when the JVM exits. Then production runs pass -XX:SharedArchiveFile=app.jsa to memory-map and reuse that archive for faster startup.

open as a page

What are the ways to trigger a CRaC checkpoint with Spring, and how does spring.context.checkpoint=onRefresh differ from an on-demand checkpoint?

level: middleimportance: should knowfreq 22%

basics

~10 s

Two ways: set spring.context.checkpoint=onRefresh to auto-checkpoint during startup (context refreshed but lifecycle beans not yet started), or run jcmd <pid> JDK.checkpoint on a fully running app. Restore both with java -XX:CRaCRestoreFrom=<dir>.

open as a page

How does Spring Boot 3.2+ integrate with CRaC, and how do you trigger an automatic checkpoint?

level: middleimportance: should knowfreq 40%

basics

~10 s

Add the org.crac dependency and run on a CRaC-enabled JDK. Spring drives its Lifecycle beans on checkpoint/restore. You can auto-checkpoint at startup with -Dspring.context.checkpoint.restore=onRefresh, or trigger manually with jcmd JDK.checkpoint.

open as a page

What do the native-image optimization levels (-O0 through -O3, and -Ob) mean, and which would you use for a production throughput-bound Spring service versus local development?

level: middleimportance: should knowfreq 38%

basics

~20 s

-O sets compiler optimization: -O0 none (debugging), -O2 the production default, -O3 more aggressive (Oracle GraalVM), -O1 in between. -Ob optimizes build time, not runtime — use it for fast local dev builds, never for production throughput.

open as a page

What garbage collector does a GraalVM native image use by default, and why does that choice fit the native use case?

level: middleimportance: should knowfreq 40%

basics

~20 s

By default a native image uses the Serial GC — a simple, single-threaded, stop-the-world collector. It has the lowest memory overhead, which matches native's goal of a small footprint, though it can limit throughput and cause longer pauses under heavy load.

open as a page

How does Spring Boot's training-run archive work, and what does -Dspring.context.exit=onRefresh do?

level: seniorimportance: should knowfreq 20%

basics

~10 s

-Dspring.context.exit=onRefresh tells Spring Boot to start the context, finish refreshing (all beans created, classes loaded), then exit the JVM before serving traffic. Run it with -XX:ArchiveClassesAtExit to capture those classes into a CDS archive.

open as a page

Why must resources like connection pools be released before a CRaC checkpoint, and how does Spring coordinate that through the Lifecycle contract?

level: seniorimportance: should knowfreq 18%

basics

~20 s

A memory snapshot can't hold live OS handles; CRaC even fails the checkpoint if unmanaged file descriptors are open. Spring registers the context as a CRaC resource and, in beforeCheckpoint, calls stop() on Lifecycle beans (descending phase) to close pools/sockets, then start() on restore.

open as a page

A native-image Spring service is throughput-bound under sustained load. Why is the default GC a problem, and what would you change?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Native images default to the Serial GC: single-threaded, low footprint, but stop-the-world pauses that hurt throughput on larger heaps under load. For a throughput-bound service on Oracle GraalVM (Linux), switch to G1 with --gc=G1.

open as a page

When would you choose CDS over a GraalVM native image (or plain JVM) for a Spring Boot service, and how do CDS and AOT relate?

level: principalimportance: should knowfreq 16%

basics

~20 s

Choose CDS when you want faster startup and lower memory but must keep full JVM dynamism (reflection, proxies, agents) and cheap, fast builds. Native image starts far faster but has a closed-world model and long builds. CDS and AOT are complementary and stack on the JVM.

open as a page

When would you choose CRaC over GraalVM native image (or vice versa) for a Spring Boot service?

level: principalimportance: should knowfreq 30%

basics

~20 s

Choose CRaC when you need fast startup but must keep full JIT throughput, dynamic behavior (reflection/agents), and easy builds — and deploy on Linux. Choose native image when you need lowest memory and startup and can accept closed-world build constraints and lower peak throughput.

open as a page

You migrated a load-bearing Spring service to native image for startup and footprint, but steady-state throughput regressed versus the JVM. As the tech lead, how do you decide whether and how to close the gap?

level: principalimportance: should knowfreq 30%

basics

~20 s

First confirm throughput actually matters here. If it does, apply the levers in order of payoff: G1 GC, -O3, and PGO — but they require Oracle GraalVM (licensing) and heavier CI. If the cost outweighs the benefit, keep this service on the JVM.

open as a page

As a tech lead, how do the slow, memory-hungry native builds factor into deciding between a JVM jar and a native image for a service?

level: principalimportance: should knowfreq 35%

basics

~20 s

Native builds are slow (minutes) and need lots of RAM per build, so they lengthen CI and cost more to run. You accept that when startup/footprint gains (serverless cold-start, dense scaling) outweigh the build overhead and the peak-throughput loss; otherwise ship a JVM jar.

open as a page

What does -XX:+AutoCreateSharedArchive do, and how does it change the CDS workflow versus the manual two-step approach?

level: seniorimportance: nice to knowfreq 12%

basics

~20 s

-XX:+AutoCreateSharedArchive (JDK 19+), used with -XX:SharedArchiveFile=app.jsa, makes one flag do both jobs: if the archive is missing or stale it creates it at exit, and if it's valid it uses it. No separate training-run flag needed.

open as a page

How do you make a custom resource participate in Spring's CRaC checkpoint/restore, and when would you use org.crac.Resource directly instead of SmartLifecycle?

level: seniorimportance: nice to knowfreq 12%

basics

~20 s

Usually implement SmartLifecycle: close in stop(), reopen in start(), and Spring handles the CRaC callbacks. Use org.crac.Resource directly (register on Core.getGlobalContext()) for non-Spring components or when you need CRaC hooks decoupled from Spring's lifecycle phases.

open as a page

As a principal engineer, what operational and security gotchas would you weigh before adopting Spring CRaC, and how does it compare to GraalVM native image?

level: principalimportance: nice to knowfreq 14%

basics

~20 s

CRaC needs a special JDK and a snapshot that may embed secrets and stale time/RNG/DNS state; every restored instance shares that memory, so reseed randomness and refresh time-sensitive state after restore. Versus native image: CRaC keeps the full JVM and JIT but is less portable and heavier to operate.

open as a page