⚙️ JVM Internals & Memory · Advanced

JVM flags & tuning basics in Java

-Xms, -Xmx, -Xss, choosing a GC, container awareness.

🧩 The mysteryYou set -Xmx2g in a 2 GB container, and Kubernetes keeps killing your pod for using too much memory. The heap never got full. What's eating the rest?

Four knobs

**-Xms sets the initial heap, -Xmx the maximum heap, -Xss** the stack size of each thread, and -XX: flags tune features, like **-XX:+UseZGC** to pick a collector.

java -Xms2g -Xmx2g -Xss1m \
     -XX:+UseZGC -jar app.jar

Container-aware defaults

Modern JVMs are container-aware: they read cgroup memory and CPU limits, not the host's. Without heap flags, the max heap defaults to 25% of the available memory (MaxRAMPercentage).

🔮 Predict it

No flags in a container

A container has a 2 GB memory limit and the JVM gets no heap flags. Roughly what max heap does it choose?

  1. About 512 MB
  2. 2 GB, the whole container
  3. A quarter of the host machine's RAM
Show the answer

About 512 MB: 25% of the container limit, because the JVM reads the container's limit, not the host's. Many services raise the percentage to 50–75%.

⚠️ The trap

-Xmx is not the process limit

-Xmx limits only the heap. Metaspace, thread stacks, the code cache, direct buffers and GC structures all come on top. With -Xmx2g in a 2 GB container there's no headroom, and the container gets killed.

# container limit: 2 GB
java -Xmx2g -jar app.jar  # no headroom!

Sizing in a container

✗ Fixed size
java -Xmx4g -jar app.jar

Wrong as soon as the container limit changes.

✓ Proportional
java -XX:MaxRAMPercentage=75 \
     -jar app.jar

Scales automatically with whatever limit the container gets.

🤔 Think first

Why -Xms = -Xmx?

Many production services set -Xms equal to -Xmx. What does that buy them?

Think about it, then reveal the answer

The heap is sized once at startup: committed up front, so the JVM never grows or shrinks it under load, and any memory shortage shows up at startup, not at peak traffic.

💼 In the real world

Deep recursion in prod

A recursive parser throws StackOverflowError on deeply nested but valid input. **-Xss gives threads bigger stacks as a quick fix; an iterative algorithm is the durable one. And always measure before you tune**: most flags only matter once data shows a problem.

Key takeaways

  1. -Xmx limits only the heap, not total process memory
  2. -Xss sets the per-thread stack size
  3. Containers: prefer -XX:MaxRAMPercentage over fixed sizes
  4. Default max heap = 25% of available memory
🤯 Did you know?

java -XX:+PrintFlagsFinal -version prints every JVM flag with its final value: hundreds of them.

Practice questions

A container has a 2 GB memory limit and the JVM gets no heap flags. Roughly what max heap does it choose?

  1. 2 GB, all of the container
  2. About 512 MB, 25% of the container limit
  3. A quarter of the host machine's RAM
  4. Exactly 64 MB
Check your answer

About 512 MB, 25% of the container limit. Modern JVMs read the container limit and apply MaxRAMPercentage, which defaults to 25%. Many services raise it to 50-75%.

A recursive parser throws StackOverflowError on deeply nested but valid input. Which flag gives threads a bigger stack?

  1. -XX:MaxMetaspaceSize
  2. -Xss
  3. -Xmx
  4. -Xms
Check your answer

-Xss. -Xss sets the stack size for new threads. A cleaner long-term fix is often an iterative algorithm instead of deep recursion.

Flags set the stage. When production misbehaves you need eyes inside the JVM. Next: jcmd, thread dumps, heap dumps and Flight Recorder.