- 1 1. Einführung
- 2 2. Was ist der Java‑Heap?
- 2.1 2-1. Gesamtüberblick über Java-Speicherbereiche
- 2.2 2-2. Die Rolle des Heaps
- 2.3 2-3. Was passiert, wenn der Heap voll ist?
- 2.4 2-4. „Einfach den Heap vergrößern“ ist halb richtig und halb falsch
- 2.5 2-5. Heap‑Layout (Eden / Survivor / Old)
- 2.6 2-6. Warum ein Heap‑Mangel bei Anfängern und fortgeschrittenen Entwicklern häufig ist
- 3 3. Häufige Ursachen des “java heap space”-Fehlers
- 3.1 3-1. Speicherdruck durch Laden großer Daten
- 3.2 3-2. Übermäßiges Anhäufen von Daten in Sammlungen
- 3.3 3-3. Speicherlecks (unbeabsichtigte Objektbindung)
- 3.4 3-4. JVM‑Heap‑Größe zu klein (Standardwerte sind klein)
- 3.5 3-5. Langlaufende Muster, bei denen Objekte weiter ansammeln
- 3.6 3-6. Missverständnisse bei Begrenzungen in Containern (Docker / Kubernetes)
- 4 4. Wie man die Heap‑Größe prüft
- 4.1 4-1. Heap‑Größe von der Befehlszeile prüfen
- 4.2 4-2. Heap‑Größe innerhalb eines laufenden Programms prüfen
- 4.3 4-3. Prüfung mit Werkzeugen wie VisualVM oder Mission Control
- 4.4 4-4. Prüfung in IDEs (Eclipse / IntelliJ)
- 4.5 4-5. Prüfung in Anwendungsservern (Tomcat / Jetty)
- 4.6 4-6. Heap‑Überprüfung in Docker / Kubernetes (Wichtig)
- 4.7 4-7. Zusammenfassung: Heap‑Überprüfung ist der erste zwingende Schritt
- 5 5. Lösung #1: Heap‑Größe erhöhen
- 5.1 5-1. Heap‑Größe über die Befehlszeile erhöhen
- 5.2 5-2. Konfiguration für permanente Server‑Apps (Tomcat / Jetty usw.)
- 5.3 5-3. Heap‑Einstellungen für Spring‑Boot‑Apps
- 5.4 5-4. Heap‑Einstellungen in Docker / Kubernetes (Wichtig)
- 5.5 5-5. Beispiele für Heap‑Einstellungen in CI/CD‑ und Cloud‑Umgebungen
- 5.6 5-6. Behebt das Vergrößern des Heaps immer das Problem? → Es gibt Grenzen
- 6 6. Lösung #2: Optimieren Sie Ihren Code
- 6.1 6-1. Überdenken Sie die Verwendung von Collections
- 6.2 6-2. Vermeiden Sie die Massenverarbeitung großer Daten (Verarbeitung in Stücken)
- 6.3 6-3. Vermeiden Sie unnötige Objekterstellung
- 6.4 6-4. Seien Sie vorsichtig bei der String-Verkettung
- 6.5 6-5. Nicht zu viele Caches überbauen
- 6.6 6-6. Objekte nicht innerhalb großer Schleifen neu erstellen
- 6.7 6-7. Speicherintensive Arbeit in separate Prozesse aufteilen
- 6.8 6-8. Code‑Optimierung ist ein wichtiger Schritt zur Vermeidung von Wiederholungen
- 7 7. Lösung #3: GC (Garbage Collection) optimieren
- 8 7-1. Was ist GC (Garbage Collection)?
- 9 7-2. GC‑Typen und Eigenschaften (Wie man wählt)
- 10 7-3. Wie man GC explizit umschaltet
- 11 7-4. GC-Protokolle ausgeben und Probleme visuell untersuchen
- 12 7-5. Fälle, in denen GC-Latenz „java heap space“ auslöst
- 13 7-6. Wichtige Punkte beim Tuning von G1GC
- 14 7-7. Zusammenfassung des GC-Tunings
- 15 8. Lösung #4: Speicherlecks erkennen
- 16 8-1. Was ist ein Speicherleck? (Ja, es passiert in Java)
- 17 8-2. Typische Muster von Speicherlecks
- 18 8-3. Prüfstellen, um „Anzeichen“ eines Speicherlecks zu erkennen
- 19 8-4. Tool #1: Lecks visuell mit VisualVM prüfen
- 20 8-5. Tool #2: Tiefenanalyse mit Eclipse MAT (Memory Analyzer Tool)
- 21 8-6. Wenn Sie den Dominator‑Baum verstehen, beschleunigt sich die Analyse dramatisch
- 22 8-7. Wie man einen Heap-Dump erfasst (Kommandozeile)
- 23 8-8. Eine echte Leckbehebung erfordert Code‑Änderungen
- 24 8-9. Wie man „Heap‑Mangel“ von „Speicherleck“ unterscheidet
- 25 8-10. Zusammenfassung: Wenn Heap‑Tuning OOM nicht behebt, vermuten Sie ein Leck
- 26 9. „java heap space“-Probleme in Docker / Kubernetes und deren Behebung
- 27 9-1. Warum Heap‑Space‑Fehler in Containern so häufig sind
- 28 9-2. Verbesserungen in Java 8u191+ und Java 11+
- 29 9-3. Explizites Setzen der Heap‑Größe in Containern (Erforderlich)
- 30 9-4. Fallen bei Speicher‑Einstellungen in Kubernetes (K8s)
- 31 9-5. Automatischer Heap‑Prozentsatz in Java 11+ (MaxRAMPercentage)
- 32 9-6. Warum OOMKilled in Containern so oft passiert (Real‑World‑Muster)
- 33 9-7. Containerspezifische Checkpoints mittels GC‑Logs und Metriken
- 34 9-8. Zusammenfassung: „Explizite Einstellungen“ sind die Vorgabe in Containern
- 35 10. Anti‑Patterns, die vermieden werden sollten (Schlechter Code / Schlechte Einstellungen)
- 36 10-1. Unbegrenzte Collections unbegrenzt wachsen lassen
- 37 10-2. Riesige Dateien oder Daten auf einmal laden
- 38 10-3. Daten weiterhin in statischen Variablen halten
- 39 10-4. Übermäßiger Einsatz von Stream / Lambda und Erzeugen riesiger Zwischennlisten
- 40 10-5. Massive String‑Verkettung mit dem +‑Operator
- 41 10-6. Zu viele Caches erstellen und nicht verwalten
- 42 10-7. Protokolle oder Statistiken kontinuierlich im Speicher behalten
- 43 10-8. Nicht-Angabe von -Xmx in Docker-Containern
- 44 10-9. Übermäßiges Tuning der GC‑Einstellungen
- 45 10-10. Zusammenfassung: Die meisten Anti‑Pattern entstehen durch „zu viel speichern“
- 46 11. Reale Beispiele: Dieser Code ist gefährlich (Typische Muster von Speicherproblemen)
- 47 11-1. Massenladen großer Daten
- 48 11-2. Muster für Sammlungsausdehnung
- 49 11-3. Zu viele Zwischenobjekte über die Stream‑API erzeugen
- 50 11-4. JSON oder XML auf einmal parsen
- 51 11-5. Alle Bilder / Binärdaten in den Speicher laden
- 52 11-6. Unendliche Retention über statischen Cache
- 53 11-7. Missbrauch von ThreadLocal
- 54 11-8. Zu viele Ausnahmen erzeugen
- 55 11-9. Zusammenfassung: Gefährlicher Code „still“ frisst Ihren Heap
- 56 12. Java‑Speichermanagement‑Best Practices (Wesentlich, um Wiederholungen zu verhindern)
- 57 12-1. Heap‑Größe explizit festlegen (insbesondere in der Produktion)
- 58 12-2. Richtig überwachen (GC, Speichernutzung, OOM)
- 59 12-3. Verwenden Sie „kontrollierte Caches“
- 60 12-4. Seien Sie vorsichtig beim übermäßigen Einsatz der Stream-API und Lambdas
- 61 12-5. Wechseln Sie große Dateien / große Daten zu Streaming
- 62 12-6. Gehen Sie vorsichtig mit ThreadLocal um
- 63 12-7. Erfassen Sie periodisch Heap-Dumps, um Lecks früh zu erkennen
- 64 12-8. Halten Sie das GC-Tuning minimal
- 65 12-9. Erwägen Sie das Aufteilen der Architektur
- 66 12-10. Zusammenfassung: Java‑Speichermanagement ist eine schichtweise Optimierung
- 67 13. Zusammenfassung: Schlüsselpunkte zur Vermeidung von „java heap space“-Fehlern
- 68 13-1. Das eigentliche Problem ist nicht „Heap ist zu klein“, sondern „Warum geht er leer?“
- 69 13-2. Erste Schritte zur Untersuchung
- 70 13-3. Hochriskante Muster, die in der Produktion häufig vorkommen
- 71 13-4. Grundlegende Fixes betreffen Systemdesign und Datenverarbeitung
- 72 13-5. Die drei wichtigsten Botschaften
- 73 13-6. Java‑Speichermanagement ist eine Fähigkeit, die echten Vorteil verschafft
- 74 14. FAQ
- 75 Q1. Was ist der Unterschied zwischen java.lang.OutOfMemoryError: Java heap space und GC overhead limit exceeded?
- 76 Q2. Wenn ich einfach den Heap vergrößere, ist das Problem dann gelöst?
- 77 Q3. Wie stark kann ich den Java‑Heap erhöhen?
- 78 Q4. Warum bekommt Java OOMKilled in Containern (Docker/K8s)?
- 79 Q5. Gibt es eine einfache Möglichkeit zu erkennen, ob es sich um ein Speicherleck handelt?
- 80 Q6. Heap‑Einstellungen in Eclipse / IntelliJ werden nicht übernommen
- 81 Q7. Stimmt es, dass Spring Boot viel Speicher verbraucht?
- 82 Q8. Welchen GC sollte ich verwenden?
- 83 Q9. Wie sollte ich den Heap in serverlosen Umgebungen (Cloud Run / Lambda) handhaben?
- 84 Q10. Wie kann ich verhindern, dass Java‑Heap‑Probleme wieder auftreten?
- 85 Zusammenfassung: Nutzen Sie das FAQ, um Zweifel zu beseitigen und praktische Speichermaßnahmen anzuwenden
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
OutOfMemoryErrortritt 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
OutOfMemoryErrorin 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:
- Die Grundlagen: „Was der Java‑Heap ist“
- Typische Ursachen
- 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
newerstellte 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
List→ sie wächst, ohne geleert zu werden - Verwendung einer
Mapals 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()undclose()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
-Xmsund-Xmxauf 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
-Xmxnicht 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:MaxRAMPercentageangemessen setzen-Xmxan 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
-Xmxexplizit 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 Memory | Recommended Heap (Approx.) |
|---|---|
| 1GB | 512–800MB |
| 2GB | 1.2–1.6GB |
| 4GB | 2–3GB |
| 8GB | 4–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
StringBuilderfü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
ConcurrentHashMapals 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<User> cache = new ArrayList<>();
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
- Öffnen Sie den Heap-Dump (*.hprof)
- Führen Sie den „Leak Suspects Report“ aus
- Suchen Sie Collections, die große Mengen Speicher behalten
- Überprüfen Sie den Dominator‑Baum, um „Eltern“-Objekte zu identifizieren
- 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
HashMapmit 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
staticzu 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
-Xmxauf 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:
- K8s‑Speicher‑Limit = 1 GB
- Kein
-Xmxkonfiguriert - Java bezieht den Host‑Speicher und versucht, einen Heap > 1 GB zu reservieren
- 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
--memoryallein führt Java möglicherweise nicht dazu, den Heap korrekt zu berechnen- Immer
-Xmxsetzen - Spielraum für nativen Speicher und Thread‑Stacks lassen
- Werte unter den Kubernetes‑Speicherlimits festlegen
- Ab Java 11 kann
MaxRAMPercentagenü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
staticlebt 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
staticnur 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<String> 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
sessionswerden 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
-Xmsund-Xmxexplizit - 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üsselpunkte 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
-Xmxin 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.


