Beheben Sie “java.lang.OutOfMemoryError: Java heap space” in Java: Ursachen, Grundlagen des Heaps und praktische Lösungen

目次

1. Einführung

Wenn Sie in Java entwickeln, ist Ihnen dann schon einmal passiert, dass Ihre Anwendung plötzlich abstürzt und die Konsole Folgendes anzeigt:

java.lang.OutOfMemoryError: Java heap space

Dieser Fehler bedeutet „Java hat keinen nutzbaren Speicher mehr (den Heap).“
Allein aus der Fehlermeldung wird jedoch nicht sofort ersichtlich:

  • Was den Heap erschöpft hat
  • Was Sie anpassen sollten und wie
  • Ob das Problem im Code oder in der Konfiguration liegt

Infolgedessen greifen viele zu „Schnelllösungen“ wie „einfach -Xmx erhöhen“ oder „mehr Server‑Speicher hinzufügen.“

Aber die Heap‑Größe zu erhöhen, ohne die eigentliche Ursache zu verstehen, ist nicht nur keine echte Lösung – sie kann auch andere Probleme auslösen.

  • GC (Garbage Collection) wird schwerer und die Antwortzeiten verschlechtern sich
  • Der Gesamtspeicher des Servers wird knapp und beeinträchtigt andere Prozesse
  • Ein echter Speicher‑Leak bleibt bestehen und ein OutOfMemoryError tritt erneut auf

Deshalb ist „java heap space“ nicht einfach „wenig Speicher“.
Sie sollten es als ein Zeichen für ein zusammengesetztes Problem betrachten, das Anwendungsdesign, Implementierung und Infrastruktur‑Einstellungen betrifft.

1-1. Zielgruppe

Dieser Artikel richtet sich an Leser, die:

  • Die Grundlagen von Java verstehen (Klassen, Methoden, Collections usw.)
  • Aber nicht vollständig nachvollziehen, wie der Speicher innerhalb der JVM verwaltet wird
  • „java heap space“ oder OutOfMemoryError in Entwicklung/Test/Produktion erlebt haben – oder darauf vorbereitet sein wollen
  • Java in Docker/Containern/der Cloud betreiben und fühlen sich etwas unsicher bei den Speichereinstellungen

Ihre Jahre an Java‑Erfahrung spielen keine Rolle.
Wenn Sie den Fehler „richtig verstehen und die Ursache selbst isolieren“ möchten, soll dieser Leitfaden direkt in der Praxis nützlich sein.

1-2. Was Sie in diesem Artikel lernen werden

In diesem Artikel erklären wir den Fehler „java heap space“ vom Mechanismus her nach oben – nicht nur als Liste von Fixes.

Wichtige Themen umfassen:

  • Was der Java‑Heap ist wp:list /wp:list

    • Wie er sich vom Stack unterscheidet
    • Wo Objekte alloziert werden
    • Typische Muster, die „java heap space“ verursachen wp:list /wp:list

    • Massives Laden großer Datenmengen

    • Überwuchernde Collections und Caches
    • Speicher‑Leaks (Code, der Referenzen am Leben hält)
    • Wie man die Heap‑Größe prüft und erhöht wp:list /wp:list

    • Befehlszeilen‑Optionen (-Xms, -Xmx)

    • IDE‑Einstellungen (Eclipse / IntelliJ usw.)
    • Konfigurationspunkte von Anwendungs‑Servern (Tomcat usw.)
    • Speichersparende Techniken im Code wp:list /wp:list

    • Überprüfung der Nutzung von Collections

    • Fallstricke bei Streams und Lambdas
    • Chunk‑Strategien für große Datenmengen
    • Die Beziehung zwischen GC und dem Heap wp:list /wp:list

    • Wie GC grundsätzlich funktioniert

    • Wie man GC‑Logs auf Basis‑Level liest
    • Erkennen von Speicher‑Leaks und Einsatz von Tools wp:list /wp:list

    • Einen Heap‑Dump erstellen

    • Einstieg in die Analyse mit VisualVM oder Eclipse MAT
    • Dinge, auf die man in Container‑Umgebungen (Docker / Kubernetes) achten sollte wp:list /wp:list

    • Die Beziehung zwischen Containern und -Xmx

    • Speicher‑Limits via cgroups und dem OOM‑Killer

Im zweiten Teil des Artikels beantworten wir außerdem häufige Fragen im FAQ‑Format, zum Beispiel:

  • „Soll ich jetzt einfach den Heap erhöhen?“
  • „Wie weit kann ich den Heap sicher vergrößern?“
  • „Wie erkenne ich grob, ob es ein Speicher‑Leak ist?“

1-3. Wie man diesen Artikel liest

Der Fehler „java heap space“ ist sowohl für Personen wichtig, die:

  • Einen Produktions‑Vorfall sofort beheben müssen
  • Probleme vorbeugen wollen, bevor sie auftreten

Wenn Sie eine sofortige Lösung benötigen, können Sie zu den praktischen Abschnitten springen, etwa:

  • Wie man die Heap‑Größe ändert
  • Wie man nach Speicher‑Leaks sucht

Wenn Sie hingegen ein gründliches Verständnis erlangen wollen, lesen Sie in folgender Reihenfolge:

  1. Die Grundlagen: „Was der Java‑Heap ist“
  2. Typische Ursachen
  3. Anschließend Lösungen und Tuning‑Schritte

Dieser Ablauf hilft Ihnen, den Mechanismus hinter dem Fehler sauber zu verstehen.

2. Was ist der Java‑Heap?

Um den “java heap space”-Fehler richtig zu verstehen, müssen Sie zunächst wissen wie Java den Speicher verwaltet.
In Java ist der Speicher nach Zweck in mehrere Bereiche unterteilt, und darunter spielt der Heap eine entscheidende Rolle als der Speicherbereich für Objekte.

2-1. Gesamtüberblick über Java-Speicherbereiche

Java-Anwendungen laufen auf der JVM (Java Virtual Machine).
Die JVM verfügt über mehrere Speicherbereiche, um verschiedene Arten von Daten zu verwalten. Die drei gebräuchlichsten sind:

■ Arten von Speicherbereichen

  • Heap Der Bereich, in dem Objekte, die von der Anwendung erstellt werden, gespeichert werden. Wenn dieser erschöpft ist, erhalten Sie den Fehler “java heap space”.
  • Stack Der Bereich für Methodenaufrufe, lokale Variablen, Referenzen und mehr. Wenn dieser überläuft, erhalten Sie “StackOverflowError”.
  • Method Area / Metaspace Speichert Klasseninformationen, Konstanten, Metadaten und JIT‑Kompilierungsergebnisse.

In Java werden alle mit new erstellten Objekte im Heap abgelegt.

2-2. Die Rolle des Heaps

Der Java-Heap ist der Ort, an dem Dinge wie die folgenden gespeichert werden:

  • Mit new erstellte Objekte
  • Arrays (einschließlich des Inhalts von List/Map usw.)
  • Intern von Lambdas erzeugte Objekte
  • Strings und Puffer, die von StringBuilder verwendet werden
  • Datenstrukturen, die im Collection‑Framework verwendet werden

Mit anderen Worten, wenn Java etwas „im Speicher behalten“ muss, wird es fast immer im Heap abgelegt.

2-3. Was passiert, wenn der Heap voll ist?

Ist der Heap zu klein – oder erstellt die Anwendung zu viele Objekte – führt Java GC (Garbage Collection) aus, um Speicher freizugeben, indem nicht mehr benötigte Objekte entfernt werden.

Wenn jedoch wiederholte GC nicht genug Speicher freigeben kann und die JVM keinen Speicher mehr zuweisen kann, erhalten Sie:

java.lang.OutOfMemoryError: Java heap space

und die Anwendung wird zum Stoppen gezwungen.

2-4. „Einfach den Heap vergrößern“ ist halb richtig und halb falsch

Ist der Heap einfach zu klein, kann eine Vergrößerung das Problem lösen – zum Beispiel:

-Xms1024m -Xmx2048m

Ist die eigentliche Ursache jedoch ein Speicherleck oder eine ineffiziente Verarbeitung riesiger Daten im Code, kauft das Vergrößern des Heaps nur Zeit und behebt das zugrunde liegende Problem nicht.

Kurz gesagt, das Verständnis „warum der Heap voll wird“ ist das Wichtigste.

2-5. Heap‑Layout (Eden / Survivor / Old)

Der Java-Heap ist grob in zwei Teile unterteilt:

  • Young generation (neu erstellte Objekte) wp:list /wp:list

    • Eden
    • Survivor (S0, S1)
    • Old generation (langlebige Objekte)

GC funktioniert je nach Bereich unterschiedlich.

Young Generation

Objekte werden zunächst im Eden abgelegt, und kurzlebige Objekte werden schnell entfernt.
GC wird hier häufig ausgeführt, ist aber relativ leichtgewichtig.

Old Generation

Objekte, die lange genug überleben, werden von Young nach Old befördert.
GC im Old-Bereich ist aufwändiger, sodass ein kontinuierliches Wachstum dieses Bereichs Latenz oder Pausen verursachen kann.

In vielen Fällen entsteht ein “heap space”-Fehler letztlich, weil die Old‑Generation voll wird.

2-6. Warum ein Heap‑Mangel bei Anfängern und fortgeschrittenen Entwicklern häufig ist

Da Java die Garbage Collection automatisch durchführt, gehen viele davon aus, dass „die JVM das gesamte Speicher‑Management übernimmt.“

In Wirklichkeit gibt es viele Wege, den Heap zu erschöpfen, zum Beispiel:

  • Code, der ständig große Mengen an Objekten erzeugt
  • Referenzen, die in Collections am Leben gehalten werden
  • Streams/Lambdas, die unbeabsichtigt riesige Daten erzeugen
  • Überdimensionierte Caches
  • Fehlinterpretation der Heap‑Grenzen in Docker‑Containern
  • Falsche Heap‑Konfiguration in einer IDE

Deshalb ist das Verständnis, wie der Heap funktioniert, der schnellste Weg zu einer zuverlässigen Lösung.

3. Häufige Ursachen des “java heap space”-Fehlers

Heap‑Mangel ist ein häufiges Problem in vielen realen Umgebungen, aber die Ursachen lassen sich im Wesentlichen in drei Kategorien einteilen: Datenvolumen, Code/Design und Fehlkonfiguration.
In diesem Abschnitt ordnen wir typische Muster und erklären, warum sie zum Fehler führen.

3-1. Speicherdruck durch Laden großer Daten

Das häufigste Muster ist, wenn die Daten selbst so groß sind, dass der Heap erschöpft wird.

■ Häufige Beispiele

  • Laden einer riesigen CSV/JSON/XML auf einmal in den Speicher
  • Abrufen einer massiven Anzahl von Datenbankdatensätzen auf einen Schlag
  • Eine Web‑API liefert eine sehr große Antwort (Bilder, Protokolle usw.)

Ein besonders gefährliches Szenario ist:

Wenn der „Rohstring vor dem Parsen“ und die „Objekte nach dem Parsen“ gleichzeitig im Speicher existieren.

Zum Beispiel, wenn Sie ein 500 MB‑JSON als einzelnen String laden und es dann mit Jackson deserialisieren, kann die gesamte Speichernutzung leicht 1 GB überschreiten.

■ Vorgehensweise zur Minderung

  • Einführung von Chunk‑basiertem Lesen (Streaming‑Verarbeitung)
  • Verwendung von Paging für den Datenbankzugriff
  • Vermeiden Sie das Behalten von Zwischendaten länger als nötig

Die Befolgung der Regel „große Daten in Chunks verarbeiten“ trägt wesentlich dazu bei, einen Heap‑Erschöpfung zu verhindern.

3-2. Übermäßiges Anhäufen von Daten in Sammlungen

Dies ist bei Anfängern bis zu fortgeschrittenen Entwicklern äußerst verbreitet.

■ Typische Fehler

  • Kontinuierliches Hinzufügen von Logs oder temporären Daten zu einer Listsie wächst, ohne geleert zu werden
  • Verwendung einer Map als Cache (aber nie Einträge entfernen)
  • Ständiges Erzeugen neuer Objekte innerhalb von Schleifen
  • Erzeugen einer riesigen Anzahl temporärer Objekte über Streams oder Lambdas

In Java, solange eine Referenz besteht, kann die GC das Objekt nicht entfernen.
In vielen Fällen behalten Entwickler unbeabsichtigt Referenzen bei.

■ Vorgehensweise zur Minderung

  • Definieren Sie einen Lebenszyklus für Caches
  • Setzen Sie Kapazitätsgrenzen für Sammlungen
  • Für groß‑Daten‑Mechanismen regelmäßig leeren

Zur Referenz, selbst wenn es nicht wie ein Speicherleck „aussieht“:

List<String> list = new ArrayList<>();
for (...) {
    list.add(heavyData);  // ← grows forever
}

Diese Art von Code ist sehr gefährlich.

3-3. Speicherlecks (unbeabsichtigte Objektbindung)

Da Java eine GC hat, denken viele, „Speicherlecks passieren nicht in Java.“
In der Praxis passieren Speicherlecks absolut in Java.

■ Häufige Leak‑Hotspots

  • Objekte in statischen Variablen behalten
  • Vergessen, Listener oder Callbacks abzumelden
  • Referenzen in Streams/Lambdas am Leben lassen
  • Objekte, die sich in langlaufenden Batch‑Jobs ansammeln
  • Große Daten in ThreadLocal speichern und der Thread wird wiederverwendet

Speicherlecks sind etwas, das man in Java nicht vollständig vermeiden kann.

■ Vorgehensweise zur Minderung

  • Überprüfen Sie, wie Sie statische Variablen verwenden
  • Stellen Sie sicher, dass removeListener() und close() immer aufgerufen werden
  • Für langlaufende Prozesse einen Heap‑Dump erstellen und untersuchen
  • ThreadLocal vermeiden, es sei denn, es ist wirklich nötig

Da Speicherlecks auch bei Erhöhung des Heaps wieder auftreten, ist die Untersuchung der Grundursache unerlässlich.

3-4. JVM‑Heap‑Größe zu klein (Standardwerte sind klein)

Manchmal ist die Anwendung in Ordnung, aber der Heap selbst ist einfach zu klein.

Die Standard‑Heap‑Größe variiert je nach Betriebssystem und Java‑Version.
In Java 8 wird sie üblicherweise auf etwa 1/64 bis 1/4 des physischen Speichers festgelegt.

Eine gefährliche Konfiguration, die in der Produktion häufig zu sehen ist, ist:

No -Xmx specified, while the app processes large data

■ Häufige Szenarien

  • Nur die Produktion hat ein größeres Datenvolumen, und der Standard‑Heap reicht nicht aus
  • Ausführung in Docker ohne Setzen von -Xmx
  • Spring Boot gestartet als Fat‑JAR mit Standardwerten

■ Vorgehensweise zur Minderung

  • Setzen Sie -Xms und -Xmx auf geeignete Werte
  • In Containern verstehen Sie physikalischen Speicher vs. cgroup‑Grenzen und konfigurieren Sie entsprechend

3-5. Langlaufende Muster, bei denen Objekte weiter ansammeln

Anwendungen wie die folgenden neigen dazu, im Laufe der Zeit Speicher‑Druck aufzubauen:

  • Langlaufende Spring‑Boot‑Anwendungen
  • Speicherintensive Batch‑Jobs
  • Web‑Anwendungen mit hohem Nutzeraufkommen

Batch‑Jobs zeigen insbesondere oft dieses Muster:

  • Speicher wird verbraucht
  • GC erholt kaum genug
  • Es bleibt eine gewisse Anhäufung, und der nächste Durchlauf führt zu OOM

This leads to many verzögerten Heap‑Speicherfehlern.

3-6. Missverständnisse bei Begrenzungen in Containern (Docker / Kubernetes)

Es gibt eine häufige Stolperfalle in Docker/Kubernetes:

■ Stolperfalle

  • -Xmx nicht setzen → Java bezieht sich auf den physischen Host‑Speicher statt auf das Container‑Limit → es wird zu viel verwendet → der Prozess wird vom OOM‑Killer beendet

Dies ist einer der häufigsten Produktionsvorfälle.

■ Abhilfe

  • -XX:MaxRAMPercentage angemessen setzen
  • -Xmx an das Speicher‑Limit des Containers anpassen
  • „UseContainerSupport“ in Java 11+ verstehen

4. Wie man die Heap‑Größe prüft

Wenn Sie einen “java heap space”-Fehler sehen, sollten Sie zuerst bestätigen, wie viel Heap derzeit zugewiesen ist.
In vielen Fällen ist der Heap einfach kleiner als erwartet – das Prüfen ist also ein kritischer erster Schritt.

In diesem Abschnitt behandeln wir Möglichkeiten, die Heap‑Größe zu prüfen, von der Befehlszeile, innerhalb des Programms, in IDEs und in Anwendungsservern.

4-1. Heap‑Größe von der Befehlszeile prüfen

Java bietet mehrere Optionen, um JVM‑Konfigurationswerte beim Start zu prüfen.

■ Verwendung von -XX:+PrintFlagsFinal

Dies ist die zuverlässigste Methode, die Heap‑Größe zu bestätigen:

java -XX:+PrintFlagsFinal -version | grep HeapSize

Sie erhalten eine Ausgabe wie:

  • InitialHeapSize … die initiale Heap‑Größe, angegeben durch -Xms
  • MaxHeapSize … die maximale Heap‑Größe, angegeben durch -Xmx

Beispiel:

uintx InitialHeapSize                          = 268435456
uintx MaxHeapSize                              = 4294967296

Das bedeutet:

  • Initialer Heap: 256 MB
  • Maximaler Heap: 4 GB

■ Konkretes Beispiel

java -Xms512m -Xmx2g -XX:+PrintFlagsFinal -version | grep HeapSize

Dies ist auch nach Änderungen der Einstellungen nützlich und stellt eine verlässliche Bestätigungsmethode dar.

4-2. Heap‑Größe innerhalb eines laufenden Programms prüfen

Manchmal möchte man die Heap‑Menge aus der laufenden Anwendung heraus prüfen.

Java macht das mit der Runtime‑Klasse einfach:

long max = Runtime.getRuntime().maxMemory();
long total = Runtime.getRuntime().totalMemory();
long free = Runtime.getRuntime().freeMemory();

System.out.println("Max Heap:    " + (max / 1024 / 1024) + " MB");
System.out.println("Total Heap:  " + (total / 1024 / 1024) + " MB");
System.out.println("Free Heap:   " + (free / 1024 / 1024) + " MB");
  • maxMemory() … die maximale Heap‑Größe (-Xmx)
  • totalMemory() … der vom JVM aktuell zugewiesene Heap
  • freeMemory() … der derzeit verfügbare Speicher innerhalb dieses Heaps

Für Web‑Apps oder langlaufende Prozesse kann das Protokollieren dieser Werte bei der Untersuchung von Vorfällen helfen.

4-3. Prüfung mit Werkzeugen wie VisualVM oder Mission Control

Man kann die Heap‑Nutzung auch visuell mit GUI‑Werkzeugen untersuchen.

■ VisualVM

  • Echtzeit‑Anzeige der Heap‑Nutzung
  • GC‑Timing
  • Heap‑Dump‑Erfassung

Es ist ein klassisches, häufig genutztes Werkzeug in der Java‑Entwicklung.

■ Java Mission Control (JMC)

  • Ermöglicht detaillierteres Profiling
  • Besonders nützlich für Operationen mit Java 11+

Diese Werkzeuge helfen, Probleme zu visualisieren, wie z. B. nur das Old‑Generation‑Wachstum.

4-4. Prüfung in IDEs (Eclipse / IntelliJ)

Wenn Sie Ihre Anwendung aus einer IDE starten, können die IDE‑Einstellungen die Heap‑Größe beeinflussen.

■ Eclipse

Window → Preferences → Java → Installed JREs

Oder setzen Sie -Xms / -Xmx unter:
Run Configuration → VM arguments

■ IntelliJ IDEA

Help → Change Memory Settings

Oder fügen Sie -Xmx unter VM‑Optionen in Run/Debug Configuration hinzu.

Vorsicht – manchmal legt die IDE selbst ein Heap‑Limit fest.

4-5. Prüfung in Anwendungsservern (Tomcat / Jetty)

Bei Web‑Anwendungen wird die Heap‑Größe häufig in den Start‑Skripten des Servers angegeben.

■ Tomcat‑Beispiel (Linux)

CATALINA_OPTS="-Xms512m -Xmx2g"

■ Tomcat‑Beispiel (Windows)

set JAVA_OPTS=-Xms512m -Xmx2g

In der Produktion ist es üblich, dies auf den Standardwerten zu belassen – und das führt häufig zu Heap‑Speicherfehlern, nachdem der Dienst eine Weile läuft.

4-6. Heap‑Überprüfung in Docker / Kubernetes (Wichtig)

In Containern interagieren physischer Speicher, Cgroups und Java‑Einstellungen auf komplizierte Weise.

In Java 11+ kann „UseContainerSupport“ den Heap automatisch anpassen, aber das Verhalten kann je nach Situation dennoch unerwartet sein, abhängig von:

  • Das Speicherlimit des Containers (z. B. --memory=512m )
  • Ob -Xmx explizit gesetzt ist

Zum Beispiel, wenn Sie nur ein Speicherlimit für den Container festlegen:

docker run --memory=512m ...

und -Xmx nicht setzen, können Sie auf folgendes stoßen:

  • Java bezieht sich auf den Host‑Speicher und versucht, zu viel zuzuweisen
  • Cgroups erzwingen das Limit
  • Der Prozess wird vom OOM‑Killer beendet

Dies ist ein sehr häufiges Produktionsproblem.

4-7. Zusammenfassung: Heap‑Überprüfung ist der erste zwingende Schritt

Heap‑Engpässe erfordern je nach Ursache sehr unterschiedliche Lösungen. Beginnen Sie mit einem Überblick, bestehend aus:

  • Der aktuellen Heap‑Größe
  • Der tatsächlichen Nutzung
  • Visualisierung mittels Tools

5. Lösung #1: Heap‑Größe erhöhen

Die direkteste Reaktion auf einen „java heap space“-Fehler ist die Erhöhung der Heap‑Größe. Wenn die Ursache ein einfacher Speicherengpass ist, kann das angemessene Vergrößern des Heaps das normale Verhalten wiederherstellen.

Allerdings ist es beim Erhöhen des Heaps wichtig, sowohl die korrekten Konfigurationsmethoden als auch die wichtigsten Vorsichtsmaßnahmen zu verstehen. Falsche Einstellungen können zu Leistungsverschlechterungen oder anderen OOM‑Problemen (Out‑Of‑Memory) führen.

5-1. Heap‑Größe über die Befehlszeile erhöhen

Wenn Sie eine Java‑Anwendung als JAR starten, ist die einfachste Methode, -Xms und -Xmx anzugeben:

■ Beispiel: Initial 512 MB, Max 2 GB

java -Xms512m -Xmx2g -jar app.jar
  • -Xms … die beim JVM‑Start reservierte anfängliche Heap‑Größe
  • -Xmx … die maximale Heap‑Größe, die die JVM nutzen kann

In vielen Fällen hilft das Setzen von -Xms und -Xmx auf denselben Wert, den Aufwand durch Heap‑Neugrößung zu reduzieren.

Beispiel:

java -Xms2g -Xmx2g -jar app.jar

5-2. Konfiguration für permanente Server‑Apps (Tomcat / Jetty usw.)

Für Web‑Anwendungen setzen Sie diese Optionen in den Start‑Skripten des Anwendungsservers.

■ Tomcat (Linux)

Setzen Sie in setenv.sh:

export CATALINA_OPTS="$CATALINA_OPTS -Xms512m -Xmx2048m"

■ Tomcat (Windows)

Setzen Sie in setenv.bat:

set CATALINA_OPTS=-Xms512m -Xmx2048m

■ Jetty

Fügen Sie Folgendes zu start.ini oder jetty.conf hinzu:

--exec
-Xms512m
-Xmx2048m

Da Web‑Apps je nach Traffic stark im Speicherverbrauch schwanken können, sollte die Produktion im Allgemeinen mehr Puffer als Testumgebungen haben.

5-3. Heap‑Einstellungen für Spring‑Boot‑Apps

Wenn Sie Spring Boot als Fat‑JAR ausführen, gelten dieselben Grundlagen:

java -Xms1g -Xmx2g -jar spring-app.jar

Spring Boot neigt dazu, mehr Speicher zu verbrauchen als ein einfaches Java‑Programm, da es beim Start viele Klassen und Konfigurationen lädt.

Es verbraucht oft mehr Speicher als eine typische Java‑Anwendung.

5-4. Heap‑Einstellungen in Docker / Kubernetes (Wichtig)

Bei Java in Containern müssen Sie vorsichtig sein, da Container‑Limits und die JVM‑Heap‑Berechnung miteinander interagieren.

■ Empfohlenes Beispiel (Docker)

docker run --memory=1g \
  -e JAVA_OPTS="-Xms512m -Xmx800m" \
  my-java-app

■ Warum Sie -Xmx explizit setzen müssen

Wenn Sie -Xmx in Docker nicht angeben:

  • Die JVM bestimmt die Heap‑Größe basierend auf dem physischen Speicher der Host‑Maschine, nicht dem Container
  • Sie könnte versuchen, mehr Speicher zuzuweisen, als der Container zulässt
  • Sie stößt auf das cgroup‑Speicherlimit und der Prozess wird vom OOM‑Killer beendet

Da dies ein sehr häufiges Produktionsproblem ist, sollten Sie immer -Xmx in Container‑Umgebungen setzen.

5-5. Beispiele für Heap‑Einstellungen in CI/CD‑ und Cloud‑Umgebungen

In cloud‑basierten Java‑Umgebungen ist eine gängige Faustregel, den Heap basierend auf dem verfügbaren Speicher zu setzen:

Total MemoryRecommended Heap (Approx.)
1GB512–800MB
2GB1.2–1.6GB
4GB2–3GB
8GB4–6GB

※ Lassen Sie den verbleibenden Speicher für das Betriebssystem, den GC‑Overhead und die Thread‑Stacks.

In Cloud‑Umgebungen kann der Gesamtspeicher begrenzt sein. Wenn Sie den Heap ohne Planung vergrößern, kann die gesamte Anwendung instabil werden.

5-6. Behebt das Vergrößern des Heaps immer das Problem? → Es gibt Grenzen

Das Vergrößern der Heap‑Größe kann den Fehler vorübergehend beseitigen, löst jedoch nicht Fälle wie:

  • Ein Speicherleck liegt vor
  • Eine Collection wächst unendlich weiter
  • Große Datenmengen werden massenhaft verarbeitet
  • Die Anwendung hat ein inkorrektes Design

Betrachten Sie das Vergrößern des Heaps daher als Notmaßnahme und stellen Sie sicher, dass Sie mit Code‑Optimierung und der Überprüfung Ihres Datenverarbeitungsdesigns nachhaken, worauf wir im nächsten Abschnitt eingehen werden.

6. Lösung #2: Optimieren Sie Ihren Code

Das Vergrößern der Heap‑Größe kann eine wirksame Abhilfe sein, aber wenn die Grundursache in Ihrer Code‑Struktur oder der Art und Weise, wie Sie Daten verarbeiten, liegt, wird der Fehler „java heap space“ früher oder später zurückkehren.

In diesem Abschnitt behandeln wir gängige, realitätsnahe Codierungsmuster, die Speicher verschwenden, sowie konkrete Ansätze zu deren Verbesserung.

6-1. Überdenken Sie die Verwendung von Collections

Java-Collections (List, Map, Set usw.) sind praktisch, aber unachtsamer Gebrauch kann leicht die Hauptursache für Speicherwachstum werden.

■ Muster ①: List / Map wächst unbegrenzt

Ein häufiges Beispiel:

List<String> logs = new ArrayList<>();

while (true) {
    logs.add(fetchLog());   // ← grows forever
}

Collections mit keiner klaren Abbruchbedingung oder Obergrenze werden in langlebigen Umgebungen zuverlässig den Heap belasten.

● Verbesserungen
  • Verwenden Sie eine begrenzte Collection (z. B. die Größe begrenzen und alte Einträge verwerfen)
  • Leeren Sie periodisch Werte, die Sie nicht mehr benötigen
  • Wenn Sie eine Map als Cache verwenden, nutzen Sie einen Cache mit Eviction → Guava Cache oder Caffeine sind gute Optionen

■ Muster ②: Keine Anfangskapazität festlegen

ArrayList und HashMap wachsen automatisch, wenn sie die Kapazität überschreiten, aber dieses Wachstum beinhaltet: ein neues Array allokieren → kopieren → das alte Array verwerfen.

Beim Umgang mit großen Datensätzen ist das Weglassen einer Anfangskapazität ineffizient und kann Speicher verschwenden.

● Verbesserungsbeispiel
List<String> items = new ArrayList<>(10000);

Wenn Sie die Größe abschätzen können, ist es besser, sie im Voraus festzulegen.

6-2. Vermeiden Sie die Massenverarbeitung großer Daten (Verarbeitung in Stücken)

Wenn Sie massive Daten auf einmal verarbeiten, geraten Sie leicht in das Worst‑Case‑Szenario: alles landet im Heap → OOM.

■ Schlechtes Beispiel (Eine riesige Datei auf einmal lesen)

String json = Files.readString(Paths.get("large.json"));
Object data = new ObjectMapper().readValue(json, Data.class);

■ Verbesserungen

  • Verwenden Sie Streaming-Verarbeitung (z. B. Jackson Streaming API)
  • Lesen Sie in kleineren Portionen (Batch‑Paging)
  • Verarbeiten Sie Streams sequenziell und behalten Sie nicht den gesamten Datensatz
● Beispiel: Verarbeiten großer JSON mit Jackson Streaming
JsonFactory factory = new JsonFactory();
try (JsonParser parser = factory.createParser(new File("large.json"))) {
    while (!parser.isClosed()) {
        JsonToken token = parser.nextToken();
        // Perform only what you need, and do not retain it in memory
    }
}

6-3. Vermeiden Sie unnötige Objekterstellung

Streams und Lambdas sind praktisch, können jedoch intern eine große Anzahl temporärer Objekte erzeugen.

■ Schlechtes Beispiel (Erstellen einer riesigen Zwischentabelle mit Streams)

List<Result> results = items.stream()
        .map(this::toResult)
        .collect(Collectors.toList());

Wenn items riesig ist, werden viele temporäre Objekte erstellt, und der Heap bläht sich auf.

● Verbesserungen
  • Verarbeiten Sie sequenziell mit einer for‑Schleife
  • Schreiben Sie nur das sofort benötigte (nicht alles behalten)
  • collect() vermeiden oder manuell steuern

6-4. Seien Sie vorsichtig bei der String-Verkettung

Java-Strings sind unveränderlich, sodass jede Verkettung ein neues Objekt erzeugt.

■ Verbesserungen

  • Verwenden Sie StringBuilder für umfangreiche Verkettungen
  • Vermeiden Sie unnötige Verkettungen beim Erzeugen von Logs
    StringBuilder sb = new StringBuilder();
    for (String s : items) {
        sb.append(s);
    }
    

6-5. Nicht zu viele Caches überbauen

Dies ist eine häufige Situation in Web‑Apps und Batch‑Verarbeitung:

  • „Wir haben einen Cache zur Geschwindigkeitssteigerung hinzugefügt.“
  • → aber vergessen, ihn zu leeren
  • → der Cache wächst weiter
  • → Heap‑Mangel → OOM

■ Verbesserungen

  • Setzen Sie TTL (zeitbasierte Ablaufzeit) und eine maximale Größe
  • Die Verwendung von ConcurrentHashMap als Cache‑Ersatz ist riskant
  • Verwenden Sie einen gut verwalteten Cache wie Caffeine, der den Speicher korrekt steuert

6-6. Objekte nicht innerhalb großer Schleifen neu erstellen

■ Schlechtes Beispiel

for (...) {
    StringBuilder sb = new StringBuilder(); // created every iteration
    ...
}

Dies erzeugt mehr temporäre Objekte als nötig.

● Verbesserung
StringBuilder sb = new StringBuilder(); 
for (...) {
    sb.setLength(0);  // reuse
}

6-7. Speicherintensive Arbeit in separate Prozesse aufteilen

Wenn Sie in Java wirklich massive Daten verarbeiten, müssen Sie möglicherweise die Architektur selbst überdenken.

  • ETL in einen dedizierten Batch‑Job auslagern
  • An verteilte Verarbeitung delegieren (Spark oder Hadoop)
  • Dienste aufteilen, um Heap‑Kontention zu vermeiden

6-8. Code‑Optimierung ist ein wichtiger Schritt zur Vermeidung von Wiederholungen

Wenn Sie nur den Heap erhöhen, stoßen Sie irgendwann an das nächste „Limit“, und derselbe Fehler tritt erneut auf.

Um „java heap space“-Fehler grundlegend zu verhindern, müssen Sie:

  • Das Datenvolumen verstehen
  • Objekt‑Erstellungsmuster überprüfen
  • Das Design von Collections verbessern

7. Lösung #3: GC (Garbage Collection) optimieren

Der „java heap space“-Fehler kann nicht nur auftreten, wenn der Heap zu klein ist, sondern auch, wenn GC den Speicher nicht effektiv zurückgewinnen kann und der Heap allmählich gesättigt wird.

Ohne ein Verständnis von GC können Sie leicht Symptome missinterpretieren, wie:
„Speicher sollte verfügbar sein, aber wir erhalten weiterhin Fehler“, oder „Das System wird extrem langsam“.

Dieser Abschnitt erklärt den grundlegenden GC‑Mechanismus in Java und praktische Optimierungspunkte, die in realen Einsätzen helfen.

7-1. Was ist GC (Garbage Collection)?

GC ist der Mechanismus von Java, um automatisch Objekte zu verwerfen, die nicht mehr benötigt werden.
Der Java‑Heap ist grob in zwei Generationen unterteilt, und GC verhält sich in jeder unterschiedlich.

● Young‑Generation (kurzlebige Objekte)

  • Eden / Survivor (S0, S1)
  • Temporäre Daten, die lokal erstellt werden, usw.
  • GC findet häufig statt, ist aber leichtgewichtig

● Old‑Generation (langlebige Objekte)

  • Aus der Young‑Generation beförderte Objekte
  • GC ist intensiver; wenn er häufig auftritt, kann die Anwendung „einfrieren“

In vielen Fällen tritt „java heap space“ letztlich auf, wenn die Old‑Generation voll ist.

7-2. GC‑Typen und Eigenschaften (Wie man wählt)

Java bietet mehrere GC‑Algorithmen.
Die richtige Wahl für Ihre Arbeitslast kann die Leistung erheblich verbessern.

● ① G1GC (Standard seit Java 9)

  • Teilt den Heap in kleine Regionen und gibt sie schrittweise zurück
  • Kann Stop‑the‑World‑Pausen kürzer halten
  • Ideal für Web‑Apps und Business‑Systeme

→ Im Allgemeinen ist G1GC eine sichere Standardwahl

● ② Parallel GC (Gut für throughput‑intensive Batch‑Jobs)

  • Parallelisiert und schnell
  • Aber Pausenzeiten können länger werden
  • Oft vorteilhaft für CPU‑intensive Batch‑Verarbeitung

● ③ ZGC (Niedrig‑Latenz‑GC mit Millisekunden‑Pausen)

  • Verfügbar seit Java 11+
  • Für latenzsensible Anwendungen (Game‑Server, HFT)
  • Effektiv selbst bei großen Heaps (Zehnen von GB)

● ④ Shenandoah (Niedrig‑Latenz‑GC)

  • Oft mit Red‑Hat‑Distributionen verbunden
  • Kann Pausenzeiten aggressiv minimieren
  • Auch in einigen Builds wie AWS Corretto verfügbar

7-3. Wie man GC explizit umschaltet

G1GC ist in vielen Setups der Standard, aber Sie können je nach Ziel einen GC‑Algorithmus angeben:

# G1GC
java -XX:+UseG1GC -jar app.jar

# Parallel GC
java -XX:+UseParallelGC -jar app.jar

# ZGC
java -XX:+UseZGC -jar app.jar

Because the GC algorithm can drastically change heap behavior and pause time, production systems often set this explicitly.

7-4. GC-Protokolle ausgeben und Probleme visuell untersuchen

Es ist entscheidend zu verstehen, wie viel Speicher der GC zurückgewinnt und wie oft Stop-the-World-Pausen auftreten.

● Grundlegende GC-Logging-Konfiguration

java \
  -Xms1g -Xmx1g \
  -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps \
  -Xloggc:gc.log \
  -jar app.jar

Durch die Untersuchung von gc.log können Sie klare Anzeichen von Heap-Druck erkennen, wie zum Beispiel:

  • Zu viele Young GCs
  • Die Old-Generation nimmt nie ab
  • Full GC tritt häufig auf
  • Jeder GC gewinnt eine ungewöhnlich kleine Menge zurück

7-5. Fälle, in denen GC-Latenz „java heap space“ auslöst

Wenn Heap-Druck durch Muster wie die folgenden verursacht wird, wird das GC-Verhalten zu einem entscheidenden Hinweis.

● Symptome

  • Die Anwendung friert plötzlich ein
  • GC läuft für Sekunden bis zu mehreren zehn Sekunden
  • Die Old-Generation wächst weiter
  • Full GC nimmt zu und schließlich tritt OOM auf

Dies weist auf einen Zustand hin, in dem GC sich sehr bemüht, aber nicht genug Speicher zurückgewinnen kann, bevor das Limit erreicht wird.

■ Häufige Grundursachen

  • Speicherlecks
  • Sammlungen dauerhaft behalten
  • Objekte leben zu lange
  • Aufblähung der Old-Generation

In diesen Fällen kann die Analyse von GC-Protokollen Ihnen helfen, Lecksignale oder Lastspitzen zu bestimmten Zeitpunkten zu identifizieren.

7-6. Wichtige Punkte beim Tuning von G1GC

G1GC ist standardmäßig stark, aber ein Tuning kann es noch stabiler machen.

● Häufige Parameter

-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=8m
-XX:InitiatingHeapOccupancyPercent=45
  • MaxGCPauseMillis → Zielpausenzeit (z. B. 200 ms)
  • G1HeapRegionSize → Regiongröße, die zur Aufteilung des Heaps verwendet wird
  • InitiatingHeapOccupancyPercent → Der Prozentsatz der Old-Gen-Auslastung, der einen GC-Zyklus auslöst

Allerdings sind in vielen Fällen die Standardwerte ausreichend, ändern Sie diese also nur, wenn ein klarer Bedarf besteht.

7-7. Zusammenfassung des GC-Tunings

GC-Optimierungen helfen Ihnen, Faktoren zu visualisieren, die nicht offensichtlich sind, wenn man nur die Heap-Größe erhöht:

  • Objektlebensdauern
  • Muster der Collection-Nutzung
  • Ob ein Speicherleck existiert
  • Wo sich der Heap-Druck konzentriert

Deshalb ist das GC-Tuning ein äußerst wichtiger Prozess zur Minderung von „java heap space“.

8. Lösung #4: Speicherlecks erkennen

Wenn der Fehler trotz Erhöhung des Heaps und Optimierung des Codes weiterhin auftritt, ist der wahrscheinlichste Verdächtige ein Speicherleck.

Viele gehen davon aus, dass Java gegenüber Speicherlecks resistent ist, weil es einen GC gibt, aber in der Praxis sind Speicherlecks eine der problematischsten und am häufigsten wiederkehrenden Ursachen in realen Umgebungen.

Hier konzentrieren wir uns auf praktische Schritte, die Sie sofort anwenden können, von der Verständnis von Lecks bis zur Nutzung von Analysewerkzeugen wie VisualVM und Eclipse MAT.

8-1. Was ist ein Speicherleck? (Ja, es passiert in Java)

Ein Java‑Speicherleck ist:

Ein Zustand, in dem Referenzen zu unnötigen Objekten verbleiben und den GC daran hindern, sie zurückzugewinnen.

Selbst bei Garbage Collection treten Lecks häufig auf, wenn:

  • Objekte in static-Feldern gehalten werden
  • Dynamisch registrierte Listener nie abgemeldet werden
  • Collections weiter wachsen und Referenzen behalten
  • ThreadLocal‑Werte unerwartet bestehen bleiben
  • Framework‑Lebenszyklen nicht mit Ihrem Objektlebenszyklus übereinstimmen

Daher sind Lecks absolut eine mögliche Normalität.

8-2. Typische Muster von Speicherlecks

● ① Collection-Wachstum (am häufigsten)

Kontinuierliches Hinzufügen zu List/Map/Set ohne Entfernen von Einträgen. In geschäftlichen Java‑Systemen stammt ein großer Teil der OOM‑Vorfälle aus diesem Muster.

● ② Objekte in statischen Variablen halten

private static List&lt;User&gt; cache = new ArrayList&lt;&gt;();

Dies wird häufig zum Ausgangspunkt eines Lecks.

● ③ Vergessen, Listener / Callbacks abzumelden

Referenzen bleiben im Hintergrund über GUI, Beobachter, Event‑Listener usw. erhalten.

● ④ Missbrauch von ThreadLocal

In Thread‑Pool‑Umgebungen können ThreadLocal‑Werte länger als beabsichtigt bestehen bleiben.

● ⑤ Von externen Bibliotheken zurückgehaltene Referenzen

Einige „versteckte Speicher“ sind schwer aus dem Anwendungscode zu verwalten, wodurch eine toolbasierte Analyse unerlässlich wird.

8-3. Prüfstellen, um „Anzeichen“ eines Speicherlecks zu erkennen

Wenn Sie das Folgende sehen, sollten Sie stark ein Speicherleck vermuten:

  • Nur die Old-Generation steigt stetig
  • Full GC wird häufiger
  • Speicher nimmt selbst nach Full GC kaum ab
  • Heap-Nutzung steigt mit der Laufzeit
  • Nur die Produktion stürzt nach langen Laufzeiten ab

Diese sind viel leichter zu verstehen, sobald sie mit Tools visualisiert werden.

8-4. Tool #1: Lecks visuell mit VisualVM prüfen

VisualVM wird in manchen Setups oft mit dem JDK gebündelt und ist als erstes Tool sehr zugänglich.

● Was Sie mit VisualVM tun können

  • Echtzeit-Überwachung der Speichernutzung
  • Bestätigung des Wachstums der Old-Generation
  • GC-Häufigkeit
  • Thread-Überwachung
  • Heap-Dumps erfassen

● Wie man einen Heap-Dump erfasst

In VisualVM öffnen Sie den Reiter „Monitor“ und klicken auf die Schaltfläche „Heap Dump“.

Sie können den erfassten Heap-Dump dann direkt in Eclipse MAT für eine tiefere Analyse übergeben.

8-5. Tool #2: Tiefenanalyse mit Eclipse MAT (Memory Analyzer Tool)

Wenn es ein branchenstandard‑Tool für die Analyse von Java‑Speicherlecks gibt, dann ist es Eclipse MAT.

● Was MAT Ihnen zeigen kann

  • Welche Objekte den meisten Speicher verbrauchen
  • Welche Referenzpfade Objekte am Leben erhalten
  • Warum Objekte nicht freigegeben werden
  • Aufblähung von Collections
  • Automatische „Leak Suspects“-Berichte

● Grundlegende Analyse‑Schritte

  1. Öffnen Sie den Heap-Dump (*.hprof)
  2. Führen Sie den „Leak Suspects Report“ aus
  3. Suchen Sie Collections, die große Mengen Speicher behalten
  4. Überprüfen Sie den Dominator‑Baum, um „Eltern“-Objekte zu identifizieren
  5. Verfolgen Sie den Referenzpfad („Path to GC Root“)

8-6. Wenn Sie den Dominator‑Baum verstehen, beschleunigt sich die Analyse dramatisch

Der Dominator‑Baum hilft Ihnen, Objekte zu identifizieren, die große Teile der Speichernutzung dominieren (kontrollieren).

Beispiele umfassen:

  • Eine massive ArrayList
  • Eine HashMap mit einer enormen Anzahl von Schlüsseln
  • Ein Cache, der nie freigegeben wird
  • Ein Singleton, gehalten durch static

Das Auffinden dieser kann die Zeit zur Lokalisierung des Lecks drastisch reduzieren.

8-7. Wie man einen Heap-Dump erfasst (Kommandozeile)

Sie können auch einen Heap-Dump mit jmap erfassen:

jmap -dump:format=b,file=heap.hprof <PID>

Sie können die JVM auch so konfigurieren, dass sie den Heap automatisch dumpen lässt, wenn ein OOM auftritt:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof

Dies ist für die Untersuchung von Produktionsvorfällen unerlässlich.

8-8. Eine echte Leckbehebung erfordert Code‑Änderungen

Wenn ein Leck existiert, sind Maßnahmen wie:

  • Erhöhung der Heap‑Größe
  • GC‑Feinabstimmung

nur temporäre lebenserhaltende Maßnahmen.

Letztendlich benötigen Sie Design‑Änderungen wie:

  • Den Teil zu beheben, der Referenzen unbegrenzt hält
  • Das Collection‑Design zu überarbeiten
  • Übermäßigen Einsatz von static zu vermeiden
  • Implementierung von Cache‑Eviction und Aufräumen

8-9. Wie man „Heap‑Mangel“ von „Speicherleck“ unterscheidet

● Im Fall eines Heap‑Mangels

  • OOM tritt schnell auf, wenn das Datenvolumen steigt
  • Es skaliert mit der Arbeitslast
  • Erhöhung des Heaps stabilisiert das System

● Im Fall eines Speicherlecks

  • OOM tritt nach langer Laufzeit auf
  • Mit steigenden Anfragen verschlechtert sich die Leistung allmählich
  • Speicher nimmt selbst nach Full GC kaum ab
  • Erhöhung des Heaps löst das Problem nicht

8-10. Zusammenfassung: Wenn Heap‑Tuning OOM nicht behebt, vermuten Sie ein Leck

Unter den „java heap space“-Problemen ist die zeitaufwändigste Ursache, die zu identifizieren ist, oft ein Speicherleck.

Aber mit VisualVM + Eclipse MAT ist es oft möglich, innerhalb von Minuten zu entdecken:

  • Objekte, die den meisten Speicher verbrauchen
  • Die Root‑Referenzen, die sie am Leben erhalten
  • Die Quelle der Collection‑Aufblähung

9. „java heap space“-Probleme in Docker / Kubernetes und deren Behebung

Modern Java‑Anwendungen laufen zunehmend nicht nur in On‑Premise‑Umgebungen, sondern auch auf Docker und Kubernetes (K8s).
Allerdings verwenden Container‑Umgebungen ein anderes Speicher‑Modell als der Host, wodurch es für Java‑Entwickler viele leicht misszuverstehende Punkte gibt und Fehler wie „java heap space“ oder OOMKilled (erzwungene Container‑Beendigung) häufig auftreten können.

Dieser Abschnitt fasst die containerspezifische Speicherverwaltung und die Einstellungen zusammen, die Sie im Echt‑Betrieb kennen müssen.

9-1. Warum Heap‑Space‑Fehler in Containern so häufig sind

Der Grund ist einfach:

Java erkennt Container‑Speicher‑Limits nicht immer korrekt.

● Ein verbreitetes Missverständnis

„Da ich ein Docker‑Speicher‑Limit --memory=512m gesetzt habe, sollte Java innerhalb von 512 MB laufen.“

→ In der Praxis kann diese Annahme falsch sein.

Bei der Entscheidung über die Heap‑Größe kann Java den physischen Speicher des Hosts anstatt der Container‑Limits heranziehen.

Als Ergebnis:

  • Java entscheidet „Der Host hat reichlich Speicher“
  • Es versucht, einen größeren Heap zu reservieren
  • Sobald das Container‑Limit überschritten wird, greift der OOM‑Killer und der Prozess wird zwangsweise beendet

9-2. Verbesserungen in Java 8u191+ und Java 11+

Ab bestimmten Java‑8‑Updates und in Java 11+ wurde UseContainerSupport eingeführt.

● Verhalten in Containern

  • Erkennt cgroup‑basierte Limits
  • Berechnet automatisch die Heap‑Größe innerhalb dieser Limits

Das Verhalten variiert jedoch weiterhin je nach Version, daher wird für die Produktion eine explizite Konfiguration empfohlen.

9-3. Explizites Setzen der Heap‑Größe in Containern (Erforderlich)

● Empfohlenes Start‑Muster

docker run \
  --memory=1g \
  -e JAVA_OPTS="-Xms512m -Xmx800m" \
  my-java-app

Wichtige Punkte:

  • Container‑Speicher: 1 GB
  • Java‑Heap: innerhalb von 800 MB halten
  • Der Rest wird für Thread‑Stacks und nativen Speicher verwendet

● Schlechtes Beispiel (sehr verbreitet)

docker run --memory=1g my-java-app   # no -Xmx

→ Java kann den Heap basierend auf dem Host‑Speicher reservieren, und sobald er 1 GB überschreitet, erhalten Sie OOMKilled.

9-4. Fallen bei Speicher‑Einstellungen in Kubernetes (K8s)

In Kubernetes ist resources.limits.memory entscheidend.

● Pod‑Beispiel

resources:
  limits:
    memory: "1024Mi"
  requests:
    memory: "512Mi"

In diesem Fall ist es in der Regel sicherer, -Xmx bei etwa 800 MB bis 900 MB zu belassen.

● Warum niedriger als das Limit setzen?

Weil Java mehr als nur den Heap nutzt:

  • Nativer Speicher
  • Thread‑Stacks (Hunderte KB × Anzahl der Threads)
  • Metaspace
  • GC‑Worker‑Overhead
  • JIT‑kompilierter Code
  • Bibliotheks‑Laden

Zusammen können diese leicht 100–300 MB verbrauchen.

In der Praxis gilt häufig die Regel:

Wenn das Limit = X ist, setzen Sie -Xmx auf etwa X × 0,7 bis 0,8 für Sicherheit.

9-5. Automatischer Heap‑Prozentsatz in Java 11+ (MaxRAMPercentage)

In Java 11 kann die Heap‑Größe automatisch nach Regeln wie folgt berechnet werden:

● Standard‑Einstellungen

-XX:MaxRAMPercentage=25
-XX:MinRAMPercentage=50

Bedeutet:

  • Der Heap ist auf 25 % des verfügbaren Speichers begrenzt
  • In kleinen Umgebungen kann er mindestens 50 % des Speichers als Heap nutzen

● Empfohlene Einstellung

In Containern ist es oft sicherer, MaxRAMPercentage explizit zu setzen:

JAVA_OPTS="-XX:MaxRAMPercentage=70"

9-6. Warum OOMKilled in Containern so oft passiert (Real‑World‑Muster)

Ein häufiges Produktions‑Muster:

  1. K8s‑Speicher‑Limit = 1 GB
  2. Kein -Xmx konfiguriert
  3. Java bezieht den Host‑Speicher und versucht, einen Heap > 1 GB zu reservieren
  4. Der Container wird zwangsweise beendet → OOMKilled

Beachten Sie, dass dies nicht unbedingt ein java heap space (OutOfMemoryError)‑Ereignis ist – es handelt sich um eine container‑seitige OOM‑Beendigung.

9-7. Containerspezifische Checkpoints mittels GC‑Logs und Metriken

In Container‑Umgebungen sollten Sie besonders auf Folgendes achten:

  • Ob die Pod‑Neustarts zunehmen
  • Ob OOMKilled‑Ereignisse protokolliert werden
  • Ob die Old‑Generation weiter wächst
  • Ob die GC‑Rückgewinnung zu bestimmten Zeitpunkten stark abfällt
  • Ob nativer (non‑heap) Speicher ausgeht

Prometheus + Grafana macht das Visualisieren deutlich einfacher.

9-8. Zusammenfassung: „Explizite Einstellungen“ sind die Vorgabe in Containern

  • --memory allein führt Java möglicherweise nicht dazu, den Heap korrekt zu berechnen
  • Immer -Xmx setzen
  • Spielraum für nativen Speicher und Thread‑Stacks lassen
  • Werte unter den Kubernetes‑Speicherlimits festlegen
  • Ab Java 11 kann MaxRAMPercentage nützlich sein

10. Anti‑Patterns, die vermieden werden sollten (Schlechter Code / Schlechte Einstellungen)

Der Fehler „java heap space“ tritt nicht nur bei wirklich zu wenig Heap auf, sondern auch, wenn bestimmte gefährliche Programmiermuster oder falsche Konfigurationen verwendet werden.

Hier fassen wir häufige Anti‑Patterns zusammen, die in der Praxis immer wieder auftauchen.

10-1. Unbegrenzte Collections unbegrenzt wachsen lassen

Eines der häufigsten Probleme ist Collection‑Bloat.

● Schlechtes Beispiel: Hinzufügen zu einer Liste ohne Begrenzung

List<String> logs = new ArrayList<>();
while (true) {
    logs.add(getMessage());  // ← grows forever
}

Bei langer Laufzeit kann das allein leicht zu einem OOM führen.

● Warum das gefährlich ist

  • Der GC kann den Speicher nicht zurückgewinnen, und die Old‑Generation bläht sich auf
  • Full‑GC wird häufig, wodurch die Anwendung eher einfriert
  • Das Kopieren riesiger Objektmengen erhöht die CPU‑Last

● Wie man das vermeidet

  • Eine Größenbegrenzung setzen (z. B. einen LRU‑Cache)
  • Periodisch leeren
  • Daten nicht unnötig behalten

10-2. Riesige Dateien oder Daten auf einmal laden

Ein häufiger Fehler bei Batch‑ und Server‑Side‑Verarbeitung.

● Schlechtes Beispiel: Ein riesiges JSON auf einmal einlesen

String json = Files.readString(Paths.get("large.json"));
Data d = mapper.readValue(json, Data.class);

● Was schiefgeht

  • Sowohl der vor‑Parse‑String als auch die nach‑Parse‑Objekte bleiben im Speicher
  • Eine 500 MB‑Datei kann leicht das Doppelte oder mehr an Speicher verbrauchen
  • Zusätzliche Zwischenobjekte werden erzeugt, und der Heap wird erschöpft

● Wie man das vermeidet

  • Streaming verwenden (sequentielle Verarbeitung)
  • In Chunks statt als Bulk‑Ladevorgang einlesen
  • Den gesamten Datensatz nicht im Speicher behalten

10-3. Daten weiterhin in statischen Variablen halten

● Schlechtes Beispiel

public class UserCache {
    private static Map<String, User> cache = new HashMap<>();
}

● Warum das gefährlich ist

  • static lebt solange, wie die JVM läuft
  • Wird es als Cache verwendet, können Einträge nie freigegeben werden
  • Referenzen bleiben bestehen und bilden einen Nährboden für Memory‑Leaks

● Wie man das vermeidet

  • static nur sparsam einsetzen
  • Ein dediziertes Cache‑Framework nutzen (z. B. Caffeine)
  • TTL und eine maximale Größenbegrenzung festlegen

10-4. Übermäßiger Einsatz von Stream / Lambda und Erzeugen riesiger Zwischennlisten

Die Stream‑API ist praktisch, kann aber intern Zwischennummern erzeugen und den Speicher belasten.

● Schlechtes Beispiel (collect erzeugt eine massive Zwischennliste)

List<Item> result = items.stream()
        .map(this::convert)
        .collect(Collectors.toList());

● Wie man das vermeidet

  • Sequenziell mit einer for‑Schleife verarbeiten
  • Unnötige Zwischennlisten vermeiden
  • Bei großen Datenmengen den Einsatz von Stream in diesem Teil überdenken

10-5. Massive String‑Verkettung mit dem +‑Operator

Da Strings unveränderlich sind, erzeugt jede Verkettung ein neues String‑Objekt.

● Schlechtes Beispiel

String result = "";
for (String s : list) {
    result += s;
}

● Was falsch ist

  • Bei jedem Durchlauf wird ein neuer String erstellt
  • Eine enorme Anzahl von Instanzen entsteht, was den Speicher belastet

● Wie man das vermeidet

StringBuilder sb = new StringBuilder();
for (String s : list) {
    sb.append(s);
}

10-6. Zu viele Caches erstellen und nicht verwalten

● Schlechte Beispiele

  • API‑Antworten unbegrenzt in einer Map speichern
  • Bilder oder Dateidaten kontinuierlich cachen
  • Kein Steuerungsmechanismus wie LRU

● Warum das riskant ist

  • Der Cache wächst mit der Zeit
  • Nicht zurückgewonnener Speicher nimmt zu
  • Das wird fast immer zu einem Produktionsproblem führen

● Wie man das vermeidet

  • Cache‑Größe begrenzen (z. B. LRU, LFU)
  • Ablaufzeiten (TTL) definieren
  • Regelmäßige Bereinigung bzw. Monitoring einführen

  • Verwenden Sie Caffeine / Guava Cache
  • Setzen Sie eine maximale Größe
  • Konfigurieren Sie TTL (Ablauf)

10-7. Protokolle oder Statistiken kontinuierlich im Speicher behalten

● Schlechtes Beispiel

List<String> debugLogs = new ArrayList<>();
debugLogs.add(message);

In der Produktion sollten Protokolle in Dateien oder Logsysteme geschrieben werden. Sie im Speicher zu behalten ist riskant.

10-8. Nicht-Angabe von -Xmx in Docker-Containern

Dies macht einen großen Teil moderner Heap-bezogener Vorfälle aus.

● Schlechtes Beispiel

docker run --memory=1g my-app

● Was ist falsch

  • Java kann den Heap basierend auf dem Host‑Speicher automatisch dimensionieren
  • Sobald er das Container‑Limit überschreitet, erhalten Sie OOMKilled

● Wie man es vermeidet

docker run --memory=1g -e JAVA_OPTS="-Xmx700m"

10-9. Übermäßiges Tuning der GC‑Einstellungen

Falsches Tuning kann nach hinten losgehen.

● Schlechtes Beispiel

-XX:MaxGCPauseMillis=10
-XX:G1HeapRegionSize=1m

Extreme Parameter können den GC zu aggressiv machen oder verhindern, dass er mithalten kann.

● Wie man es vermeidet

  • In den meisten Fällen sind Standard‑Einstellungen ausreichend
  • Nur minimal anpassen, wenn ein spezifisches, gemessenes Problem vorliegt

10-10. Zusammenfassung: Die meisten Anti‑Pattern entstehen durch „zu viel speichern“

Was all diese Anti‑Pattern gemeinsam haben, ist:

„Mehr Objekte ansammeln als nötig.“

  • Unbegrenzte Sammlungen
  • Unnötige Aufbewahrung
  • Massenladen
  • statisch‑intensive Designs
  • Cache‑Ausreißer
  • Explodierende Zwischenobjekte

Allein das Vermeiden dieser kann die Anzahl von „java heap space“-Fehlern drastisch reduzieren.

11. Reale Beispiele: Dieser Code ist gefährlich (Typische Muster von Speicherproblemen)

Dieser Abschnitt stellt gefährliche Code‑Beispiele vor, die in realen Projekten häufig vorkommen und oft zu „java heap space“-Fehlern führen, und erklärt für jedes:
„Warum es gefährlich ist“ und „Wie man es behebt.“

In der Praxis treten diese Muster häufig zusammen auf, sodass dieses Kapitel für Code‑Reviews und Incident‑Untersuchungen äußerst nützlich ist.

11-1. Massenladen großer Daten

● Schlechtes Beispiel: Alle Zeilen einer riesigen CSV lesen

List&lt;String&gt; lines = Files.readAllLines(Paths.get("big.csv"));

● Warum es gefährlich ist

  • Je größer die Datei, desto größer der Speicherdruck
  • Selbst eine 100 MB‑CSV kann mehr als das Doppelte des Speichers vor/nach dem Parsen verbrauchen
  • Das Behalten massiver Datensätze kann die Old‑Generation erschöpfen

● Verbesserung: Lesen über Stream (sequentielle Verarbeitung)

try (Stream<String> stream = Files.lines(Paths.get("big.csv"))) {
    stream.forEach(line -> process(line));
}

→ Es wird jeweils nur eine Zeile im Speicher gehalten, was dies sehr sicher macht.

11-2. Muster für Sammlungsausdehnung

● Schlechtes Beispiel: Kontinuierliches Ansammeln schwerer Objekte in einer Liste

List<Order> orders = new ArrayList<>();
while (hasNext()) {
    orders.add(fetchNextOrder());
}

● Warum es gefährlich ist

  • Jeder Wachstumsschritt allokiert das interne Array neu
  • Wenn Sie nicht alles behalten müssen, ist das reine Verschwendung
  • Lange Laufzeiten können enormen Old‑Generation‑Speicher verbrauchen

● Verbesserung: Sequenziell verarbeiten + bei Bedarf stapeln

while (hasNext()) {
    Order order = fetchNextOrder();
    process(order);      // process without retaining
}

Oder stapeln Sie es:

List<Order> batch = new ArrayList<>(1000);
while (hasNext()) {
    batch.add(fetchNextOrder());
    if (batch.size() == 1000) {
        processBatch(batch);
        batch.clear();
    }
}

11-3. Zu viele Zwischenobjekte über die Stream‑API erzeugen

● Schlechtes Beispiel: Wiederholte Zwischenergebnisse über map → filter → collect

List<Data> result = list.stream()
        .map(this::convert)
        .filter(d -> d.isValid())
        .collect(Collectors.toList());

● Warum es gefährlich ist

  • Erzeugt intern viele temporäre Objekte
  • Besonders riskant bei riesigen Listen
  • Je tiefer die Pipeline, desto höher das Risiko

● Verbesserung: Verwenden Sie eine for‑Schleife oder sequentielle Verarbeitung

List<Data> result = new ArrayList<>();
for (Item item : list) {
    Data d = convert(item);
    if (d.isValid()) {
        result.add(d);
    }
}

11-4. JSON oder XML auf einmal parsen

● Schlechtes Beispiel

String json = Files.readString(Paths.get("large.json"));
Data data = mapper.readValue(json, Data.class);

● Warum es gefährlich ist

  • Sowohl der rohe JSON‑String als auch die deserialisierten Objekte bleiben im Speicher
  • Bei Dateien der Größe von 100 MB kann der Heap sofort voll werden
  • Ähnliche Probleme können sogar bei Verwendung von Stream‑APIs auftreten, je nach Nutzung

● Verbesserung: Verwenden Sie eine Streaming‑API

JsonFactory factory = new JsonFactory();
try (JsonParser parser = factory.createParser(new File("large.json"))) {
    while (!parser.isClosed()) {
        JsonToken token = parser.nextToken();
        // Process only when needed and do not retain data
    }
}

11-5. Alle Bilder / Binärdaten in den Speicher laden

● Schlechtes Beispiel

byte[] image = Files.readAllBytes(Paths.get("large.png"));

● Warum es gefährlich ist

  • Binärdaten können von Natur aus groß und „schwer“ sein
  • In Bildverarbeitungs‑Apps ist dies eine Hauptursache für OOM

● Verbesserungen

  • Pufferung verwenden
  • Als Stream verarbeiten, ohne die gesamte Datei im Speicher zu behalten
  • Das massenhafte Einlesen von Logs mit mehreren Millionen Zeilen ist ähnlich gefährlich

11-6. Unendliche Retention über statischen Cache

● Schlechtes Beispiel

private static final List<Session> sessions = new ArrayList<>();

● Was ist falsch

  • sessions werden erst freigegeben, wenn die JVM beendet wird
  • Der Cache wächst mit den Verbindungen und führt schließlich zu OOM

● Verbesserungen

  • Einen größenverwalteten Cache verwenden (Caffeine, Guava Cache usw.)
  • Den Lebenszyklus der Session klar verwalten

11-7. Missbrauch von ThreadLocal

● Schlechtes Beispiel

private static final ThreadLocal<SimpleDateFormat> formatter =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

ThreadLocal ist nützlich, kann aber bei Thread‑Pools Werte am Leben erhalten und Lecks verursachen.

● Verbesserungen

  • ThreadLocal nur kurzlebig einsetzen
  • Nur verwenden, wenn es wirklich nötig ist
  • remove() aufrufen, um es zu leeren

11-8. Zu viele Ausnahmen erzeugen

Dies wird oft übersehen, aber Ausnahmen sind sehr schwere Objekte wegen der Erzeugung von Stack‑Traces.

● Schlechtes Beispiel

for (...) {
    try {
        doSomething();
    } catch (Exception e) {
        // log only
    }
}

→ Das Fluten von Ausnahmen kann den Speicher belasten.

● Verbesserungen

  • Verwenden Sie Ausnahmen nicht für normalen Kontrollfluss
  • Ungültige Eingaben per Validierung ablehnen
  • Vermeiden Sie das Werfen von Ausnahmen, sofern nicht nötig

11-9. Zusammenfassung: Gefährlicher Code „still“ frisst Ihren Heap

Das gemeinsame Thema ist:
„Strukturen, die den Heap nach und nach zusammendrücken, übereinander gestapelt.“

  • Massenladen
  • Unendliche Sammlungen
  • Vergessen, abzumelden/zu leeren
  • Zwischengenerierung von Objekten
  • Ausnahmeflutung
  • statische Retention
  • ThreadLocal‑Reste

In allen Fällen wird die Auswirkung bei langen Laufzeiten deutlich.

12. Java‑Speichermanagement‑Best Practices (Wesentlich, um Wiederholungen zu verhindern)

Bisher haben wir die Ursachen von „java heap space“-Fehlern und Gegenmaßnahmen wie Heap‑Erweiterung, Code‑Verbesserungen, GC‑Feinabstimmung und Leak‑Untersuchungen behandelt.

Dieser Abschnitt fasst Best Practices zusammen, die in der Praxis zuverlässig Wiederholungen verhindern.
Betrachten Sie diese als die Mindestregeln, um Java‑Anwendungen stabil zu halten.

12-1. Heap‑Größe explizit festlegen (insbesondere in der Produktion)

Produktions‑Workloads mit den Standardeinstellungen zu betreiben ist riskant.

● Best Practices

  • Setzen Sie -Xms und -Xmx explizit
  • Führen Sie die Produktion nicht mit den Standardeinstellungen aus
  • Halten Sie die Heap‑Größen zwischen Entwicklung und Produktion konsistent (vermeiden Sie unerwartete Unterschiede)

Beispiel:

-Xms1g -Xmx1g

In Docker / Kubernetes müssen Sie den Heap kleiner setzen, um den Container‑Grenzwerten zu entsprechen.

12-2. Richtig überwachen (GC, Speichernutzung, OOM)

Heap‑Probleme sind oft vermeidbar, wenn Sie frühe Warnzeichen erkennen.

● Was zu überwachen ist

  • Nutzung der Old Generation
  • Wachstumstrends der Young Generation
  • Häufigkeit von Full GC
  • GC-Pausenzeit
  • Container OOMKilled-Ereignisse
  • Pod-Neustartzähler (K8s)

● Empfohlene Werkzeuge

  • VisualVM
  • JDK Mission Control
  • Prometheus + Grafana
  • Cloud-Anbieter-Metriken (z. B. CloudWatch)

Ein allmählicher Anstieg der Speichernutzung über lange Laufzeiten ist ein klassisches Anzeichen für ein Leck.

12-3. Verwenden Sie „kontrollierte Caches“

Ein unkontrolliertes Wachstum von Caches ist einer der häufigsten Ursachen für OOM in der Produktion.

● Bewährte Verfahren

  • Verwenden Sie Caffeine / Guava Cache
  • Immer TTL (Ablauf) konfigurieren
  • Eine maximale Größe festlegen (z. B. 1.000 Einträge)
  • Statische Caches nach Möglichkeit vermeiden
    Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build();
    

12-4. Seien Sie vorsichtig beim übermäßigen Einsatz der Stream-API und Lambdas

Bei großen Datensätzen erhöht das Ketten von Stream-Operationen die Anzahl Zwischenerzeugnisse.

● Bewährte Verfahren

  • Ketten Sie map/filter/collect nicht unnötig
  • Verarbeiten Sie riesige Datensätze sequenziell mit einer for-Schleife
  • Seien Sie beim Einsatz von collect sich des Datenvolumens bewusst

Streams sind praktisch, aber nicht immer speichereffizient.

12-5. Wechseln Sie große Dateien / große Daten zu Streaming

Massenverarbeitung ist eine Hauptursache für Heap-Probleme.

● Bewährte Verfahren

  • CSV → Files.lines()
  • JSON → Jackson Streaming
  • DB → Paging
  • API → chunked fetch (Cursor/Paginierung)

Wenn Sie „nicht alles in den Speicher laden“ durchsetzen, verschwinden viele Heap-Probleme.

12-6. Gehen Sie vorsichtig mit ThreadLocal um

ThreadLocal ist mächtig, aber Fehlgebrauch kann schwere Speicherlecks verursachen.

● Bewährte Verfahren

  • Seien Sie besonders vorsichtig, wenn es mit Thread-Pools kombiniert wird
  • Rufen Sie remove() nach der Verwendung auf
  • Speichern Sie keine langlebigen Daten
  • Vermeiden Sie statische ThreadLocal nach Möglichkeit

12-7. Erfassen Sie periodisch Heap-Dumps, um Lecks früh zu erkennen

Für langfristig laufende Systeme (Web‑Apps, Batch‑Systeme, IoT) hilft das regelmäßige Erfassen und Vergleichen von Heap-Dumps, frühe Anzeichen von Lecks zu erkennen.

● Optionen

  • VisualVM
  • jmap
  • -XX:+HeapDumpOnOutOfMemoryError
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=/var/log/heapdump.hprof
    

Automatisches Dumpen bei OOM ist eine unverzichtbare Produktionseinstellung.

12-8. Halten Sie das GC-Tuning minimal

Die Vorstellung, dass „GC-Tuning die Leistung automatisch steigert“, kann gefährlich sein.

● Bewährte Verfahren

  • Beginnen Sie mit Standard‑Einstellungen
  • Nehmen Sie minimale Änderungen nur vor, wenn ein gemessenes Problem vorliegt
  • Verwenden Sie G1GC als Standardwahl
  • In vielen Fällen ist das Erhöhen des Heaps effektiver als Mikrotuning

12-9. Erwägen Sie das Aufteilen der Architektur

Wenn Datenmengen zu groß werden oder die Anwendung zu monolithisch wird und einen riesigen Heap erfordert, benötigen Sie möglicherweise architektonische Verbesserungen:

  • Microservices
  • Aufteilen der Batch‑Datenverarbeitung
  • Entkopplung mit Message Queues (Kafka usw.)
  • Verteilte Verarbeitung (Spark usw.)

Wenn „egal wie viel Heap Sie hinzufügen, es ist nie genug“, vermuten Sie ein architektonisches Problem.

12-10. Zusammenfassung: Java‑Speichermanagement ist eine schichtweise Optimierung

Heap‑Probleme werden selten durch eine einzelne Einstellung oder einen einzelnen Code‑Fix gelöst.

● Wichtigste Erkenntnisse

  • Heap immer explizit festlegen
  • Monitoring ist am wichtigsten
  • Sammel‑Aufblähungen niemals zulassen
  • Streaming für große Daten verwenden
  • Caches korrekt verwalten
  • ThreadLocal vorsichtig einsetzen
  • Lecks bei Bedarf mit Werkzeugen analysieren
  • Container erfordern ein anderes Mindset

Die Befolgung dieser Punkte verhindert mit hoher Sicherheit die meisten „java heap space“-Fehler.

13. Zusammenfassung: Schlüssel­punkte zur Vermeidung von „java heap space“-Fehlern

In diesem Artikel haben wir „java heap space“-Fehler von den Ursachen bis zur Minderung und Verhinderung von Wiederholungen behandelt.

Hier fassen wir das Wesentliche als praktische Zusammenfassung zusammen.

13-1. Das eigentliche Problem ist nicht „Heap ist zu klein“, sondern „Warum geht er leer?“

„java heap space“ ist nicht nur ein einfacher Speicherengpass.

● Die Hauptursache ist typischerweise eine der folgenden

  • Heap-Größe ist zu klein (unzureichende Konfiguration)
  • Massenverarbeitung riesiger Daten (Design‑Problem)
  • Collection‑Aufblähung (fehlendes Löschen/Design)
  • Speicherleck (Referenzen bleiben)
  • Fehlkonfiguration in Containern (Docker/K8s‑spezifisch)

Starten Sie mit: „Warum ist der Heap ausgegangen?“

13-2. Erste Schritte zur Untersuchung

① Heap‑Größe bestätigen

→ Setzen Sie explizit -Xms / -Xmx

② Laufzeit‑Speicherbeschränkungen verstehen

→ In Docker/Kubernetes Limits und Heap‑Größe angleichen
→ Auch -XX:MaxRAMPercentage prüfen

③ GC‑Logs erfassen und inspizieren

→ Wachstum des Old‑Gen und häufige Full‑GCs sind Warnsignale

④ Heap‑Dumps erfassen und analysieren

→ VisualVM / MAT nutzen, um Beweise für Lecks zu erbringen

13-3. Hochriskante Muster, die in der Produktion häufig vorkommen

Wie im gesamten Artikel gezeigt, führen die folgenden Muster häufig zu Vorfällen:

  • Massenverarbeitung riesiger Dateien
  • Hinzufügen zu List/Map ohne Begrenzung
  • Cache‑Ausarten
  • Ansammeln von Daten in static
  • Explosive Zwischenobjekte durch Stream‑Ketten
  • Missbrauch von ThreadLocal
  • Kein Setzen von -Xmx in Docker

Wenn Sie diese im Code oder in den Einstellungen sehen, untersuchen Sie sie zuerst.

13-4. Grundlegende Fixes betreffen Systemdesign und Datenverarbeitung

● Was auf Systemebene zu prüfen ist

  • Große Datenverarbeitung auf Streaming‑Verarbeitung umstellen
  • Caches mit TTL, Größenbegrenzungen und Eviction verwenden
  • Regelmäßiges Speicher‑Monitoring für langlaufende Anwendungen durchführen
  • Frühe Leck‑Anzeichen mit Tools analysieren

● Wenn es immer noch schwierig ist

  • Batch‑ vs. Online‑Verarbeitung trennen
  • Microservices einsetzen
  • Verteilte Verarbeitungsplattformen adoptieren (Spark, Flink usw.)

Architektonische Verbesserungen können erforderlich sein.

13-5. Die drei wichtigsten Botschaften

Wenn Sie sich nur drei Dinge merken:

✔ Heap immer explizit setzen

✔ Nie riesige Daten massenweise verarbeiten

✔ Lecks können ohne Heap‑Dumps nicht bestätigt werden

Nur diese drei können kritische Produktionsvorfälle, die durch „java heap space“ verursacht werden, stark reduzieren.

13-6. Java‑Speichermanagement ist eine Fähigkeit, die echten Vorteil verschafft

Java‑Speichermanagement mag schwer erscheinen, aber wenn Sie es verstehen:

  • Die Vorfall‑Untersuchung wird dramatisch schneller
  • Hochlast‑Systeme können stabil betrieben werden
  • Performance‑Tuning wird genauer
  • Sie werden zu einem Engineer, der sowohl Anwendung als auch Infrastruktur versteht

Es ist keine Übertreibung zu sagen, dass die Systemqualität proportional zum Speicher‑Verständnis ist.

14. FAQ

Abschließend folgt ein praxisnaher Q&A‑Abschnitt, der häufige Fragen rund um „java heap space“ abdeckt.

Damit wird der Artikel ergänzt und ein breiteres Nutzer‑Intent‑Spektrum erfasst.

Q1. Was ist der Unterschied zwischen java.lang.OutOfMemoryError: Java heap space und GC overhead limit exceeded?

● java heap space

  • Tritt auf, wenn der Heap physisch erschöpft ist
  • Oft verursacht durch riesige Daten, Collection‑Aufblähung oder unzureichende Einstellungen

● GC overhead limit exceeded

  • Der GC arbeitet hart, kann aber fast nichts zurückgewinnen
  • Ein Hinweis darauf, dass der GC wegen zu vieler lebender Objekte nicht mehr weiterkommt
  • Deutet häufig auf ein Speicherleck oder hängende Referenzen hin

Ein nützliches mentales Modell:
heap space = bereits die Grenze überschritten,
GC overhead = kurz bevor die Grenze erreicht wird.

Q2. Wenn ich einfach den Heap vergrößere, ist das Problem dann gelöst?

✔ Es kann vorübergehend helfen

✘ Es behebt nicht die Grundursache

  • Wenn der Heap tatsächlich zu klein für Ihre Arbeitslast ist → es hilft
  • Wenn Collections oder Lecks die Ursache sind → das Problem wird wieder auftreten

Ist die Ursache ein Leak, verzögert das Verdoppeln des Heaps nur das nächste OOM.

Q3. Wie stark kann ich den Java‑Heap erhöhen?

● Typischerweise: 50 %–70 % des physischen Speichers

Denn Sie müssen Speicher reservieren für:

  • Native Memory
  • Thread‑Stacks
  • Metaspace
  • GC‑Worker
  • OS‑Prozesse

Besonders in Docker/K8s ist es gängige Praxis, zu setzen:
-Xmx = 70 %–80 % des Container‑Limits.

Q4. Warum bekommt Java OOMKilled in Containern (Docker/K8s)?

● In vielen Fällen, weil -Xmx nicht gesetzt ist

Docker übergibt Container‑Grenzwerte nicht immer sauber an Java, sodass Java den Heap basierend auf dem Host‑Speicher dimensioniert → überschreitet das Limit → OOMKilled.

✔ Lösung

docker run --memory=1g -e JAVA_OPTS="-Xmx800m"

Q5. Gibt es eine einfache Möglichkeit zu erkennen, ob es sich um ein Speicherleck handelt?

✔ Wenn diese zutreffen, ist es sehr wahrscheinlich ein Leak

  • Der Heap‑Verbrauch steigt mit der Laufzeit kontinuierlich
  • Der Speicher nimmt selbst nach einem Full GC kaum ab
  • Der Old‑Gen wächst in einem „Stufen‑“ Muster
  • OOM tritt nach Stunden oder Tagen auf
  • Kurze Läufe sehen normal aus

Allerdings erfordert die endgültige Bestätigung Heap‑Dump‑Analyse (Eclipse MAT).

Q6. Heap‑Einstellungen in Eclipse / IntelliJ werden nicht übernommen

● Häufige Ursachen

  • Sie haben die Run‑Konfiguration nicht bearbeitet
  • Die Standardeinstellungen der IDE haben Vorrang
  • Ein anderes Startskript mit JAVA_OPTS überschreibt Ihre Einstellungen
  • Sie haben vergessen, den Prozess neu zu starten

IDE‑Einstellungen unterscheiden sich, prüfen Sie daher immer das Feld „VM‑Optionen“ in der Run/Debug‑Konfiguration.

Q7. Stimmt es, dass Spring Boot viel Speicher verbraucht?

Ja. Spring Boot verbraucht häufig mehr Speicher wegen:

  • Auto‑Konfiguration
  • Viele Beans
  • Klassenladen in Fat‑JARs
  • Eingebetteter Web‑Server (Tomcat usw.)

Im Vergleich zu einem einfachen Java‑Programm kann es in manchen Fällen etwa 200–300 MB zusätzlich verbrauchen.

Q8. Welchen GC sollte ich verwenden?

In den meisten Fällen ist G1GC die sichere Vorgabe.

● Empfehlungen je nach Workload

  • Web‑Apps → G1GC
  • Durchsatzintensive Batch‑Jobs → Parallel‑GC
  • Ultra‑niedrige Latenz‑Anforderungen → ZGC / Shenandoah

Ohne triftigen Grund wählen Sie G1GC.

Q9. Wie sollte ich den Heap in serverlosen Umgebungen (Cloud Run / Lambda) handhaben?

Serverlose Umgebungen haben enge Speichergrenzen, daher sollten Sie den Heap explizit konfigurieren.

Beispiel (Java 11):

-XX:MaxRAMPercentage=70

Beachten Sie außerdem, dass der Speicher während Cold Starts ansteigen kann, lassen Sie also Spielraum in Ihrer Heap‑Konfiguration.

Q10. Wie kann ich verhindern, dass Java‑Heap‑Probleme wieder auftreten?

Wenn Sie diese drei Regeln strikt befolgen, sinkt das Wiederauftreten drastisch:

✔ Heap explizit setzen

✔ Große Daten per Streaming verarbeiten

✔ GC‑Logs und Heap‑Dumps regelmäßig prüfen

Zusammenfassung: Nutzen Sie das FAQ, um Zweifel zu beseitigen und praktische Speichermaßnahmen anzuwenden

Dieses FAQ behandelt häufige, suchbasierte Fragen zu „java heap space“ mit praktischen Antworten.

Zusammen mit dem Hauptartikel sollte es Ihnen helfen, Java‑Speicherprobleme souverän zu bewältigen und die Systemstabilität in der Produktion deutlich zu verbessern.