← All articles

Quarkus

Quarkus heap memory vs normal spring or java heap in Spring Boot

How Quarkus differ from springboot in terms of memory management

4 min read
QuarkusSpring BootJavaHeap

Quarkus vs Spring Boot: What Really Changes in Memory

Why I’m writing this

I’ve spent years building Java microservices with Spring Boot, and more recently with Quarkus. The difference in memory behavior only hit me when I was implementing a cache for one of my applications. I sized the cache the way I always do in Spring Boot, then noticed the Quarkus service behaved differently around heap usage and headroom. That made me dig into what Quarkus does differently under the hood, and this post is what I found.

Spring Boot vs Quarkus in a nutshell

Spring BootQuarkus
PhilosophyRuntime-driven, flexibleBuild-time-driven, optimized
Bean wiringClasspath scanning, reflection and proxies at startupResolved at build time
Runtime modesJVMJVM and native (GraalVM)
StartupSecondsFaster in JVM mode, milliseconds in native
FootprintHeavier baselineLighter baseline

Why was Quarkus created?

Spring Boot was designed in a world of long-running servers on big machines. Containers, Kubernetes, and serverless changed the economics: we now pay for every MB of RAM and every second of startup across dozens of pods.

Quarkus was built for that world. Its idea is to do as much work as possible at build time (dependency injection wiring, config parsing, annotation processing) so the running application carries less baggage. It was designed so the same code can also compile to a native executable.

The real topic: memory areas

A JVM process is more than just the heap:

        Java process memory (RSS)
┌──────────────────────────────────────────┐
│  Heap (Young + Old)   ← your objects     │
│  Metaspace            ← class metadata   │
│  Thread stacks                           │
│  Code cache           ← JIT compiled code│
│  Direct / native buffers                 │
└──────────────────────────────────────────┘

Spring Boot (JVM)

At startup Spring scans the classpath, reads annotations via reflection, and builds proxies (CGLIB, AOP). All of that creates classes and metadata that live in Metaspace and objects that live on the heap, before your business code does anything.

 Startup (runtime work)
 ┌───────────┐   ┌────────────┐   ┌─────────────────────┐
 │ Classpath │ → │ Reflection │ → │ Bean definitions +  │
 │ scanning  │   │ + proxies  │   │ proxies in Heap /   │
 └───────────┘   └────────────┘   │ Metaspace           │
                                  └─────────────────────┘

Quarkus (JVM mode)

Quarkus moves that work to the build step (“augmentation”). The runtime loads fewer classes, uses much less reflection, and starts with a smaller baseline heap and Metaspace. Note that this is still the regular JVM heap. The difference is how much of it is already used before your code runs.

 Build time                          Runtime
 ┌───────────────────────────┐      ┌──────────────────────┐
 │ Scan, wire DI, parse      │  →   │ Pre-computed wiring  │
 │ config, generate bytecode │      │ Small baseline heap  │
 └───────────────────────────┘      └──────────────────────┘

Quarkus (native mode)

This is where the memory model really changes. With GraalVM native image there is no JVM, no JIT, and no Metaspace in the usual sense. Memory is managed by Substrate VM:

 Native executable
 ┌──────────────────────────────────────────┐
 │ Image heap  ← objects initialized at     │
 │               build time, baked in       │
 │ Runtime heap ← objects created at runtime│
 │               (Serial GC by default)     │
 │ Thread stacks                            │
 └──────────────────────────────────────────┘

Heap limits and GC defaults differ from a regular JVM, so don’t assume the same -Xmx behavior.

What this meant for my cache

A cache lives on the heap, so heap behavior directly affects how big it can safely be.

  • Smaller baseline in Quarkus means more heap is available for cached data than in a comparable Spring Boot service.
  • Always bound the cache. With Quarkus’ Caffeine cache, for example:
  quarkus.cache.caffeine."my-cache".maximum-size=10000
  quarkus.cache.caffeine."my-cache".expire-after-write=10M
  • In native mode, set heap limits explicitly (e.g. -Xmx) and test, because the GC is different (Serial by default) and a large cache can change pause behavior.
  • Don’t copy sizing from one framework to the other. Measure the real heap and RSS of your own service under load.

Takeaway

Quarkus doesn’t give you a “different heap” in JVM mode. It gives you a much lighter starting point, and in native mode a different memory model altogether. If you’re sizing caches, setting container limits, or tuning GC, know which mode you’re running in.

Related articles