Why Your Java App Gets OOMKilled in Kubernetes and How to Fix It

This article explains why Java applications running in Kubernetes containers are often terminated with OOMKilled (exit code 137), analyzes the underlying JVM memory‑limit mismatches, and provides practical solutions using cgroup‑aware JVM flags and memory‑tuning techniques.

Open Source Linux
Open Source Linux
Open Source Linux
Why Your Java App Gets OOMKilled in Kubernetes and How to Fix It

In everyday work we often deploy applications with Kubernetes, but issues like the JVM heap being smaller than the Docker container memory and still being OOMKilled can occur. This article introduces the meaning of Kubernetes OOMKilled exit code 137.

Exit Code 137

Indicates the container received a SIGKILL signal (kill -9), which can be triggered by the user or Docker daemon.

Common when the pod's memory limit is too low, causing OOMKilled; the state shows "OOMKilled": true and OOM logs appear in dmesg.

Root Cause Analysis

Problems often arise with JDK 8u131 or later when running JVM inside containers: the JVM defaults to using the host node's memory for native VM space (heap, direct memory, stack) instead of the container's limits.

Example command output shows a contradiction:

docker run -m 100MB openjdk:8u121 java -XshowSettings:vm -version
VM settings:
    Max. Heap Size (Estimated): 444.50M
    Ergonomics Machine Class: server
    Using VM: OpenJDK 64-Bit Server VM

Although the container memory is limited to 100 MB, the JVM reports a maximum heap of 444 MB, which can cause the node to kill the JVM.

Solution

JVM Detects cgroup Limits

Enable the flags -XX:+UnlockExperimentalVMOptions and -XX:+UseCGroupMemoryLimitForHeap so the JVM automatically senses Docker cgroup limits and adjusts the heap size dynamically.

Note: If -Xmx is also set, it overrides the -XX:+UseCGroupMemoryLimitForHeap flag.

Summary:

The flag -XX:+UseCGroupMemoryLimitForHeap lets the JVM detect the container's maximum heap.

The -Xmx flag sets a fixed maximum heap size.

Besides the heap, non‑heap memory also consumes container memory.

Trying Container‑Aware Mechanism with JDK 9

docker run -m 100MB openjdk:8u131 java \
  -XX:+UnlockExperimentalVMOptions \
  -XX:+UseCGroupMemoryLimitForHeap \
  -XshowSettings:vm -version

Result shows heap size 44.5 M.

docker run -m 1GB openjdk:8u131 java \
  -XX:+UnlockExperimentalVMOptions \
  -XX:+UseCGroupMemoryLimitForHeap \
  -XshowSettings:vm -version

Result shows heap size 228 M.

docker run -m 1GB openjdk:8u131 java \
  -XX:+UnlockExperimentalVMOptions \
  -XX:+UseCGroupMemoryLimitForHeap \
  -XX:MaxRAMFraction=1 -XshowSettings:vm -version

Result shows heap size 910.5 M, demonstrating that -XX:MaxRAMFraction=1 can allocate almost all available memory to the heap.

Analysis of Off‑Heap Memory

Beyond the heap, Java uses off‑heap (direct) memory, typically via Unsafe or DirectByteBuffer. This off‑heap space can become a hidden source of memory consumption.

JVM Parameter MaxDirectMemorySize

The flag -XX:MaxDirectMemorySize limits the size of NIO direct‑buffer allocations. If not specified, its default is the same as -Xmx (or the runtime max memory). -XX:MaxDirectMemorySize When the flag is omitted, the JVM sets the limit to Runtime.getRuntime().maxMemory().

if (s == null || s.isEmpty() || s.equals("-1")) {
    directMemory = Runtime.getRuntime().maxMemory();
} else {
    long l = Long.parseLong(s);
    if (l > -1) directMemory = l;
}

The maxDirectMemory() method then returns this value.

Conclusion

If MaxDirectMemorySize is not explicitly configured, the usable NIO direct memory equals -Xmx minus a survivor space, so the total heap + direct memory can approach double the configured -Xmx value.

Other Ways to Retrieve maxDirectMemory

Use BufferPoolMXBean or java.nio.BufferPool via SharedSecrets:

public BufferPoolMXBean getDirectBufferPoolMBean() {
    return ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class)
        .stream()
        .filter(e -> e.getName().equals("direct"))
        .findFirst()
        .orElseThrow();
}

Memory Analysis Issues

Using -XX:+DisableExplicitGC disables explicit System.gc() calls, which can prevent reclamation of old‑generation direct buffers, potentially leading to out‑of‑memory situations.

Memory diagram
Memory diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaJVMDockerKubernetescgroupOOMKilled
Open Source Linux
Written by

Open Source Linux

Focused on sharing Linux/Unix content, covering fundamentals, system development, network programming, automation/operations, cloud computing, and related professional knowledge.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.