skip to content

Startup & Footprint Trade-offs

Native gives you millisecond startup and small memory at the cost of peak throughput, a serial GC by default, long memory-hungry builds and a frozen configuration. Interviewers ask when that trade is right, and short-lived or scale-to-zero workloads are the honest answer.

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

explore

questions

5

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

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

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 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

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