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.
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
- 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. - 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.
- 03
-XX:MaxRAMPercentage=75.0explicitly 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. - 04
Always set a container's memory limit explicitly (
docker run -m 512mor Compose'smem_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. - 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.
- 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:MaxRAMPercentageexplicitly 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
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 myappExplain it without notes
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?
Why does docker run without an explicit -m memory limit defeat the entire purpose of container-based resource isolation?
Practice
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.
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:MaxRAMPercentagetoo 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.