/개발/OutOfMemoryError 5-Family 30-Second Diagnosis Runbook: heap space vs Metaspace Recovery Commands
DevelopmentOutOfMemoryErrorJVM 튜닝

OutOfMemoryError 5-Family 30-Second Diagnosis Runbook: heap space vs Metaspace Recovery Commands

With java.lang.OutOfMemoryError, classifying the family from a single line of the original error finishes 90% of the diagnosis. A practical runbook of copy-paste commands covering the 5-family decision table, jstat/jmap heap dump analysis,

OutOfMemoryError 5-Family 30-Second Diagnosis Runbook: heap space vs Metaspace Recovery Commands

OutOfMemoryError 5-Family 30-Second Diagnosis Runbook: heap space vs Metaspace Recovery Commands

It's 3 a.m., an alert fires, and the logs show java.lang.OutOfMemoryError. Plenty of people immediately bump -Xmx—and half the time that's wasted effort. Not every OutOfMemoryError is the same OOM. If it's a Metaspace leak and you only grow the heap, it will come back. If it's a container cgroup problem and you only tweak JVM flags, the process dies again with OOMKilled.

Here's the key: a single line of the original error message decides 90% of the diagnosis. This post is structured so you can scroll and copy-paste during an incident.

30-Second Decision Table: Classify the 5 Families from the Error Text

Start with the phrase attached to the error message. The text that follows OutOfMemoryError determines the family.

Error textRegionTypical cause30-second first actionNext-step command
Java heap spaceHeap (Eden/Old)Object leak or simple heap shortageCapture a heap dump, then restartjmap -dump:live + MAT analysis
MetaspaceMetaspaceClassloader leak; explosion of dynamic proxies/hot deploysCheck loaded class countTrend of the M column in jstat -gcutil
GC overhead limit exceededHeap (GC time)Heap nearly full; GC reclaiming only tiny amountsEffectively heap shortage/leakConfirm FGC explosion in jstat -gcutil
unable to create new native threadNative (thread stacks)Thread leak; excessive ulimit/stack sizeCount threadsjstack <pid> thread dump
Direct buffer memoryOff-heap direct memoryUnreleased NIO/Netty ByteBuffersCheck MaxDirectMemorySizejcmd VM.native_memory (NMT)

Just distinguishing Java heap space from Metaspace completely changes the response. That's 30 seconds.

Copy-Paste Diagnosis Command Set

Once you've identified the error family, pin down the cause. Copy these in order.

1) Live utilization by region — See at a glance which region is filling up.

Bash
# Print GC region utilization (%) every 1 second. M column is Metaspace
jstat -gcutil <pid> 1000

Among S0 S1 E O M CCS YGC YGCT FGC FGCT GCT, if O (Old) sits near 100% and never drops, that's a heap leak; if M (Metaspace) keeps climbing, that's a classloader leak.

2) Extract top occupying objects — Top 30 of what's eating the heap.

Bash
jmap -histo:live <pid> | head -30

3) Heap snapshot

Bash
jcmd <pid> GC.heap_info

4) Capture a heap dump — Take it before you restart. If you lose it, you can't find the cause.

Bash
jmap -dump:live,format=b,file=heap.hprof <pid>

Open the resulting heap.hprof in Eclipse MAT and look at the Leak Suspects report and Dominator Tree—the leaking objects show up immediately.

5) Enable GC logging — Verify the growth trend over time. Options differ by JDK version.

Bash
# Java 11+ (unified logging)
-Xlog:gc*:file=gc.log:time,uptime,level,tags

# Java 8
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

The Real Culprit Behind Metaspace Leaks: Classloaders That Never Die

A Metaspace OOM is almost always a classloader leak, not simple shortage. Unlike the heap, this is where loaded class metadata accumulates.

The most common trap in practice is Spring Boot DevTools. DevTools creates a new RestartClassLoader every time code changes; if the previous loader is held by a reference and never GC'd, classes get loaded twice. Add CGLIB dynamic proxies or repeated hot deploys and the loaded class count explodes.

CODE
# Typical leak pattern observed with jstat -gcutil (M column excerpted)
  M      CCS    YGC   FGC
 71.20  68.11    12     0    <- healthy
 84.55  79.03    18     2
 93.87  88.40    25     6
 99.12  95.71    31    14    <- FGC exploding but M doesn't drop = leak confirmed

If things were healthy, M should rise and fall as classes unload. If FGC increases but M never drops, it's a classloader leak. Count loaded classes like this.

Bash
jcmd <pid> VM.classloader_stats   # loaded count / bytes per classloader

Practitioner tip: I once left DevTools enabled only in staging and hit a Metaspace OOM during a long uninterrupted test. Always strip the DevTools dependency from production images, and set an explicit cap like -XX:MaxMetaspaceSize=256m so unbounded growth is caught early. Without a cap it keeps eating native memory until the whole container is OOMKilled.

When the JVM Misreads Memory in a Container

A common incident in container deployments is the JVM seeing the host's full memory instead of the cgroup limit.

UseContainerSupport not appliedApplied (ON by default in JDK 10+)
Recognized memoryEntire host (e.g. 64Gi)cgroup limit (512Mi)
Default heap sizingOver-allocated from the hostRatio allocated from the limit
ResultHeap exceeds limit → OOMKilledHeap cap decided safely

On JDK 8u191+ and JDK 10+, -XX:+UseContainerSupport is enabled by default. Prefer sizing the heap as a percentage rather than an absolute (-Xmx).

Bash
# 75% of the container limit as max heap. The remaining 25% is for Metaspace/threads/direct buffers
-XX:MaxRAMPercentage=75.0

cgroup v2 migrations have brought recognition issues back, so JDK 17/21 LTS is recommended. cgroup v2 support is solid there.

OOMKilled(137) and JVM OOM Are Different Incidents

Confusing the two means you fix the wrong thing.

OOMKilledJVM OutOfMemoryError
Where you find itkubectl describe podApplication logs
What you seeReason: OOMKilled, Exit Code: 137java.lang.OutOfMemoryError stack trace
Who killed itKernel via SIGKILL(9)JVM throwing an exception itself
CauseContainer total memory > limitA specific JVM region exceeded its limit

The key is that container total usage is not just the heap. When heap + Metaspace + thread stacks + Direct buffer + code cache exceeds the limit, the kernel OOMKills the process. That's why setting -Xmx exactly to the limit always dies from off-heap memory. That's why you leave headroom with MaxRAMPercentage=75.

Pre-deploy checklist

  • Does -Xmx/MaxRAMPercentage leave headroom (70–75%) vs. the container limit?
  • Is -XX:MaxMetaspaceSize set explicitly?
  • Is Spring DevTools removed from the production image?
  • Is GC logging (-Xlog:gc*) enabled so you keep a trend?
  • Is automatic heap dump on OOM (-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump) configured?
  • Have you verified memory recognition on JDK 17/21 LTS + cgroup v2?

References: Official Docs

The primary source for the behavior, settings, and errors in this post is the following official documentation. Check version-specific options and exact behavior there.

Frequently Asked Questions (FAQ)

Q. I raised -Xmx and a few days later I got another heap space OOM. A. It's likely a heap leak, not simple shortage. If the Old generation doesn't drop after Full GC in jstat -gcutil, it's a leak. Dump with jmap -dump:live and find the culprit in MAT's Dominator Tree. Growing the heap only stretches the incident interval; it will come back.

Q. It's a Metaspace OOM—why isn't the classloader getting GC'd? A. A classloader stays alive as a whole if even one class/instance it loaded is still referenced from a GC root. DevTools reloads, cached CGLIB proxies, and instances stuck in ThreadLocals are common causes. Confirm exploding loader counts with jcmd <pid> VM.classloader_stats.

Q. The Pod dies with Exit Code 137 but there's no OOM in the application logs. A. That's container OOMKilled, not JVM OOM. Total usage including off-heap (Metaspace, threads, Direct buffer) exceeded the limit, so don't raise -Xmx—lower MaxRAMPercentage or raise the limit to leave headroom.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·
관련 공식 문서Oracle Java 공식 문서

Comments

Be the first to comment.