{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreig37yghr4ekmp3q3mw4asrrxhreisz2sqg2op6q7ze76y2tkfwste",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mogxqthgdul2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreiducgxeno3oeol75umo32bmore5m6ve5qaxb7cssmjmd3iqxjpbkm"
    },
    "mimeType": "image/webp",
    "size": 279100
  },
  "path": "/schiff_heimlich/your-java-container-is-lying-to-you-about-its-memory-1g04",
  "publishedAt": "2026-06-16T22:54:22.000Z",
  "site": "https://dev.to",
  "textContent": "##  The part of memory Java doesn't tell you about\n\nJava doesn't just use heap. The JVM also allocates:\n\n  * **Metaspace** — class metadata, loaded by the JVM itself\n  * **Code cache** — JIT-compiled native code\n  * **Thread stacks** — each thread gets its own\n  * **Direct byte buffers** (NIO) — allocated off-heap by many libraries\n  * **Internal JVM bookkeeping**\n\n\n\nThis is called native memory, and it's invisible to your usual heap monitoring. When your container hits its cgroup memory limit, the kernel doesn't care how much heap you have left — it kills the process when the _total_ RSS exceeds the limit.\n\nA 512MB container running a JVM with 256MB heap can easily OOM at around 350–400MB total RSS, because metaspace, code cache, and buffers have already eaten into the headroom you didn't know you needed.\n\n##  The fix nobody explains properly\n\nThe old way: `-Xms256m -Xmx256m`. Fixed heap size, ignores container limits.\n\nThe better way:\n\n\n\n    -XX:MaxRAMPercentage=75.0\n\n\nThis tells the JVM to size the heap as a percentage of the _container's actual memory limit_ , not some fixed number. If your container has 512MB, the heap gets roughly 384MB. The remaining ~128MB is left for native memory, JIT overhead, and everything else the JVM allocates outside the heap.\n\nFor most workloads, 75% is a reasonable starting point. If you're running into native memory pressure (you'll see it in `jcmd VM.native_memory`), dial it down to 70%.\n\nA few other flags worth knowing:\n\n\n\n    # Pre-touch heap pages at startup instead of on first access\n    -XX:+AlwaysPreTouch\n\n    # Cap metaspace growth so it can't run away\n    -XX:MaxMetaspaceSize=256m\n\n\n`AlwaysPreTouch` is a tradeoff — it increases startup time but prevents those surprise OOMs when a traffic spike touches cold heap pages for the first time.\n\n##  How to actually see what's happening\n\nHeap usage comes from your app, but native memory is opaque by default. Enable native memory tracking:\n\n\n\n    -XX:NativeMemoryTracking=detail\n\n\nThen query it at runtime:\n\n\n\n    jcmd <pid> VM.native_memory summary\n\n\nOutput looks like:\n\n\n\n    Native Memory Tracking:\n    Total: reserved=618MB, committed=412MB\n    - Heap         : 256MB reserved, 180MB committed\n    - Class        :  45MB reserved,  38MB committed\n    - Thread       :  12MB reserved,  12MB committed\n    - Code         :  28MB reserved,  22MB committed\n    - Internal     :   8MB reserved,   8MB committed\n\n\nThat's the total picture. Watch the \"committed\" column under Heap against the overall RSS — if RSS is consistently 100–150MB above committed heap, that's native overhead you need to account for when sizing your container.\n\n##  The short version\n\nYour container limit needs to cover heap _plus_ native memory. If you only tune the heap, you're flying blind. Switch to `-XX:MaxRAMPercentage`, enable `NativeMemoryTracking` so you can actually see what's being used, and you'll stop getting OOMs when heap looks fine.\n\nIt's a 15-minute change and it eliminates one of those \"but the monitoring said we had headroom\" incidents that show up at 2am.",
  "title": "Your Java Container Is Lying to You About Its Memory"
}