JVM memory planner for containers
How a Java process fills a container's memory limit — heap, metaspace, code cache, thread stacks, direct buffers, GC structures and native memory — with the heap the JVM picks by itself on JDK 17, 21, 25 and 27, the flags that fit, OOMKilled versus OutOfMemoryError, and NMT output to fill it in.
Added
Works with
- Java thread dump analyser Databases and streamingtakes the thread count from a thread dump
- Java GC log analyser Databases and streamingtakes heap size, collector, JDK and CPU count from a GC log
- Kubernetes manifest builder and explainer Containers and Kubernetesopens a Deployment with this memory limit and JAVA_TOOL_OPTIONS in the manifest builder
How to use it
"The heap is 1 GiB and the container has 1 GiB" is how a Java service gets OOMKilled. The heap is only the biggest of several parts. This page adds them up against the limit and works out the heap that fits.
How to use it
-
Enter the container’s memory limit the way Kubernetes writes it (
2Gi,1536Mi; note that2Gmeans 2·109 bytes) and its CPUs. -
Pick the JDK (17, 21, 25 or 27), the architecture (thread stacks are 1 MiB on x86-64 Linux and 2 MiB on arm64) and how the heap is set: the JVM’s default,
-XX:MaxRAMPercentage, or-Xmx. -
Adjust the estimates outside the heap: threads and the stack each really uses, metaspace, code cache, direct buffers, other JVM memory and native memory. The defaults are a typical Spring Boot service.
How it adds up shows each part against the limit and what is left. What the JVM does with no
flags reproduces HotSpot’s ergonomics for that container: the max and initial heap
(MaxRAMPercentage 25%, but MinRAMPercentage 50% when the memory is under about 250 MiB, and
never below 124.8 MiB otherwise) and the collector it picks (Serial with fewer than 2 CPUs or less
than 1792 MB before JDK 27, G1 otherwise). I checked these against java -XX:+PrintFlagsFinal
-XX:MaxRAM=… on JDK 17, 21 and 24 — for 64 MiB to 6 GiB the formula gives the same heap as
the JVM.
Flags that fit the limit gives the heap that leaves your safety margin free, as
JAVA_TOOL_OPTIONS, with caps for metaspace and direct memory, -XX:+ExitOnOutOfMemoryError and
a heap dump on OOM, and the Kubernetes resources and env to go with it. Open as a
Deployment in the Kubernetes builder opens the Kubernetes manifest
builder with a Deployment that has the limit, the request and JAVA_TOOL_OPTIONS filled in.
Measuring instead of guessing
Start the JVM with -XX:NativeMemoryTracking=summary (a few percent slower), let it warm up,
then run jcmd <pid> VM.native_memory summary and paste the output under Fill in from Native
Memory Tracking. The page reads every category (any scale=, JDK 8 to 25) and fills in the
heap, metaspace, code cache, threads and their stacks, direct buffers (NMT’s "Other") and the
rest. NMT can’t see memory that native libraries allocate, or glibc’s malloc arenas: compare
NMT’s committed total with the container’s RSS.
The thread count can come from the thread dump analyser, and the heap size, collector and JDK from the GC log analyser; both have a button that opens this page with them.
The numbers
-
GC structures: a share of the heap plus a part per GC thread, from NMT on JDK 21 and 24 at 1 and 4 GiB: G1 about 2% + 4.6 MiB per GC thread, Parallel 3.6% + 3.2 MiB, Serial 0.3%, ZGC about 1% + 6–10 MiB, Shenandoah 3% + 0.7 MiB.
-
Thread stacks count only touched pages; the worst case (every thread using its full
-Xss) is shown when it would not fit. -
Direct memory is limited to the heap size unless
-XX:MaxDirectMemorySizeis set. -
Above about 31 GiB of heap the JVM stops using compressed object pointers.
The defaults table cites where each one comes from: PrintFlagsFinal on 17.0.14, 21.0.7 and
24.0.2, the java tool reference for JDK 25, and JEPs 439, 474 and 490 (ZGC), 491 (virtual
threads and synchronized), 519 (compact object headers), 521 (generational Shenandoah) and 523
(G1 everywhere in JDK 27).
These are estimates for planning. The JVM, the libraries and the load decide the real figure: measure with NMT and watch RSS before trusting a tight limit.
Linking
Share link copies the settings (?limit=2Gi&cpus=2&jdk=21&heap=percent&pct=75). Parameters:
limit, cpus, jdk, arch (x64, aarch64), heap (auto, percent, xmx), pct,
xmx, gc, threads, stack, xss (KiB), meta, code, direct, jvm, native (MiB),
margin (%). Example:
a 1 GiB, 1-CPU container on JDK 21.
Privacy
Everything is worked out in this page. NMT output you paste is not stored or put in links.
Related: Kubernetes manifest builder (its Units & QoS tab explains
Gi against G), thread dump analyser, GC log
analyser.
Feedback is a gift. Something to improve, a feature to add, or a bug? Tell me.
Runs entirely in your browser; nothing you type leaves this page.