When a Kubernetes pod dies from memory issues, two causes account for most cases: the Linux kernel may have terminated it during an OOM kill, or the application inside hit a java.lang.OutOfMemoryError. On the surface, both look identical – restarts happen, traffic gets dropped, error messages stay unclear. But underneath, their roots differ completely. Mistaking one for the other leads straight into wasted debugging time.
Pumping up -Xmx seems helpful, though it skips the real problem almost every time. Outside the heap, memory quietly collects in overlooked spots: metaspace creeps upward, thread stacks pile deep, off-heap memory grows outside the JVM heap. When the container limit overlooks those zones, consequences hit fast – the system cuts things short without warning. At that point, increasing heap size alone will not help. Stable deployments require container memory limits that reflect the JVM’s full runtime memory usage.
What Does OOMKilled Mean in Kubernetes Pods?
A container gets shut down hard when Kubernetes shows OOMKilled – memory demand pushed it over the edge. One reason might be that the pod broke its personal memory cap. Another possibility? The whole machine was starved for RAM, forcing the system to take action. When things get tight, the OS ends the app without notice. There is no pause, no cleanup time. From the outside, it seems broken. Yet the software inside may have behaved perfectly fine. Faults weren’t rooted in lines of script. Always came down to how much space the machine could use.
Peek inside a pod using kubectl describe pod , then scroll through lines that look something like this:
Last State: TerminatedReason: OOMKilledExit Code: 137
This isn’t coming from Java at all. Voice here is the kernel itself. Exit code 137 means the process received SIGKILL (9) from the Linux kernel.Got shut down fast, no warning. No clean stop. No heap dump left behind.Most times, the problem shows up because the container’s memory cap sits below the JVM’s real-time demands. Sometimes though, the boundary works early on- until a creeping memory drain nudges consumption past the edge.
Here’s what slips past plenty of coders: the JVM runs on more than just heap space. Think metaspace, those hidden thread stacks, buffers living outside the heap, garbage collection costs piling up, even memory eaten by the runtime. Set -Xmx at 512 megabytes if you like- actual footprint inside a container still climbs above it. Often far above.
What Is Java OutOfMemoryError?
A Java OutOfMemoryError is an exception thrown by the JVM itself. The JVM tried to allocate memory and failed but the process is still alive. Depending on the type of error, the JVM may be recoverable, or it may be completely broken.
The most common types are:
java.lang.OutOfMemoryError: Java heap spacejava.lang.OutOfMemoryError: GC overhead limit exceededjava.lang.OutOfMemoryError: Metaspacejava.lang.OutOfMemoryError: unable to create new native thread
Each error corresponds to a different JVM memory region. A heap space error means objects aren’t being collected fast enough. GC overhead indicates that the JVM is spending over 98% of its time on garbage collection and can only reclaim less than 2% of the heap. Metaspace issues are usually caused by class loading problems, especially in applications that dynamically generate classes.
A frequently overlooked point is that Java’s OutOfMemoryError doesn’t immediately cause Kubernetes to terminate the pod. At least according to the logs, the container continues to exist. However, the application may already be in an incorrect state. Some threads may get stuck indefinitely trying to obtain memory that won’t be available. Requests may exceed their limits and remain pending. The JVM process may continue running instead of shutting down properly, but the application may become unresponsive. Sometimes the pod shuts down spontaneously. In other cases, Kubernetes detects a problem through health checks and forces a restart. In both cases, OutOfMemoryError appears in the application’s log files. This is not visible when the kubectl describe pod is run.
OOMKilled vs Java OOM: Key Differences
The table below summarizes the key differences between OOMKilled and a Java OutOfMemoryError:
| Aspect | OOMKilled | Java OOM |
| Who acts | Linux kernel | JVM |
| Trigger | Container exceeds its memory limit (cgroup enforcement) | Heap, metaspace, or managed memory runs out |
| Warning | No stack trace or JVM error | Full stack trace in logs |
| Exit code | 137 | Varies |
| Where to look | kubectl describe pod → Reason: OOMKilled, Exit Code: 137 | Application logs → java.lang.OutOfMemoryError |
| Fix | Increase container memory limit, review -Xmx + overhead, check off-heap usage | Tune heap settings, fix memory leaks |

Fig: Decision tree summarizing the key differences between OOMKilled and Java OOM
Timing plays a key role here too. When OOMKilled hits, it happens right away- cross the line, and the pod disappears instantly. Unlike OOMKilled, a Java OOM may not immediately terminate the process. Sometimes the JVM raises the error quietly in the background, handles it internally, records a message, then limps along for minutes in an unstable condition with no clear signs. That silence makes it tough to link what you see later back to that original crash.
How to Diagnose OOMKilled vs Java OutOfMemoryError
Diagnosing whether a crash was caused by OOMKilled or a Java OutOfMemoryError requires checking the pod termination reason, container memory usage, and JVM-level evidence such as heap dumps.
Step 1: Check Pod Termination Reason
The first thing to always check is the reason the pod was terminated. Don’t make assumptions; verify the information.
kubectl get pod <pod-name> -o jsonpath='{.status.containerStatuses[0].lastState.terminated}'
Step 2: Check Container Memory Usage
Start by spotting “OOMKilled” under “Why” this signals the container got shut down. Instead of assuming, check how much memory it was allowed versus what it actually used. A mismatch here often tells the whole story.
kubectl top pod <pod-name>
Step 3: Capture a Heap Dump for Java OOM
When Java runs out of memory, check the heap. To get a snapshot before it crashes, turn on these options.
-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/tmp/heapdump.hprof
Once the dump file is created, upload it to HeapHero. HeapHero will show you which objects are consuming heap, how many instances have accumulated, and automatically flag leak suspects no tooling setup required.
Demo: Triggering a Java OutOfMemoryError
What if we tried a hands-on example instead? Imagine filling up memory nonstop until it breaks – yes, on purpose. This code mirrors real-world mistakes, like piling session data into a static container without ever removing old items. Think of cached web requests stacking endlessly, each one lingering forever. A single unchecked growth path, running wild inside the app. That quiet buildup leads straight to crashes nobody expects.
Step 1: Create a Simple Memory Leak Program
A fresh User pops into existence each loop, then slips into the static list. Since that list just keeps growing, nothing ever gets cleaned up by the garbage collector – memory quietly piles up over time.
import java.util.*;class User { String name; byte[] data; public User(String name) { this.name = name; this.data = new byte[200 * 1024]; // 200KB payload }}public class LeakDemo { static List<User> users = new ArrayList<>(); public static void main(String[] args) throws Exception { int i = 0; while (true) { users.add(new User("user-" + i++)); if (i % 100 == 0) { System.out.println("Users created: " + i); Thread.sleep(50); } } }}
Step 2: Run the Program with Heap Dump Flags
Start by building and launching the program. If you turn on heap dumping, set the heap to precisely 32 megabytes. Anything below means crashes arrive sooner as memory slips away – no question. Exceeding that amount – say, by boosting RAM or letting the process live longer – delays the failure, yet doesn’t stop it. The leak stays steady regardless of capacity; rather than rush to fill space, it creeps slowly through time.
javac LeakDemo.javajava -Xmx32m -XX:+HeapDumpOnOutOfMemoryError LeakDemo
A few seconds later:
Users created: 100java.lang.OutOfMemoryError: Java heap spaceDumping heap to java_pid22156.hprof ...Heap dump file created [33890257 bytes in 0.012 secs]
Look at this shift on your own; facts stand out plainly. What stands out – app shows “User count hit: 100,” so nearly all formed ahead of storage maxing. One account seems to occupy 200 KB, adding up close to 20 MB across every profile. Then, without delay, memory export finishes within 0.012 seconds – how does that happen? Half full already? That fast? Picture it. A machine at work halts dead when memory nears four gigs. All motion pauses. The freeze holds for thirty seconds – sometimes longer.
Step 3: Analyze the Heap Dump with HeapHero
That .hprof file drops into HeapHero once it’s ready. Depending on size, scanning kicks off fast – sometimes under ten minutes. Soon after, out pops a complete analysis report out of nowhere, a single ArrayList claimed almost all the heap’s room. Right there, it became clear – this list swallowed way more memory than anything else around.
Down the list, instances pop up – each tagged with a count and its share of memory. Your made objects show too, labeled with digits beside them.The biggest chunk of memory, when you look at size, goes to byte[] arrays.
You can clearly see this by looking at the lines: LeakDemo.users is directed to an ArrayList object, and this list contains an array of Objects. This list itself holds an array of Objects, and this array contains references to multiple User objects. As long as this reference chain isn’t broken, the garbage collector can’t clean up these objects, and a significant portion of memory continues to be held by this structure. This points to a byte block. None of them are being released because each piece keeps the next alive.
A sudden collapse, which progressed silently for hours and remained unseen online for more than twelve hours, is now rapidly revealing its source and being reduced to what truly matters. Predictions are being replaced by minutes.
HeapHero Analysis Results

Fig: The HeapHero report provides detailed memory analysis and troubleshooting details regarding the distribution of objects and ArrayLists in memory.

Fig: The HeapHero report you see maps exactly what’s causing our memory to bloat, allowing us to see the true state of the heap.
Common Causes of OOMKilled Errors in Kubernetes
OOMKilled errors typically occur when the total memory used by a container exceeds its configured limit, often due to JVM overhead, memory leaks, or misconfigured container settings.
1. Container Memory Limits Are Too Low
Most of the OOMKilled errors we see come down to the same mistake: just setting -Xmx and calling it a day. As a rule of thumb, you need to set your container memory to your -Xmx value plus an extra 256–512 MB of buffer. And keep in mind, that gap can easily grow—especially if you’ve got a lot of threads or native code eating into that overhead.
2. Application Memory Leaks
One moment everything works. Hours pass, maybe even days, until out of nowhere the app stops. Just like shown earlier: bits piling up silently – static groups holding data, triggers stacking, memory stores expanding endlessly. Not cleaned. Not released. Then it crashes. Here’s when HeapHero steps in, quietly fixing what others miss.
3. JVM Not Configured for Containers
Back when Java was before version 8u191 or hadn’t reached Java 10 yet, the JVM paid no attention to how much memory containers actually had. Instead of checking limits inside the container, it looked straight at the entire RAM of the physical server. Running outdated JVM builds? That behavior might explain what you’re seeing now.
java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
4. Off-Heap Memory Usage
Native memory, direct ByteBuffers, JNI, and frameworks like Netty allocate memory outside the heap. This doesn’t show up in heap dumps, but it absolutely counts against the container memory limit.
Conclusion
This piece looked at two core ways memory problems show up when Java apps run on Kubernetes. One is OOMKilled – the Linux kernel steps in, stops the container dead, because it crossed its memory limit. The other comes from inside the app itself, where the JVM throw an out of memory error while things are still moving. Each situation points to different pressure, yet both shut down operations fast.Spotting the difference shifts how you tackle the problem entirely. When it’s OOMKilled, focus turns to container constraints, JVM settings, along with total memory usage. A Java OOM instead pulls attention toward heap behavior, examining how objects are being allocated over time.When checking heap behavior, HeapHero cuts down time finding issues, particularly those hidden from standard monitoring. Because it pairs pod-wide insights with deep JVM data, spotting real problems inside containers becomes far more straightforward. What looks invisible in ordinary stats often shows up here. With both layers together, confusion drops sharply. Details emerge faster than before. Instead of guessing, patterns take shape clearly across the system.

Share your Thoughts!