<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kkkevin</title>
    <description>The latest articles on DEV Community by kkkevin (@kkkevins).</description>
    <link>https://dev.to/kkkevins</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4216369%2F024f4b18-a778-4a0d-acbe-36d24ed93d1e.png</url>
      <title>DEV Community: kkkevin</title>
      <link>https://dev.to/kkkevins</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kkkevins"/>
    <language>en</language>
    <item>
      <title>Java OOMKilled in Kubernetes: Find the Memory Pool with NMT</title>
      <dc:creator>kkkevin</dc:creator>
      <pubDate>Fri, 06 Nov 2026 02:30:04 +0000</pubDate>
      <link>https://dev.to/kkkevins/java-oomkilled-in-kubernetes-find-the-memory-pool-with-nmt-5dpb</link>
      <guid>https://dev.to/kkkevins/java-oomkilled-in-kubernetes-find-the-memory-pool-with-nmt-5dpb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F02jkbe4dzu9jgza1nqpb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F02jkbe4dzu9jgza1nqpb.jpg" alt="A pile of laptop and desktop RAM memory modules on a wooden table" width="799" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;“&lt;a href="https://commons.wikimedia.org/wiki/File:Pile_of_Laptop_%26_Desktop_RAM_Memory_Modules.jpg" rel="noopener noreferrer"&gt;Pile of Laptop &amp;amp; Desktop RAM Memory Modules&lt;/a&gt;” by Wilbysuffolk, &lt;a href="https://creativecommons.org/licenses/by-sa/4.0" rel="noopener noreferrer"&gt;CC BY-SA 4.0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Java pod that restarts with &lt;code&gt;OOMKilled&lt;/code&gt; is a frustrating problem, because the JVM often leaves no stack trace and no &lt;code&gt;OutOfMemoryError&lt;/code&gt;. The kernel simply ends the process. This guide is for engineers who run Java services on Kubernetes and need to answer one question: &lt;strong&gt;which memory pool grew&lt;/strong&gt;? We will use Native Memory Tracking (NMT) and &lt;code&gt;jcmd&lt;/code&gt; to find out, then map the result to a fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Java OOMKilled is not a Java OutOfMemoryError
&lt;/h2&gt;

&lt;p&gt;Two different things get confused. A Java &lt;code&gt;OutOfMemoryError&lt;/code&gt; is thrown by the JVM when it cannot satisfy an allocation inside its own limits, for example when the heap is full. &lt;code&gt;OOMKilled&lt;/code&gt; means the container as a whole used more memory than its Kubernetes limit. The &lt;a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/" rel="noopener noreferrer"&gt;Kubernetes documentation&lt;/a&gt; explains that memory limits are enforced by the kernel with out-of-memory kills, and that this enforcement is reactive: a container can briefly exceed the limit, and it is killed when the kernel detects memory pressure.&lt;/p&gt;

&lt;p&gt;The container’s memory is more than the Java heap. It also includes class metadata, thread stacks, JIT-compiled code, GC data structures, direct buffers and memory from native libraries. A pod can be killed while the heap graph looks healthy. That is why heap dumps alone often do not explain it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirm that it really was an OOM kill
&lt;/h2&gt;

&lt;p&gt;Start by checking the termination reason. The &lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/" rel="noopener noreferrer"&gt;Kubernetes memory tutorial&lt;/a&gt; shows that a killed container reports &lt;code&gt;reason: OOMKilled&lt;/code&gt; and &lt;code&gt;exitCode: 137&lt;/code&gt;, which is 128 + 9 (SIGKILL).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get pod my-app-7d9f -o jsonpath='{.status.containerStatuses[0].lastState.terminated}'
kubectl describe pod my-app-7d9f | grep -A6 "Last State"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;my-app-7d9f&lt;/code&gt; with your pod name. If the reason is something else (for example an error exit from your own code), this guide does not apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do the budget arithmetic first
&lt;/h2&gt;

&lt;p&gt;Before measuring, add up what the JVM is allowed to use. The &lt;a href="https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html" rel="noopener noreferrer"&gt;java command reference&lt;/a&gt; gives these facts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-XX:MaxRAMPercentage&lt;/code&gt; defaults to 25 percent, and the RAM it is a percentage of is limited by container constraints.&lt;/li&gt;
&lt;li&gt;The default maximum code cache is 240 MB (48 MB without tiered compilation).&lt;/li&gt;
&lt;li&gt;The default thread stack size on Linux/x64 is 1024 KB.&lt;/li&gt;
&lt;li&gt;Metaspace is not limited by default (&lt;code&gt;-XX:MaxMetaspaceSize&lt;/code&gt;), and direct buffer size is chosen automatically unless you set &lt;code&gt;-XX:MaxDirectMemorySize&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Take a hypothetical pod with a 1024Mi limit whose JVM runs with &lt;code&gt;-XX:MaxRAMPercentage=75&lt;/code&gt;, so the maximum heap is 768Mi. If the service has 200 threads on Linux/x64, their stacks can reserve up to 200 x 1 MB = 200 MB. Only the stack pages a thread actually touches count toward the container, so real use is usually lower, but in the worst case heap plus stacks is 968 MB. That leaves 56 MB of the 1024Mi limit for metaspace, code cache, GC structures, direct buffers and everything else. A budget this tight can explain a kill without any leak at all.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://opshubs.space/tools/jvm-container-memory-calculator/" rel="noopener noreferrer"&gt;JVM Container Memory Calculator&lt;/a&gt; does this addition for you with your own heap, metaspace, thread and direct memory inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable Native Memory Tracking
&lt;/h2&gt;

&lt;p&gt;NMT is off by default. According to the java command reference, &lt;code&gt;-XX:NativeMemoryTracking&lt;/code&gt; accepts &lt;code&gt;off&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt; and &lt;code&gt;detail&lt;/code&gt;. The &lt;a href="https://docs.oracle.com/en/java/javase/21/troubleshoot/diagnostic-tools.html" rel="noopener noreferrer"&gt;Java troubleshooting guide&lt;/a&gt; states that enabling NMT costs a 5-10 percent performance drop, so turn it on in a test or canary environment first. It cannot be switched on in a running JVM; it needs a restart.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;env:
  - name: JAVA_TOOL_OPTIONS
    value: "-XX:NativeMemoryTracking=summary"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sets the flag through &lt;code&gt;JAVA_TOOL_OPTIONS&lt;/code&gt;, which the JVM reads at startup, so you do not have to edit the image. Use &lt;code&gt;detail&lt;/code&gt; only when &lt;code&gt;summary&lt;/code&gt; points to a category and you need call sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the NMT summary with jcmd
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;jcmd&lt;/code&gt; inside the container. In many images the Java process is PID 1. &lt;code&gt;jcmd&lt;/code&gt; ships with the JDK, so JRE-only or distroless images may not include it; in that case use an image variant that has the JDK tools, or an ephemeral debug container with a JDK that shares the pod’s process namespace. Run &lt;code&gt;jcmd&lt;/code&gt; as the same user as the Java process, or it cannot attach.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl exec my-app-7d9f -- jcmd 1 VM.native_memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output lists the total reserved and committed memory, then one block per category. The categories documented by Oracle include Java Heap, Class, Thread, Code, GC, Compiler, Internal, Symbol and Native Memory Tracking itself. The page notes that the list may change between releases. Focus on &lt;strong&gt;committed&lt;/strong&gt; values, since committed memory is what counts against the container.&lt;/p&gt;

&lt;p&gt;One limit matters here: NMT does not track allocations made by non-JVM native code, such as a JNI library. If the NMT total is far below what the container reports, suspect native libraries rather than the JVM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare against a baseline
&lt;/h3&gt;

&lt;p&gt;A single snapshot rarely shows growth. Take a baseline after warm-up, wait while the problem develops, then diff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl exec my-app-7d9f -- jcmd 1 VM.native_memory baseline
# ...wait until memory has grown...
kubectl exec my-app-7d9f -- jcmd 1 VM.native_memory summary.diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diff prints each category with a plus or minus delta. The category with the largest positive delta is your suspect. If you need allocation sites, restart with &lt;code&gt;-XX:NativeMemoryTracking=detail&lt;/code&gt; and use &lt;code&gt;detail.diff&lt;/code&gt; the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Match the growing pool to a fix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Growing category&lt;/th&gt;
&lt;th&gt;Typical cause&lt;/th&gt;
&lt;th&gt;What to try&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java Heap&lt;/td&gt;
&lt;td&gt;Heap max is too large for the limit, or real object growth&lt;/td&gt;
&lt;td&gt;Lower &lt;code&gt;-XX:MaxRAMPercentage&lt;/code&gt;, or take a heap dump with &lt;code&gt;-XX:+HeapDumpOnOutOfMemoryError&lt;/code&gt; (disabled by default) and &lt;code&gt;-XX:HeapDumpPath&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thread&lt;/td&gt;
&lt;td&gt;Too many threads, each with a stack&lt;/td&gt;
&lt;td&gt;Cap thread pools; reduce &lt;code&gt;-Xss&lt;/code&gt; only if the code allows it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Class&lt;/td&gt;
&lt;td&gt;Metaspace growth from class loading, for example repeated redeploys or dynamic proxies&lt;/td&gt;
&lt;td&gt;Find the class loader leak; set &lt;code&gt;-XX:MaxMetaspaceSize&lt;/code&gt; to fail early with a clear error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;Code cache filling toward its maximum&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;-XX:ReservedCodeCacheSize&lt;/code&gt; against your budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GC&lt;/td&gt;
&lt;td&gt;Collector structures that scale with heap size&lt;/td&gt;
&lt;td&gt;Reduce heap, or leave more headroom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nothing in NMT, but container grows&lt;/td&gt;
&lt;td&gt;Direct buffers or native code outside NMT&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;-XX:MaxDirectMemorySize&lt;/code&gt;; review JNI or native libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Setting &lt;code&gt;-XX:MaxMetaspaceSize&lt;/code&gt; or &lt;code&gt;-XX:MaxDirectMemorySize&lt;/code&gt; does not remove a leak, but it turns a silent kernel kill into a Java &lt;code&gt;OutOfMemoryError&lt;/code&gt; with a message, which is much easier to diagnose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set limits that leave headroom
&lt;/h2&gt;

&lt;p&gt;Once you know the pools, size the container with a margin. A reasonable pattern is to set the heap as a percentage and leave the rest explicitly for non-heap memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resources:
  requests:
    memory: "1536Mi"
  limits:
    memory: "1536Mi"
env:
  - name: JAVA_TOOL_OPTIONS
    value: "-XX:MaxRAMPercentage=60 -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=128m"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line by line: the request equals the limit so the scheduler reserves what the pod can use; &lt;code&gt;MaxRAMPercentage=60&lt;/code&gt; gives a heap cap of about 921Mi (0.60 x 1536Mi); the metaspace and direct memory caps are bounded explicitly. These values are an illustration, not a recommendation. Replace them with the committed sizes you measured with NMT. Note that &lt;code&gt;Mi&lt;/code&gt; is a binary unit; &lt;code&gt;1536M&lt;/code&gt; would be a different, smaller amount.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Java &lt;code&gt;OOMKilled&lt;/code&gt; means the whole container exceeded its limit; it is not the same as a Java &lt;code&gt;OutOfMemoryError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirm with &lt;code&gt;reason: OOMKilled&lt;/code&gt; and exit code 137 before investigating.&lt;/li&gt;
&lt;li&gt;Add up heap, stacks, code cache, metaspace and direct memory first; many kills are budget mistakes.&lt;/li&gt;
&lt;li&gt;Enable &lt;code&gt;-XX:NativeMemoryTracking=summary&lt;/code&gt; (5-10 percent overhead, restart needed), take a &lt;code&gt;baseline&lt;/code&gt;, then run &lt;code&gt;summary.diff&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;NMT does not see memory allocated by non-JVM native code.&lt;/li&gt;
&lt;li&gt;Cap metaspace and direct memory so failures are explicit and diagnosable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Treat a Java &lt;code&gt;OOMKilled&lt;/code&gt; as an accounting problem. Work out the budget, measure which pool grows with NMT diffs, and then either fix the growth or give the container honest headroom. Repeat the diff after each change to confirm that the growing category is the one you fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://opshubs.space/tools/jvm-container-memory-calculator/" rel="noopener noreferrer"&gt;JVM Container Memory Calculator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opshubs.space/tools/" rel="noopener noreferrer"&gt;OpsHubs ops calculators and tools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://opshubs.space/java-oomkilled-in-kubernetes-find-the-memory-pool-with-nmt/" rel="noopener noreferrer"&gt;OpsHubs&lt;/a&gt;, where you can also find free &lt;a href="https://opshubs.space/tools/" rel="noopener noreferrer"&gt;ops calculators&lt;/a&gt;. This article was drafted with AI assistance and reviewed before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>java</category>
      <category>devops</category>
      <category>cloudnative</category>
    </item>
  </channel>
</rss>
