Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

|
//

Java Memory Leaks und andere Übeltäter (2.Akt)

2.3.2011 | 6 Minuten Lesezeit

Der erste Teil dieser Blog-Serie Java OutOfMemoryError – Eine Tragödie in sieben Akten hat sich mit der Architektur des Speichers innerhalb einer JVM beschäftigt und aufgezeigt wo und unter welchen Umständen ein java.lang.OutOfMemoryError auftreten kann.

In diesem Blog-Eintrag möchte ich mich im Detail damit beschäftigen, wie es zu diesem Fehler in einer Java-Anwendung kommen kann.

Im vorangegangenen Eintrag haben wir gesehen, dass es völlig unterschiedliche Arten von OutOfMemoryError gibt. Der am häufigsten auftretende Fehler ist allerdings

1Exception in thread "main": java.lang.OutOfMemoryError: Java heap space

Dieser Fehler bedeutet, dass auf dem Heap nicht mehr genug Speicher zur Verfügung war, um eine Speicheranfrage für ein neues Objekt zu erfüllen – sprich: kein neues Objekt mehr auf dem Heap angelegt werden konnte. Da jeder Heap nach der JVM-Spezifikation zwingend über einen Garbage Collector verfügen muss, bedeutet das auch, dass dieser keinen Speicher mehr freiräumen konnte und der Heap komplett mit „lebenden“ Objekten belegt ist.

Um besser zu verstehen, wie solche Situationen entstehen können, möchte ich zunächst beschreiben, was ein „lebendes“ Objekt in Java ist.

In Java werden Objekte auf dem Heap erzeugt und leben so lange, wie sie noch referenziert werden. Der Garbage Collector prüft bei einer GC-Phase, ob ein Objekt noch referenziert ist – ist dies nicht der Fall, wird es vom Garbage Collector als „Müll“ markiert und später aufgeräumt (es gibt andere GC-Algorithmen, wie Copying Collectoren oder Garbage-First Ansätze, diese sind aber für das Verständnis nicht relevant). Dabei ist aber nicht jede Referenz für das Überleben eines Objekts entscheidend, sondern nur so genannten GC Root Referenzen. Gerade im Zusammenhang mit Java Memory Leaks sind GC Roots ein zentrales Konzept, das man verstehen muss, um die kritischen Referenzen auf ein Objekt identifizieren zu können. Garbage Collector Roots sind Objekte, auf die es keine eingehenden Referenzen gibt und die dafür verantwortlich sind, dass referenzierte Objekte im Speicher gehalten werden. Wird ein Objekt weder direkt noch indirekt von einem GC Root referenziert, wird es als „unreachable“ gekennzeichnet und zur Garbage Collection freigegeben. Es gibt vereinfacht drei Arten von Garbage Collection Roots:

•    Temporäre Variablen auf dem Stack eines Threads

•    Statische Variablen von Klassen

•    Spezielle native Referenzen in JNI

Anhand dieses konkreten Beispiels lässt sich das am besten verdeutlichen:

1public class MyFrame extends javax.swing.JFrame {
2 
3  // reachable via Classloader as soon class is loaded
4  public static final ArrayList STATIC = new ArrayList();
5 
6  // as long as the JFrame is not dispose()'d,
7  // it is reachable via a native window
8  private final ArrayList jni = new ArrayList()
9 
10  // while this method is executing parameter is kept reachable,
11  // even if it is not used
12  private void myMethod(ArrayList parameter) {
13 
14    // while this method is running, this list is reachable from the stack
15    ArrayList local = new ArrayList();
16  }
17 
18}

Grundsätzlich kann man drei unterschiedliche Problemfelder im Zusammenhang mit Java OutOfMemoryError Problemen im Heap sehen:

  • Objekte, die noch eine GC Root Referenz haben, aber nie mehr im Anwendungscode genutzt werden. Hier spricht man von Java Memory Leaks.
  • Zu viele Objekte oder zu große Objekte. Bedeutet, dass nicht genug Heap für die Ausführung der Anwendung zur Verfügung steht, weil zu große Objektbäume im Speicher gehalten werden (z.B. Caches).
  • Zu viele temporäre Objekte. Bedeutet, dass temporär bei der Verarbeitung im Java-Code zu viel Speicher benötigt wird.

Java Memory Leaks

Java Memory Leaks entstehen also dann, wenn Objekte noch eine GC Root Referenz haben, aber nie mehr in der Anwendung gebraucht werden. Diese „Loitering Objects“ belegen danach für die komplette Laufzeit der JVM Speicher. Entstehen solche „Objekt-Leichen“ kontinuierlich in der Anwendungslogik, wird der Speicher unweigerlich volllaufen und es kommt zu einem java.lang.OutOfMemoryError. Typische „Problemkinder“ sind beispielsweise statische Collections, die als eine Art Cache genutzt werden. Häufig werden hier Objekte hinzugefügt, aber nie wieder herausgeholt (Hand aufs Herz: Wie oft hat man schon die put() und add() Methoden benutzt und wie oft die remove() Methode?). Die hinzugefügten Objekte werden durch die statischen Collection-Einträge referenziert und können aufgrund der GC Root Referenz (statisch) nicht mehr freigegeben werden.

Im Zusammenhang mit Memory Leaks wird oft auch von so genannten Dominatoren oder Dominator Trees gesprochen. Das Konzept der Dominatoren kommt aus der Graphentheorie und definiert einen Knoten als Dominator eines anderen Knotens, wenn dieser nur durch ihn erreicht werden kann. Auf das Speichermanagement umgesetzt ist also ein Objekt A ein Dominator eines anderen Objekts B, wenn es kein weiteres Objekt C gibt, das eine Referenz auf B hält. Ein Dominator Tree ist dann ein Teilbaum, in dem diese Bedingung vom Wurzelknoten aus für alle Kinder gilt. Wird also die Wurzelreferenz freigegeben, wird auch der ganze Dominator Tree freigeben. Sehr große Dominatorenbäume sind also sehr gute potenzielle Kandidaten bei der Memory-Leak-Suche.

Je nach Erzeugungs-Frequenz und Größe der nicht mehr benötigten Objekte, sowie konfigurierter Größe des Java Heaps, tritt der OutOfMemoryError früher oder später auf. Gerade letztere Variante, so genannte „schleichende Memory Leaks“ findet man in vielen Anwendungen und oft werden diese Probleme „ignoriert“ und man begegnet ihnen mit Maßnahmen wie:

  • Größere Heaps, um Zeit bis zum Auftreten des Fehlers zu gewinnen. Im Zeitalter von 64bit JVMs wird diese Maßnahme leider immer beliebter.
  • Neustart von Applikationsservern in der Nacht. Dadurch erfolgt quasi ein „Reset“ des Speichers. Läuft der Speicher also innerhalb von 24 Stunden nicht komplett voll, kann man den Fehler durch den Neustart vermeiden.

Beide Varianten sind gefährlich, weil sie sich negativ auf die Performance auswirken und die Gefahr bergen, dass durch verändertes Benutzerverhalten oder mehr „Traffic“ der Fehler doch schneller auftritt als erwartet. Die Performance wird zudem durch den Garbage Collector negativ beeinflusst, da eine immer voller werdende „Tenured Generation“ dazu führt, dass der GC mehr Objekte durchlaufen muss und die „Mark“-Phase immer mehr Zeit benötigt – bei großen Heaps wird die Menge der zu analysierenden Objekte noch größer. Der 3. und 4. Akt dieser Serie werden sich daher im Detail mit der Analyse dieser Memory Leaks beschäftigen, um solche Situationen zu vermeiden.

Zu viel Speicher

Es gibt auch Fälle, bei denen ein OutOfMemoryError im Heap nicht durch ein Memory Leak im eigentlichen Sinne verursacht wird, sondern die Anwendung einfach zu viel Speicher konsumiert. In diesem Fall hat man entweder den Heap zu klein gewählt und muss diesen vergrößern (siehe auch Teil 3 dieser Serie) oder man muss den Speicherverbrauch der Anwendung verringern, z.B. in dem man Cache-Größen kleiner wählt.

Besonders kritisch ist aber auch der hohe Verbrauch von temporärem Speicher, weil dieser gerade bei Enterprise-Anwendungen dazu führen kann, dass bei bestimmten parallelen Zugriffen ein OutOfMemoryError auftritt – diese sind somit nicht deterministisch und verursachen dadurch ein größeres Unbehagen, weil man ihnen nicht mit nächtlichen Neustarts begegnen kann. Das nachfolgende Beispiel zeigt einen möglichen Problem-Code:

1byte[] image = getTheByteImage();
2response.setContentType("image/jpeg");
3ServletOutputStream out = response.getOutputStream();
4out.write(image);
5out.flush();
6out.close();

Nicht direkt offensichtlich ist hier der Speicherverbrauch, jedoch wird bei jedem Aufruf das Bild als Byte-Array auf den Heap gelegt bevor es an den Browser gesendet wird. Eine bessere Variante wäre, das Bild direkt zu streamen:

1InputStream image = getTheImageAsStream();
2response.setContentType("image/jpeg");
3ServletOutputStream out = response.getOutputStream();
4IOUtils.copy(image, out);
5out.flush();
6out.close();

(BufferedStreams und IOUtils verwenden intern zwar auch byte[], jedoch sind diese in der Regel viel kleiner)

In diesem Zusammenhang haben wir erstmal nur java.lang.OutOfMemoryError Probleme im Heap beleuchtet. Wir bereits im vorangegangenen Blog-Eintrag erwähnt können aber auch in anderen Speicherbereichen (z.B. der Permanent Generation) OutOfMemoryError auftreten – hierzu werde ich aber noch einmal gesondert einen Beitrag schreiben.

Im nächsten Teil dieser Serie „Konfiguration und Überwachung der Java Virtual Machine“ werde ich zeigen, wie man  Einstellungen des Heaps bei der Sun JVM konfiguriert und optimiert und zudem zeigen wie man den Speicher mit Hilfe von JVM Bordmitteln überwacht.

„Java Heapdumps erzeugen und verstehen“ wird sich dann im vierten Teil der Serie mit der Erzeugung und Auswertung von Heapdumps beschäftigen und im Detail zeigen, wie man die in diesem Artikel beschrieben Memory Leak Urheber aufspürt.

Die nächsten beiden Teile werden also eher praktisch und weniger theoretisch sein und ich plane einige kleine Screencasts zu integrieren, um anschauliche Beispiele zu geben.

|

Beitrag teilen

Gefällt mir

2

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.