Chris Collins

Tools · infra

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

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

  1. Enter the container’s memory limit the way Kubernetes writes it (2Gi, 1536Mi; note that 2G means 2·109 bytes) and its CPUs.

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

  3. 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:MaxDirectMemorySize is 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.

Runs entirely in your browser; nothing you type leaves this page.