Why your JVM uses more memory than -Xmx
The classic incident: the pod has a 512 Mi limit, the heap is capped at -Xmx384m, the heap
never fills up — and the kernel still OOM-kills the container. Nothing leaked. The process
simply needs memory the heap flag does not govern, and the limit did not account for it.
What a Java process actually keeps in memory
The resident set of a JVM is the heap plus a series of native regions, each with its own sizing rules:
- Heap — the only part
-Xmxcontrols. Fully committed at startup when the initial size equals the maximum, which makes the footprint predictable. - Metaspace — class metadata, roughly 5 KiB per loaded class as a rule of thumb. The compressed class space is a region inside this reservation, not an extra on top.
- Code cache — JIT-compiled methods. The JVM reserves 240 MiB by default with tiered compilation; committed memory grows with actual JIT activity.
- Thread stacks — each platform thread reserves the full
-Xss(1 MiB by default on 64-bit). What gets committed is usually only 40–120 KiB per thread, but deep call chains can commit up to the full stack, so conservative sizing multiplies threads by-Xss. - GC structures — card tables, remembered sets, region metadata, relocation data. The cost scales with heap size and differs by collector: roughly 1% of the heap for Serial, 3% for Parallel, 5% for G1 and 6% for ZGC.
- Direct buffers — NIO memory used heavily by Netty and file/socket I/O. If
-XX:MaxDirectMemorySizeis not set, the limit silently defaults to the-Xmxvalue. - Everything else — symbol tables, JIT compiler working memory, JNI allocations, and the C library's own allocator overhead (glibc malloc arenas), which no JVM flag controls at all.
Sizing the container
A workable limit is the sum of the parts above plus explicit headroom — 15% is a sensible default — for the pieces no model can see. Two practical rules follow:
- Never set the container limit equal to
-Xmx. That is the incident from the first paragraph. - Prefer
-XX:+ExitOnOutOfMemoryErrorin containers: a fast, clean restart beats a JVM limping through memory exhaustion.
The calculator runs this model interactively — in both directions: from a desired heap to a container limit, or from a fixed limit to the largest heap that fits.
Estimate, then measure
Estimates get you a safe starting point; production tells you the truth. Enable Native Memory Tracking, capture a summary under peak load and compare it with the model — the NMT guide and the NMT Analyzer cover the workflow.