Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~14 min· topic 2 of 4

Topic 6.2

The JVM Doesn't Know It's in a Container (Unless You Tell It)

In one line

For years, the JVM sized its default heap based on the HOST machine's total memory, even when running inside a container with a much smaller limit — this single fact has caused more production container crashes than almost any other Java-specific Docker issue.

0/4 · 0%

Think of it like this

A person who's always lived in a mansion moving into a studio apartment but still buying furniture sized for the mansion's rooms — it simply won't fit, and something has to give (in the JVM's case, an out-of-memory kill from the OS).

Key ideas

  1. 01

    The core problem, historically: older JVMs computed default heap sizing (-Xmx) based on the TOTAL memory of the host machine, which they could see via the OS, even when running inside a cgroup-limited container with a much smaller memory ceiling — the JVM would happily try to allocate a heap far larger than the container was actually permitted to use.

  2. 02

    Modern JVMs (Java 10+, and Java 17 which most current Spring Boot apps target, is well past this point) are CONTAINER-AWARE by default — they correctly read the container's cgroup memory limit and size the default heap as a percentage of THAT limit, not the host's total memory. This mostly fixed the historical problem, but explicit control is still valuable and still worth understanding.

  3. 03

    -XX:MaxRAMPercentage=75.0 explicitly tells the JVM to use up to 75% of the CONTAINER's memory limit for its heap (leaving headroom for thread stacks, metaspace, and JVM overhead outside the heap) — more predictable and tunable than relying purely on the JVM's own automatic defaults, especially in memory-tight containers.

  4. 04

    Always set a container's memory limit explicitly (docker run -m 512m or Compose's mem_limit: 512m) rather than leaving it unbounded — an unbounded container can consume memory without limit and potentially starve or crash OTHER containers/processes on the same host, which is exactly the isolation guarantee containers are supposed to provide.

  5. 05

    CPU behaves similarly: the JVM detects available CPU count from the container's cgroup CPU limit (not the host's total core count) on modern JVMs, which affects default thread-pool sizing for things like the ForkJoinPool's common pool and Garbage Collector thread counts — under-provisioning CPU limits on a container running CPU-bound work will directly and measurably slow it down.

  6. 06

    Practical rule of thumb for a Spring Boot app: pick a container memory limit with real headroom above your app's typical working set (measure it under realistic load, don't guess), set -XX:MaxRAMPercentage explicitly rather than trusting only automatic defaults, and monitor actual JVM memory metrics (via Micrometer/Actuator) in any environment that matters.

In your stack

  • →

    A concrete, realistic Spring Boot production command: docker run -m 768m myapp:v1 java -XX:MaxRAMPercentage=70.0 -jar app.jar — a 768MB container limit, with the JVM heap capped at 70% of that (leaving roughly 230MB for thread stacks, metaspace, direct buffers, and general JVM/OS overhead outside the Java heap itself).

Code & diagrams

memory-aware-dockerfile.mdmarkdown

The exact CMD line and run flags that make the JVM respect its container's real limit.

# In the Dockerfile:
CMD ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

# When running: ALWAYS set an explicit memory limit for the container
docker run -m 512m --name myapp myapp:v1

# Confirm what the JVM actually detected and decided at startup
docker exec myapp java -XX:+PrintFlagsFinal -version | grep -i maxheapsize

# Watch REAL memory usage live, compared against the container's set limit
docker stats myapp

Explain it without notes

01

Why did older JVMs running inside a memory-limited container sometimes get killed by the operating system's out-of-memory killer, even though -Xmx was never set to an unreasonably high value explicitly?

02

Why does docker run without an explicit -m memory limit defeat the entire purpose of container-based resource isolation?

Practice

01

Run a Spring Boot container with -m 256m (deliberately tight) and -XX:MaxRAMPercentage=75.0, generate some load, and watch docker stats to see whether it approaches or exceeds the limit.

02

Compare java -XX:+PrintFlagsFinal -version | grep -i maxheapsize output when run OUTSIDE any container versus INSIDE a container with a small -m limit — confirm the reported default heap size differs.

Trade-offs

  • ↔

    Setting -XX:MaxRAMPercentage too conservatively (too low) leaves the JVM heap smaller than it could safely be, potentially causing more frequent garbage collection than necessary — too aggressively (too high) risks leaving too little headroom for non-heap JVM memory (metaspace, thread stacks, direct buffers), which is its own path to an OOM kill. 70-75% is a reasonable, widely-used starting point, but real load testing beats a guessed number every time.

Done when you can

  • I always set an explicit container memory limit (-m or mem_limit) rather than leaving it unbounded.

  • I set -XX:MaxRAMPercentage explicitly rather than relying purely on JVM defaults for anything beyond casual local development.

  • I know how to check what heap size the JVM actually decided on inside a running container.