Späť na blog

JVM Metaspace OOM v Kubernetes: Prečo MaxMetaspaceSize Nestačí

Jedinny OOM, ktory nas stale zabijal, bol metaspace, nie heap. “Nastavil som -XX:MaxMetaspaceSize=256m ale pod je stále OOMKilled.” Príčina: Metaspace je off-heap pamäť ktorá rastie s načítanými triedami, váš container limit nemá dostatočnú rezervu, a JVM neuvoľňuje nepoužívané triedy.

Prostredie: JVM 11+, Kubernetes s memory limits, Spring Boot/microservices, dynamické načítavanie tried (reflection, proxies, Groovy)

Problém

Záhadné OOM

Životný cyklus podu:

T+0:00   Pod štartuje, spotreba pamäte: 400MB
         Heap: 256MB, Metaspace: 80MB, Ostatné: 64MB

T+1:00   Traffic narastá, viac code paths sa vykonáva
         Metaspace: 120MB (načítava viac tried)

T+4:00   Metaspace: 200MB (proxies, reflection, lambdy)

T+8:00   Metaspace: 280MB → MaxMetaspaceSize dosiahnutý!
         java.lang.OutOfMemoryError: Metaspace

         Alebo horšie: Container limit dosiahnutý prvý
         OOMKilled (exit code 137) - žiadna Java chyba!

Problém s Matematikou Containera

# Vaša Kubernetes konfigurácia
resources:
  limits:
    memory: "512Mi"
  requests:
    memory: "512Mi"

# Vaše JVM flagy
JAVA_OPTS: "-Xmx256m -XX:MaxMetaspaceSize=256m"

# Čo si myslíte:
# Heap (256MB) + Metaspace (256MB) = 512MB ✓

# Čo sa skutočne deje:
# Heap: 256MB
# Metaspace: až 256MB
# Code cache: 48MB (default)
# Thread stacks: 1MB × N threadov
# Direct buffers: závisí
# JNI/native: závisí
# GC overhead: závisí
# Spolu: 256 + 256 + 48 + 50 + X = 610MB+ → OOMKilled!

Príčina

Čo Je v Metaspace

Obsah Metaspace:

┌─────────────────────────────────────────────────────────────┐
│ Metaspace                                                   │
│ ├─ Metadáta tried (Klass štruktúry)                        │
│ ├─ Metadáta metód (bytecode, JIT compiled code refs)       │
│ ├─ Constant pools                                           │
│ ├─ Anotácie                                                 │
│ ├─ Method counters a profiling dáta                         │
│ └─ Statické premenné (presunuté z PermGen v Java 8)        │
│                                                             │
│ Čo spôsobuje RAST:                                          │
│ - Spring proxies (CGLib/ByteBuddy)                         │
│ - Hibernate entity proxies                                  │
│ - Lambda výrazy (každá je trieda!)                          │
│ - Reflection-heavy frameworky                               │
│ - Groovy/scripting engines                                  │
│ - Dynamické generovanie tried                               │
│ - ClassLoader leaky                                         │
└─────────────────────────────────────────────────────────────┘

Prečo Sa Triedy Neuvoľňujú

// Triedy môžu byť uvoľnené len keď je ich ClassLoader nedosiahnuteľný
// Toto sa v aplikačných serveroch stáva zriedka

// Spring Boot: Jeden aplikačný ClassLoader
// → Triedy načítané raz, nikdy neuvoľnené

// Bežný ClassLoader leak pattern:
public class CacheWithClassRef {
    // Táto cache drží Class referencie
    private static Map<String, Class<?>> classCache = new HashMap<>();

    // Triedy v cache NIKDY nemôžu byť uvoľnené
    // Aj keď odstránite záznamy, ak niečo referencuje Class...
}

// ThreadLocal držiaci inštanciu triedy:
private static ThreadLocal<MyService> service = new ThreadLocal<>();
// Ak thread pool znovupoužíva thready, triedy zostávajú načítané

Diagnostika

Kontrola Spotreby Metaspace

# Získanie Metaspace metrík z JVM
kubectl exec pod-name -- jcmd 1 VM.metaspace

# Výstup:
# Total: reserved=280MB, committed=260MB, used=245MB
# Class: reserved=256MB, committed=240MB, used=230MB
# ├─ Classloader: 1234 tried, 180MB použitých
# ├─ Classloader: 567 tried, 45MB použitých
# └─ ...
# Počet načítaných tried
kubectl exec pod-name -- jcmd 1 VM.classloaders

# Alebo cez JMX
kubectl exec pod-name -- jcmd 1 GC.class_stats

# Zoznam všetkých načítaných tried s veľkosťami
kubectl exec pod-name -- jcmd 1 VM.class_hierarchy

Monitoring Načítavania Tried

// JVM flagy pre viditeľnosť načítavania tried
-XX:+TraceClassLoading
-XX:+TraceClassUnloading
-Xlog:class+load=info
-Xlog:class+unload=info

// V logoch uvidíte:
// [class,load] com.example.Proxy$123 source: __JVM_DefineClass__
// Veľa dynamicky generovaných tried = rast Metaspace

Nájdenie ClassLoader Leakov

# Heap dump a analýza ClassLoaderov
kubectl exec pod-name -- jcmd 1 GC.heap_dump /tmp/heap.hprof
kubectl cp pod-name:/tmp/heap.hprof ./heap.hprof

# V Eclipse MAT alebo VisualVM:
# 1. Nájdite všetky ClassLoader inštancie
# 2. Skontrolujte retained size každého
# 3. Hľadajte duplikátne triedy načítané rôznymi loadermi

Riešenie

Možnosť 1: Správne Dimenzovanie Pamäte Containera

# Započítajte VŠETKY JVM pamäťové regióny
resources:
  limits:
    memory: "768Mi"  # Dajte viac rezervy
  requests:
    memory: "768Mi"

# JVM flagy s explicitnými limitmi
env:
  - name: JAVA_OPTS
    value: >-
      -Xmx256m
      -XX:MaxMetaspaceSize=128m
      -XX:ReservedCodeCacheSize=64m
      -XX:MaxDirectMemorySize=64m
      -XX:+UseContainerSupport
      -XX:MaxRAMPercentage=50.0

# Rozpočet pamäte:
# Heap: 256MB (Xmx)
# Metaspace: 128MB (MaxMetaspaceSize)
# Code cache: 64MB (ReservedCodeCacheSize)
# Direct memory: 64MB (MaxDirectMemorySize)
# Thread stacks: ~100MB (100 threadov × 1MB)
# JVM overhead: ~50MB
# Spolu: ~662MB s rezervou do 768MB

Možnosť 2: Povolenie Uvoľňovania Tried

# JVM flagy pre podporu uvoľňovania tried
-XX:+CMSClassUnloadingEnabled  # Pre CMS (deprecated)
-XX:+ClassUnloading            # Pre G1/ZGC (default on)
-XX:+ClassUnloadingWithConcurrentMark  # Pre G1

# Agresívnejšie Metaspace GC
-XX:MinMetaspaceFreeRatio=10   # Default 40
-XX:MaxMetaspaceFreeRatio=30   # Default 70

# Vynútenie zmenšenia Metaspace po uvoľnení
-XX:+ShrinkHeapInSteps

Možnosť 3: Zníženie Dynamického Generovania Tried

// Spring: Preferujte interfaces pred class proxying
@Service
public class MyServiceImpl implements MyService {
    // Interface proxy (JDK dynamic proxy) vs
    // Class proxy (CGLib - generuje viac tried)
}

// Spring Boot konfigurácia
spring.aop.proxy-target-class=false  # Používajte JDK proxies

// Hibernate: Znížte generovanie proxy
spring.jpa.properties.hibernate.bytecode.use_reflection_optimizer=false
spring.jpa.properties.hibernate.bytecode.provider=none
// Lambda optimalizácia - znovupoužitie namiesto inline
// ZLE: Každá lambda je nová trieda
list.forEach(item -> process(item));

// LEPŠIE: Method reference (môže zdieľať triedu)
list.forEach(this::process);

// NAJLEPŠIE pre hot paths: Znovupoužitie functional interface inštancie
private final Consumer<Item> processor = this::process;
list.forEach(processor);

Možnosť 4: ClassLoader Izolácia

// Pre plugin systémy alebo scripting engines
// Použite samostatné ClassLoadery ktoré môžu byť garbage collected

public class PluginLoader {
    public void loadAndExecute(String pluginJar) {
        // URLClassLoader môže byť zatvorený a GC'd
        try (URLClassLoader loader = new URLClassLoader(
                new URL[]{new URL("file:" + pluginJar)},
                null  // Bez parenta - izoluje triedy
        )) {
            Class<?> plugin = loader.loadClass("com.plugin.Main");
            plugin.getMethod("run").invoke(plugin.getDeclaredConstructor().newInstance());
        }
        // Po try bloku je loader zatvorený
        // Triedy môžu byť uvoľnené pri ďalšom GC
    }
}

Možnosť 5: Native Memory Tracking

# Povolenie NMT pre zobrazenie všetkých pamäťových regiónov
-XX:NativeMemoryTracking=summary  # alebo detail

# Získanie rozpisu pamäte
kubectl exec pod-name -- jcmd 1 VM.native_memory summary

# Výstup zobrazuje:
# Total: reserved=1500MB, committed=800MB
# - Java Heap: 256MB
# - Class (Metaspace): 180MB
# - Thread: 120MB (120 threadov)
# - Code: 50MB
# - GC: 30MB
# - Internal: 20MB
# - Symbol: 15MB
# ...

Monitoring

groups:
  - name: jvm-metaspace
    rules:
      - alert: MetaspaceHigh
        expr: |
          jvm_memory_used_bytes{area="nonheap", id="Metaspace"} /
          jvm_memory_max_bytes{area="nonheap", id="Metaspace"} > 0.85
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Spotreba Metaspace nad 85%"

      - alert: ClassLoadingRate
        expr: |
          rate(jvm_classes_loaded_classes_total[5m]) > 100
        for: 30m
        labels:
          severity: warning
        annotations:
          summary: "Vysoká rýchlosť načítavania tried - možný leak"

      - alert: ContainerOOMRisk
        expr: |
          container_memory_working_set_bytes /
          container_spec_memory_limit_bytes > 0.9
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Container blízko memory limitu - riziko OOMKill"

Checklist

## JVM Metaspace OOM Kubernetes

### Diagnostika
- [ ] Skontrolujte spotrebu Metaspace: jcmd 1 VM.metaspace
- [ ] Spočítajte načítané triedy: jcmd 1 VM.classloaders
- [ ] Povoľte NMT: -XX:NativeMemoryTracking=summary
- [ ] Sledujte trendy načítavania tried v čase

### Dimenzovanie Containera
- [ ] Vypočítajte celkovú JVM pamäť (heap + metaspace + codecache + threads + overhead)
- [ ] Nastavte container limit 20-30% nad JVM total
- [ ] Explicitne nastavte MaxMetaspaceSize
- [ ] Explicitne nastavte ReservedCodeCacheSize

### Zníženie Metaspace
- [ ] Preferujte JDK proxies pred CGLib
- [ ] Znovupoužívajte lambda inštancie v hot paths
- [ ] Skontrolujte ClassLoader leaky v heap dumpe
- [ ] Zatvárajte/dispose ClassLoadery keď sú hotové

Záver

Poučenie: MaxMetaspaceSize limituje Metaspace ale nezabraňuje container OOM. Musíte započítať VŠETKY JVM pamäťové regióny do vášho container limitu a aktívne spravovať načítavanie tried.

Kľúčové princípy:

  1. Container limit > Heap + Metaspace + CodeCache + Threads + Overhead
  2. Triedy sa zriedka uvoľňujú - raz načítané, zostávajú
  3. Dynamické proxies a lambdy vytvárajú triedy - každá stojí Metaspace
  4. Používajte NMT pre pochopenie skutočnej spotreby pamäte

Súvisiace Články

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "JVM Metaspace OOM v Kubernetes: Prečo MaxMetaspaceSize Nestačí". https://www.michal-drozd.com/sk/blog/jvm-metaspace-oom-kubernetes/ (Publikované 23. decembra 2024).