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

5 Kommentare

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

Exception 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:

public class MyFrame extends javax.swing.JFrame {
 
  // reachable via Classloader as soon class is loaded
  public static final ArrayList STATIC = new ArrayList();
 
  // as long as the JFrame is not dispose()'d,
  // it is reachable via a native window
  private final ArrayList jni = new ArrayList()
 
  // while this method is executing parameter is kept reachable,
  // even if it is not used
  private void myMethod(ArrayList parameter) {
 
    // while this method is running, this list is reachable from the stack
    ArrayList local = new ArrayList();
  }
 
}

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:

byte[] image = getTheByteImage();
response.setContentType("image/jpeg");
ServletOutputStream out = response.getOutputStream();
out.write(image);
out.flush();
out.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:

InputStream image = getTheImageAsStream();
response.setContentType("image/jpeg");
ServletOutputStream out = response.getOutputStream();
IOUtils.copy(image, out);
out.flush();
out.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.

Als Mitbegründer und Advisor der codecentric AG berät Mirko bei der strategischen Ausrichtung des Unternehmens. Er ist Geschäftsführer und Mitgründer der Startups CenterDevice und Instana – beides disruptive Geschäftsmodelle auf Basis von Big-Data- und Cloud-Technologien.

Seine Interessen liegen im Bereich der Veränderung von Geschäftsmodellen durch moderne Technologien und Software, also der zunehmenden Digitalisierung der Welt und den daraus resultierenden Veränderungen und Potential für Unternehmen.
Im Privatleben widmet er sich am liebsten seiner Familie, zum sportlichen Ausgleich fährt er Mountainbike.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentare

  • 4. März 2011 von Mirko Novakovic

    Hi Sandeep,

    if I understand you right, you ask if there are scenarios where objects are „really“ leaking, so no references to the object and GC cannot free the memory, right?

    This scenario is not possible in Java – if it would occur, this would be a bug in the JVM implementation.

    There are interesting scenarios, where a reference is not reachable anymore, but the memory cannot be freed, e.g.:

     
    public void someMethod() {
     
      try {
         String xml = getSomeBigXML();
         // do something
      } catch (AnException) {
         // handle exception
      }
     
      // After this point xml is not reachable,
      // but cannot be freed until the method is finished...
     
      // do something long running
    }

    In this scenario the „big“ xml Object cannot be freed by the GC, because it has still a GC root, but it is not reachable outside the try-catch block. If the method is long running, this could turn into a memory problem.

    • In the above code, just after the catch if you do xml = null then the GC can clean this during it’s phase?
      It may sound silly but I’m not so good at garbage collection. Thanks.

    • In the above example, Why GC needs to wait before deallocate memory taken by xml Object ?

      By default does GC wait for method to end before deallocating any of the variables created inside the method. Does it not takes code blocks into account ?

      Please clarify.

      • 23. April 2014 von Mirko Novakovic

        Hi Amit,

        my understanding is that variable xml will be put on the method stack, as it is a local variable of the method someMethod. Even if the variable is not reachable anymore (because we are outside of the scope), the variable is still on the stack and references the object that is returned by getSomeBigXML(). The object itself is on the „normal“ Java heap and not on the stack.

        I could imagine that modern JVMs and their JIT are clever enough to „see“ that the variable is never used again and dereference it, so that the GC can clean it up. Look at this bug report which indicated that the JIT can mark an object eligable for finalization before the method has ended: http://bugs.java.com/view_bug.do?bug_id=6721588

        So I would assume that it is much better with modern JVMs and really depends on the implementation what happens to the object. Anyway I would recommend to avoid these situations.

        Mirko

        Mirko

  • 6. März 2011 von Mirko Novakovic

    Adam,

    handling OOM errors is not advisable. OOM is a subclass of VirtualMachineError which indicates „that the Java Virtual Machine is broken or has run out of resources necessary for it to continue operating“. (see Javadoc) This said, in my point of view you should consider the JVM broken and the status of your application is unstable.

    A good option to „control“ the memory in your application is to use java.lang.ref references to tell the JVM how to handle your object – e.g. if you use a WeakReference, this references doesn’t prevent the GC to finalize the object if it is not used anymore in your application (as a „normal“ reference does). java.util.WeakHashMap uses WeakReferences for all entries so this would be a possible cache implementation. These type of references also allow you to better „intercept“ the lifecycle of an object.

    Mirko

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.