Dev.to · 8 min read

MaxRAMPercentage vs Xmx: Sizing Java Heap in Containers

MaxRAMPercentage vs Xmx: Sizing Java Heap in Containers

Your Java service runs in a Kubernetes pod with a memory limit, and you have to decide how big the heap may grow. There are two ways to say it: a fixed -Xmx, or a percentage with -XX:MaxRAMPercentage. This guide explains how MaxRAMPercentage vs Xmx behaves inside a container, which one to pick, and how to choose the number so the pod is not OOMKilled. It is written for engineers who deploy JVM services on Kubernetes and have to defend the setting in a review. “Shipping container stacks – Port of Rotterdam – 2014 – 2” by AgainErick, CC BY-SA 4.0 What the JVM does when you set nothing Since container support is on by default, the JVM reads the cgroup memory limit instead of the node’s RAM. The java command reference says the default for -XX:+UseContainerSupport is true, and that the VM uses the detected memory and processor count to allocate resources. The same reference lists these heap defaults, all measured against the memory the JVM considers available (capped by -XX:MaxRAM, which defaults to the available memory or 128 GB, whichever is lower): -XX:MaxRAMPercentage: 25 percent. -XX:InitialRAMPercentage: 1.5625 percent. -XX:MinRAMPercentage: 50 percent, applied to small heaps of roughly 125 MB. Microsoft’s guide to containerizing Java applications turns that into a table of default maximum heap by available memory: up to 256 MB gives 50 percent, 256 MB to 512 MB gives about 127 MB, and above 512 MB gives 25 percent. The practical result is that a 2 GiB container with no flags gets a heap of only about 512 MiB. That is safe but wasteful, which is why most teams change it. Watch the naming trap: despite its name, -XX:MinRAMPercentage does not set a minimum heap. As the Microsoft guide points out, it sets the maximum percentage for systems with up to 256 MB of RAM. To set a starting heap, use -Xms or -XX:InitialRAMPercentage. MaxRAMPercentage vs Xmx: how they differ -Xmx is an absolute size and is equivalent to -XX:MaxHeapSize according to the same reference. -XX:MaxRAMPercentage is relative: the JVM multiplies the percentage by the memory it detected at startup. Everything else follows from that. Question -Xmx -XX:MaxRAMPercentage Value depends on the pod limit No, you must keep it in sync by hand Yes, it follows the limit automatically Same manifest for several sizes Needs per-size config Works unchanged Predictable exact heap Yes Only after you do the arithmetic Risk if the limit is lowered later Heap may now exceed the limit, so the pod is OOMKilled Heap shrinks with the limit If both are set, the explicit -Xmx is what ends up as the maximum heap. Do not rely on memory for this; check it on your JDK, as shown later. Why the heap cannot equal the container limit Kubernetes enforces memory limits reactively: the resource management documentation says the kernel enforces them through cgroups, and a container that goes over can be OOM-killed. The JVM uses much more than the heap: metaspace (unlimited by default, per the java reference for -XX:MaxMetaspaceSize), thread stacks, the code cache, direct buffers and GC structures. The Microsoft guide states it plainly: do not set the maximum heap equal to the container memory, because that can cause container OOM errors and crashes. It suggests 75 percent of container memory for the heap as a starting point. To see where the rest goes in your own service, use the memory-pool method from Java OOMKilled in Kubernetes: Find the Memory Pool with NMT. For a quick estimate, the JVM Container Memory Calculator adds heap, metaspace and direct memory for you. Choosing the percentage: a worked example The numbers below are hypothetical and computed from the limit alone. The “left for non-heap” column is limit minus heap. Memory limit 25% (default) 50% 75% Left at 75% 512Mi 128Mi 256Mi 384Mi 128Mi 1024Mi 256Mi 512Mi 768Mi 256Mi 2048Mi 512Mi 1024Mi 1536Mi 512Mi 4096Mi 1024Mi 2048Mi 3072Mi 1024Mi A percentage hides an important fact: non-heap memory does not grow in proportion to the limit. A service needing roughly the same metaspace, stacks and code cache will have a much larger share of a 512Mi pod taken up by them than of a 4096Mi pod. So 75 percent can be fine at 4096Mi and too high at 512Mi. A better way to choose is to work backwards from a measurement: Measure non-heap memory under load (NMT, see the link above). Suppose, hypothetically, it is 280Mi. Add a safety margin for spikes, for example 100Mi. Non-heap budget: 380Mi. Subtract from the limit: with a 1024Mi limit, the heap may use 1024 – 380 = 644Mi. Divide: 644 / 1024 = 62.9 percent. Round down to 62. For a 512Mi limit with a measured 200Mi of non-heap plus a 64Mi margin, the result is (512 – 264) / 512 = 48.4 percent, so 48. Note that this is below the 50 percent the JVM would already use for tiny heaps, which shows why small pods need measuring rather than a blanket 75. The Kubernetes QoS Class Checker is useful at this point to confirm how your requests and limits translate: Mi and M are different units, and a limit written as 1G is 1,000,000,000 bytes, not 1024Mi. A ready-to-use Deployment snippet The manifest below sets the limit and makes the heap follow it. Requests equal limits, so the memory is reserved, which is the condition under which Microsoft recommends setting the initial heap equal to the maximum. apiVersion: apps/v1 kind: Deployment metadata: name: orders-api spec: replicas: 2 selector: matchLabels: app: orders-api template: metadata: labels: app: orders-api spec: containers: - name: app image: registry.example.com/orders-api:1.0.0 resources: requests: memory: "1024Mi" limits: memory: "1024Mi" env: - name: JAVA_TOOL_OPTIONS value: "-XX:MaxRAMPercentage=62 -XX:InitialRAMPercentage=62" requests.memory equal to limits.memory reserves the whole amount for the container. JAVA_TOOL_OPTIONS is read by the JVM at startup, so the same image works with any command line. MaxRAMPercentage=62 gives a heap of about 635Mi on a 1024Mi limit; InitialRAMPercentage=62 starts the heap at that size so the JVM does not resize it repeatedly. Verify what the JVM actually chose Never trust the arithmetic alone. Run the same flags under the same limit and read the result. With Docker, you can reproduce a Kubernetes limit locally: docker run --rm --memory=1g eclipse-temurin:21-jre \ java -XX:MaxRAMPercentage=62 -XX:+PrintFlagsFinal -version \ | grep -E 'MaxHeapSize|MaxRAMPercentage|InitialHeapSize' Here --memory=1g sets a 1 GiB cgroup limit, -XX:+PrintFlagsFinal prints every flag after ergonomics, and grep keeps the three that matter. MaxHeapSize is in bytes, so divide by 1048576 to get MiB. To test the precedence rule for your JDK, add -Xmx256m to the same command and compare the output. For a running pod, ask the live process: kubectl exec deploy/orders-api -- jcmd 1 VM.flags kubectl exec deploy/orders-api -- jcmd 1 GC.heap_info VM.flags shows the effective MaxHeapSize, and GC.heap_info shows how much of it is in use. This assumes the JDK image includes jcmd and the Java process is PID 1; a JRE-only or distroless image will not have the tool. When a fixed Xmx is still the better choice You run one tuned service and want an exact, reviewed heap size in the manifest. The same image runs with very different limits and you want the heap to stay constant while only the limit changes, for example in a load test. Your measurements show non-heap memory is stable, so subtracting a fixed amount is more accurate than a percentage. If you use -Xmx, put a comment next to the memory limit in the manifest and change them together. The usual failure is somebody lowering the limit during a cost review and leaving the old -Xmx in place. Key takeaways The default maximum heap is 25 percent of the detected memory for containers above 512 MB, so an untuned 2 GiB pod uses only about 512 MiB of heap. Prefer -XX:MaxRAMPercentage when the same image runs with different limits; prefer -Xmx when you want one exact, reviewed value. Never set the heap equal to the container limit; 75 percent is a reasonable start for larger pods, and small pods need a measured value. Work backwards: limit minus measured non-heap memory minus a margin, divided by the limit. -XX:MinRAMPercentage is not a minimum heap; use -XX:InitialRAMPercentage or -Xms. Confirm the result with -XX:+PrintFlagsFinal or jcmd VM.flags instead of trusting the calculation. Conclusion Use a percentage when you want the heap to follow the pod limit, use -Xmx when you want a fixed number, and in both cases leave room for everything the JVM keeps outside the heap. Measure that room once, derive the percentage from it, and check the effective value on the real container. Related reading Java OOMKilled in Kubernetes: Find the Memory Pool with NMT JVM Container Memory Calculator Kubernetes QoS Class Checker Originally published at OpsHubs, where you can also find free ops calculators. This article was drafted with AI assistance and reviewed before publishing.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Startup & VC News