skip to content

Heap Sizing

Setting initial and maximum heap, why pinning them equal avoids resize pauses on long-running servers, and how to align the limits with a container's memory cgroup. Interviewers like this one because a heap sized to the container's whole memory limit — leaving no headroom for stacks, class metadata, and off-heap buffers — is a classic production kill.

on this pageshow

questions

5

What do the Java command-line options -Xms and -Xmx set, and what does the HotSpot JVM choose for them when you specify neither?

level: juniorimportance: must knowfreq 55%

answer

  1. -Xms initial and floor, -Xmx ceiling
  2. defaults roughly 1/64 and 1/4 of available memory
  3. container-aware by default since JDK 10
  4. reserved vs committed
  5. -Xmx bounds the heap, not the process

basics

~20 s

-Xms is the initial heap size the JVM commits at startup; -Xmx is the maximum it may grow to. With neither set, HotSpot defaults to roughly 1/64 of available memory for initial and 1/4 for maximum, reading the container limit rather than host RAM when running under one.

solid answer

~50 s

-Xms sets the initial, and effectively minimum, committed heap; -Xmx sets the upper bound. Between them the JVM grows and shrinks the heap as demand changes, committing more operating-system memory when occupancy after collection is high. With neither flag, HotSpot's default ergonomics pick about 1/64 of available memory as the initial size and 1/4 as the maximum - equivalently -XX:InitialRAMPercentage around 1.56 and -XX:MaxRAMPercentage 25. Since JDK 10, container support is on by default, so 'available memory' means the cgroup memory limit when one is set, not the physical RAM of the host. Two things to be clear about. These bound the Java heap only; metaspace, code cache, thread stacks and direct buffers live outside it, so process memory is always larger than -Xmx. And when the live set genuinely does not fit under -Xmx after a full collection, the JVM throws OutOfMemoryError rather than growing further.

code

text · 8 lines
text
# absolute
java -Xms2g -Xmx2g -jar app.jar

# percentage of the container/cgroup limit (scales with the limit)
java -XX:InitialRAMPercentage=60 -XX:MaxRAMPercentage=60 -jar app.jar

# what did the JVM actually pick?
java -XX:+PrintFlagsFinal -version | grep -E 'InitialHeapSize|MaxHeapSize'

go deeper

for a junior

Know which flag is initial and which is maximum, the rough default fractions, and that they cover only the heap.

for a middle

Add reserved versus committed, the free-ratio-driven growth, and the percentage flags for containers.

for a senior

Set them from a measured live set and an explicit non-heap budget rather than accepting ergonomic defaults.

for a principal

Standardise how services express heap size across the fleet - percentage-based in containers, absolute where the instance is fixed - so limits stay coherent when instance sizes change.

## The two bounds The Java heap is the region the garbage collector manages, holding all objects and arrays. Its size is bounded at both ends by command-line options that have kept their non-standard names for decades. -Xms sets the initial heap: the amount the JVM reserves and commits at startup. In practice it also acts as a floor, because HotSpot will not shrink the committed heap below it. -Xmx sets the maximum: the ceiling beyond which the heap will not grow. Both accept suffixes, so -Xms512m and -Xmx4g are typical spellings. Newer equivalents express the same thing as fractions of available memory: -XX:InitialRAMPercentage and -XX:MaxRAMPercentage, which are the better choice in containers because they scale when the limit changes. ## Growth and shrinkage between them When a collection ends and too little of the heap is free, the JVM commits more memory, up to -Xmx. When far too much is free, it may return memory, down to -Xms. HotSpot's classic controls for this are -XX:MinHeapFreeRatio and -XX:MaxHeapFreeRatio (40 and 70 by default in the collectors that use them): keep free space between those percentages after a collection, expanding or shrinking to get there. It is worth separating two words. Reserved memory is address space the JVM claimed up front, typically the whole of -Xmx. Committed memory is what is actually backed by the operating system and counts toward the process footprint. Growing the heap means committing more of the already-reserved range. ## The defaults If you pass neither flag, HotSpot chooses using its ergonomics. On a server-class machine the rule of thumb is initial about 1/64 of available memory and maximum about 1/4, with small-memory adjustments at the low end. The important modern detail is what 'available memory' means: since JDK 10 the JVM is container-aware by default (-XX:+UseContainerSupport), so inside a container with a memory limit it reads the cgroup limit rather than the host's physical RAM. Before that, a JVM in a 512 MB container on a 64 GB host would happily size its heap for 64 GB and be killed by the kernel. A default maximum of 25% is deliberately conservative and is usually wrong for a dedicated service container, where three quarters of the memory you paid for would sit unused. Setting -XX:MaxRAMPercentage explicitly - or a fixed -Xmx - is the normal production practice. ## What these flags do not cover A very common misunderstanding is that -Xmx caps the memory of the Java process. It does not. Outside the heap the process also holds class metadata in metaspace, JIT-compiled code in the code cache, one stack per thread, direct and mapped byte buffers, garbage-collector bookkeeping structures, and whatever native libraries allocate. A process with -Xmx1g routinely uses well over 1.3 GB of resident memory. Any memory budget - a container limit, a capacity plan - has to account for all of it. ## When the ceiling is reached If the application's reachable data cannot fit under -Xmx, the collector will run repeatedly, reclaim little, and eventually the JVM reports OutOfMemoryError for the heap. Raising -Xmx is the right response only when the live set is legitimately that large; otherwise it postpones the same failure. Note also that the JVM refuses to start if -Xms exceeds -Xmx, and that an -Xms larger than the machine or container can supply fails at startup rather than later. ## Practical starting point For a dedicated container, decide the total budget, subtract a realistic non-heap allowance, and set the heap explicitly - either as a fixed -Xmx or as a percentage - rather than relying on a 25% default that was chosen for shared desktop machines.

  • Does -Xmx limit how much memory the Java process uses in total?
    No. It limits only the garbage-collected heap. Metaspace, the JIT code cache, per-thread stacks, direct byte buffers, GC bookkeeping and native library allocations all sit outside it, so resident memory is always meaningfully larger than -Xmx and has to be budgeted separately.
  • Why prefer -XX:MaxRAMPercentage over a fixed -Xmx in a container?
    Because it is expressed relative to the cgroup memory limit, the same image behaves sensibly when the limit is raised or lowered without editing the command line. It also removes the risk of an -Xmx that silently exceeds a reduced container limit, which would let the heap grow into a kernel OOM kill.

saying these in an interview costs you the question

  • Saying -Xmx caps total process memory
  • Assuming a JVM in a container sizes its heap from host RAM on modern JDKs
  • Thinking the JVM commits all of -Xmx at startup regardless of -Xms
  • Believing the default maximum heap is a fixed number of megabytes rather than a fraction of available memory
  • Setting -Xms larger than -Xmx and expecting it to be clamped rather than rejected

context

open as a page

Why is it common practice to set -Xms equal to -Xmx on long-running server JVMs, and what is the argument against doing it?

level: middleimportance: must knowfreq 60%

basics

~20 s

Pinning them equal stops the JVM repeatedly committing and releasing memory, which costs collection work and page faults, and it makes the process footprint predictable so the peak cannot surprise a container limit later. Against it: you pay for memory you may never use, and elastic services lose the ability to give it back.

open as a page

A Java service in a container with a 2 GiB memory limit keeps being killed by the kernel out-of-memory killer, yet it never throws a Java heap OutOfMemoryError. How do you size the JVM so this stops happening?

level: seniorimportance: must knowfreq 60%

basics

~20 s

The container limit covers the whole process, not just the heap. Set the maximum heap to a fraction of the limit - commonly 50-75% depending on thread count and off-heap use - leaving room for metaspace, code cache, stacks, direct buffers and GC structures, then verify the real resident footprint with Native Memory Tracking.

open as a page

Why can raising a HotSpot maximum heap setting from 31 GB to 33 GB leave a Java service with less usable capacity than it had before?

level: middleimportance: should knowfreq 35%

basics

~20 s

Below roughly 32 GB, HotSpot stores object references as 32-bit compressed pointers. Above that threshold it must use full 64-bit references, so every object with references grows and the whole live set expands - typically enough to swallow the extra 2 GB and more.

open as a page

Beyond the Java heap, what else consumes memory in a running JVM process, and how do you put an explicit bound on each part when budgeting a service's total memory footprint?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Metaspace and compressed class space, the JIT code cache, one stack per thread, direct and mapped byte buffers, garbage-collector bookkeeping, and native library allocations. Bound them with -XX:MaxMetaspaceSize, -XX:ReservedCodeCacheSize, -Xss plus bounded thread pools, and -XX:MaxDirectMemorySize; measure the rest with Native Memory Tracking.

open as a page