Frage Erstellen eines Speicherlecks mit Java


Ich hatte gerade ein Interview und ich wurde gebeten, ein Speicherleck mit Java zu erstellen. Unnötig zu sagen, dass ich ziemlich dumm war, keine Ahnung zu haben, wie ich überhaupt anfangen sollte, einen zu erstellen.

Was wäre ein Beispiel?


2634


Ursprung


Antworten:


Hier ist ein guter Weg, um ein echtes Speicherleck (Objekte, auf die durch Ausführen von Code nicht zugegriffen werden kann, die aber immer noch im Speicher gespeichert sind), in reinem Java zu erstellen:

  1. Die Anwendung erstellt einen lange laufenden Thread (oder verwenden Sie einen Thread-Pool, um noch schneller zu lecken).
  2. Der Thread lädt eine Klasse über einen (optional angepassten) ClassLoader.
  3. Die Klasse weist einen großen Speicherblock zu (z. new byte[1000000]), speichert eine starke Referenz darauf in einem statischen Feld und speichert dann einen Verweis auf sich selbst in einem ThreadLocal. Das Zuweisen des zusätzlichen Arbeitsspeichers ist optional (das Lecken der Klasseninstanz ist ausreichend), das Leck wird jedoch wesentlich schneller ausgeführt.
  4. Der Thread löscht alle Verweise auf die benutzerdefinierte Klasse oder den ClassLoader, von dem sie geladen wurde.
  5. Wiederholen.

Das funktioniert, weil das ThreadLocal einen Verweis auf das Objekt behält, das einen Verweis auf seine Klasse behält, die wiederum einen Verweis auf seinen ClassLoader behält. Der ClassLoader behält wiederum einen Verweis auf alle geladenen Klassen.

(Es war schlimmer in vielen JVM-Implementierungen, besonders vor Java 7, weil Klassen und ClassLoaders direkt in permgen zugewiesen wurden und niemals GC'd wurden. Unabhängig davon, wie die JVM Klassen entladen, verhindert ein ThreadLocal trotzdem eine Klassenobjekt wird zurückgewonnen.)

Eine Variation dieses Schemas ist der Grund, warum Anwendungscontainer (wie Tomcat) Speicher wie ein Sieb verlieren können, wenn Sie Anwendungen, die zufällig ThreadLocals verwenden, häufig erneut bereitstellen. (Da der Anwendungscontainer wie beschrieben Threads verwendet und jedes Mal, wenn Sie die Anwendung erneut bereitstellen, wird ein neuer ClassLoader verwendet.)

Aktualisieren: Da viele Leute danach fragen, Hier ist ein Beispielcode, der dieses Verhalten in Aktion zeigt.


1931



Referenz des statischen Feldhalteobjekts [esp letztes Feld]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

Berufung String.intern() auf langen String

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

(Unclosed) offene Streams (Datei, Netzwerk etc ...)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Nicht geschlossene Verbindungen

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Bereiche, die vom Garbage Collector von JVM nicht erreichbar sindB. Speicher, der über native Methoden zugewiesen wurde

In Webanwendungen werden einige Objekte im Anwendungsbereich der Anwendung gespeichert, bis die Anwendung explizit gestoppt oder entfernt wird.

getServletContext().setAttribute("SOME_MAP", map);

Falsche oder unangemessene JVM-Optionen, so wie die noclassgc Option auf IBM JDK, die eine ungenutzte Klassenbereinigung verhindert

Sehen IBM Jdk-Einstellungen.


1059



Eine einfache Sache ist, ein HashSet mit einem falschen (oder nicht existenten) zu verwenden hashCode() oder equals()und fügen Sie dann "Duplikate" hinzu. Anstatt Duplikate zu ignorieren, wie es sollte, wird das Set immer nur wachsen und Sie werden nicht in der Lage sein, sie zu entfernen.

Wenn Sie wollen, dass diese schlechten Schlüssel / Elemente herumhängen, können Sie ein statisches Feld wie verwenden

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

390



Unten wird es einen nicht-offensichtlichen Fall geben, in dem Java neben dem Standardfall von vergessenen Zuhörern statische Referenzen, gefälschte / modifizierbare Schlüssel in Hashmaps oder nur festsitzende Threads ohne Chance gibt, ihren Lebenszyklus zu beenden.

  • File.deleteOnExit()- Leckt immer die Schnur, Wenn der String ein Teilstring ist, ist das Leck noch schlimmer (der zugrunde liegende char [] wird ebenfalls durchgesickert) - in Java 7 Substring kopiert auch die char[], also trifft das später nicht zu; @Daniel, keine Notwendigkeit für Abstimmungen, obwohl.

Ich konzentriere mich auf Threads, um die Gefahr von nicht verwalteten Threads zu zeigen, möchte nicht einmal den Swing berühren.

  • Runtime.addShutdownHook und nicht entfernen ... und dann sogar mit removeShutdownHook aufgrund eines Fehler in Thread Klasse in Bezug auf unstarted Fäden kann es nicht gesammelt bekommen, effektiv den Thread auslaufen. JGroup hat das Leck in GossipRouter.

  • Erstellen, aber nicht starten a Thread geht in die gleiche Kategorie wie oben.

  • Erstellen eines Threads erbt die ContextClassLoader und AccessControlContext, plus die ThreadGroup und irgendwas InheritedThreadLocalall diese Referenzen sind potentielle Lecks, zusammen mit den ganzen vom Klassenlader geladenen Klassen und allen statischen Referenzen und ja-ja. Der Effekt ist besonders sichtbar mit dem gesamten j.u.c.Executor-Framework, das eine sehr einfache Funktion bietet ThreadFactory Interface, aber die meisten Entwickler haben keine Ahnung von der lauernden Gefahr. Auch viele Bibliotheken starten Threads auf Anfrage (viel zu viele beliebte Branchenbibliotheken).

  • ThreadLocal Caches; das sind in vielen Fällen böse. Ich bin sicher, dass jeder ziemlich viel einfacher Caches auf Thread basiert gesehen hat, auch die schlechte Nachricht: wenn der Thread geht mehr hält als das Leben des Kontext Classloader zu erwarten, es ist ein reines schönes kleines Leck ist. Verwenden Sie keine ThreadLocal-Caches, wenn sie nicht wirklich benötigt werden.

  • Berufung ThreadGroup.destroy() wenn die ThreadGroup selbst keine Threads hat, aber untergeordnete ThreadGroups behält. Ein fehlerhaftes Leck, das verhindert, dass die ThreadGroup von ihrem übergeordneten Element entfernt wird, aber alle untergeordneten Elemente werden nicht aufgezählt.

  • Mit WeakHashMap und dem Wert (in) wird direkt auf den Schlüssel verwiesen. Dies ist schwierig ohne einen Heap-Dump zu finden. Das gilt für alle erweitert Weak/SoftReference das könnte einen harten Verweis auf das geschützte Objekt zurückhalten.

  • Verwenden java.net.URL mit dem HTTP (S) -Protokoll und Laden der Ressource von (!). Dieser ist besonders, der KeepAliveCache erstellt einen neuen Thread im System ThreadGroup, der den Kontext-Classloader des aktuellen Threads verliert. Der Thread wird bei der ersten Anfrage erstellt, wenn kein lebendiger Thread existiert, also können Sie entweder Glück haben oder einfach nur lecken. Das Leck ist bereits in Java 7 behoben und der Code, der den Thread ordnungsgemäß erstellt, entfernt den Kontext-Classloader. Es gibt wenige weitere Fälle (wie ImageFetcher, auch behoben), ähnliche Threads zu erstellen.

  • Verwenden InflaterInputStream Vorbeigehen new java.util.zip.Inflater() im Konstruktor (PNGImageDecoder zum Beispiel) und nicht anruft end() von dem Inflater. Nun, wenn Sie den Konstruktor mit nur übergeben newkeine Chance ... Und ja, rufen close() Im Stream wird der Inflator nicht geschlossen, wenn er manuell als Konstruktorparameter übergeben wird. Dies ist kein echtes Leck, da es vom Finalizer veröffentlicht wird ... wenn es für notwendig erachtet wird. Bis zu diesem Moment frisst es natives Gedächtnis so sehr, dass Linux oom_killer den Prozess ungestraft beenden kann. Das Hauptproblem ist, dass die Finalisierung in Java sehr unzuverlässig ist und G1 es bis 7.0.2 schlimmer gemacht hat. Moral der Geschichte: Native Ressourcen sobald wie möglich freisetzen; Der Finalizer ist einfach zu schlecht.

  • Derselbe Fall mit java.util.zip.Deflater. Dieser ist viel schlechter, da Deflater in Java speicherhungrig ist, d. H. Immer 15 Bits (maximal) und 8 Speicherpegel (maximal 9) verwendet, die mehrere hundert KB nativen Speicher zuweisen. Glücklicherweise, Deflater ist nicht weit verbreitet und meines Wissens enthält JDK keinen Missbrauch. Immer anrufen end() wenn Sie manuell ein Deflater oder Inflater. Der beste Teil der letzten zwei: Sie können sie nicht über normale Profiling-Tools finden.

(Ich kann einige Zeitverschwender hinzufügen, die ich auf Anfrage angetroffen habe.)

Viel Glück und bleib in Sicherheit. Lecks sind böse!


228



Die meisten Beispiele sind "zu komplex". Sie sind Randfälle. Mit diesen Beispielen hat der Programmierer einen Fehler gemacht (wie zum Beispiel equals / hashcode nicht neu definiert), oder wurde von einem Eckfall der JVM / JAVA gebissen (Laden der Klasse mit statischem ...). Ich denke, das ist nicht die Art von Beispiel, die ein Interviewer haben möchte oder sogar der häufigste Fall.

Aber es gibt wirklich einfachere Fälle für Speicherlecks. Der Garbage Collector gibt nur frei, auf was nicht mehr verwiesen wird. Wir als Java-Entwickler kümmern uns nicht um Speicher. Wir weisen es bei Bedarf zu und lassen es automatisch frei. Fein.

Aber jede langlebige Anwendung neigt dazu, einen gemeinsamen Status zu haben. Es kann alles Mögliche sein, Statik, Singleton ... Oft neigen nicht-triviale Anwendungen dazu, komplexe Objekte zu Graphen zu machen. Man vergisst einfach, einen Verweis auf null zu setzen, oder es vergisst öfter, ein Objekt aus einer Sammlung zu entfernen, um einen Speicherleck zu erzeugen.

Natürlich neigen alle Arten von Listenern (wie UI-Listener), Caches oder jeder langlebige, gemeinsam genutzte Zustand dazu, Speicherlecks zu erzeugen, wenn sie nicht richtig gehandhabt werden. Was zu verstehen ist, ist, dass dies kein Fall von Java Corner oder ein Problem mit dem Garbage Collector ist. Es ist ein Designproblem. Wir entwerfen, dass wir einem langlebigen Objekt einen Listener hinzufügen, aber wir entfernen den Listener nicht, wenn er nicht mehr benötigt wird. Wir cachen Objekte, aber wir haben keine Strategie, sie aus dem Cache zu entfernen.

Wir haben vielleicht ein komplexes Diagramm, das den vorherigen Zustand speichert, der für eine Berechnung benötigt wird. Aber der vorherige Zustand ist selbst mit dem Staat verbunden und so weiter.

So müssen wir SQL-Verbindungen oder -Dateien schließen. Wir müssen korrekte Verweise auf null setzen und Elemente aus der Sammlung entfernen. Wir werden geeignete Caching-Strategien haben (maximale Speichergröße, Anzahl der Elemente oder Timer). Alle Objekte, die es einem Listener ermöglichen, benachrichtigt zu werden, müssen sowohl eine addListener-Methode als auch eine removeListener-Methode bereitstellen. Und wenn diese Benachrichtigungen nicht mehr verwendet werden, müssen sie ihre Listenerliste löschen.

Ein Speicherleck ist tatsächlich wirklich möglich und perfekt vorhersehbar. Keine Notwendigkeit für spezielle Sprachfunktionen oder Eckfälle. Speicherlecks sind entweder ein Indikator dafür, dass etwas fehlt oder sogar Designprobleme.


150



Die Antwort hängt ganz davon ab, was der Interviewer dachte, dass sie gefragt haben.

Ist es in der Praxis möglich, Java auslaufen zu lassen? Natürlich ist es, und es gibt viele Beispiele in den anderen Antworten.

Aber es gibt mehrere Meta-Fragen, die vielleicht gestellt wurden?

  • Ist eine theoretisch "perfekte" Java-Implementierung anfällig für Lecks?
  • Versteht der Kandidat den Unterschied zwischen Theorie und Realität?
  • Versteht der Kandidat, wie die Garbage Collection funktioniert?
  • Oder wie soll die Müllsammlung im Idealfall funktionieren?
  • Wissen sie, dass sie andere Sprachen über native Schnittstellen aufrufen können?
  • Wissen sie, dass sie in diesen anderen Sprachen Speicher verlieren?
  • Weiß der Kandidat überhaupt, was Speichermanagement ist und was hinter der Szene in Java vor sich geht?

Ich lese deine Meta-Frage als "Was ist eine Antwort, die ich in dieser Interview-Situation hätte verwenden können". Und deshalb werde ich mich auf Interviewfähigkeiten statt auf Java konzentrieren. Ich glaube, dass Sie eher die Situation wiederholen werden, wenn Sie die Antwort auf eine Frage in einem Interview nicht kennen, als Sie an einem Ort sein müssen, an dem Sie wissen müssen, wie Sie Java zum Lecken bringen können. Also, hoffentlich wird dies helfen.

Eine der wichtigsten Fähigkeiten, die Sie für das Vorstellungsgespräch entwickeln können, besteht darin, dass Sie lernen, aktiv auf die Fragen zu hören und mit dem Interviewer zu arbeiten, um seine Absicht zu extrahieren. Damit können Sie Ihre Frage nicht nur so beantworten, wie sie es möchten, sondern auch zeigen, dass Sie über wichtige Kommunikationsfähigkeiten verfügen. Und wenn es um die Wahl zwischen vielen gleich talentierten Entwicklern geht, werde ich denjenigen beauftragen, der zuhört, denkt und versteht, bevor sie jedes Mal antworten.


133



Das Folgende ist ein ziemlich sinnloses Beispiel, wenn Sie nicht verstehen JDBC. Oder zumindest, wie JDBC erwartet, dass ein Entwickler schließt Connection, Statement und ResultSet Instanzen, bevor sie gelöscht oder Referenzen verloren gehen, anstatt sich auf die Implementierung von finalize.

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

Das Problem mit dem oben genannten ist, dass die Connection Objekt ist nicht geschlossen, und daher bleibt die physische Verbindung offen, bis der Garbage Collector vorbeikommt und feststellt, dass er nicht erreichbar ist. GC wird das aufrufen finalize Methode, aber es gibt JDBC - Treiber, die das nicht implementieren finalizezumindest nicht so Connection.close ist implementiert. Das resultierende Verhalten besteht darin, dass der Speicher aufgrund nicht erreichbarer Objekte, die gesammelt werden, zurückgewonnen wird, Ressourcen (einschließlich Speicher), die mit dem Speicher verbunden sind Connection Objekt könnte einfach nicht zurückgewonnen werden.

In einem solchen Fall, wo die Connectionist es finalize Methode bereinigt nicht alles, man könnte tatsächlich feststellen, dass die physische Verbindung zum Datenbankserver mehrere Speicherbereinigungszyklen dauern wird, bis der Datenbankserver schließlich herausfindet, dass die Verbindung nicht aktiv ist (falls dies der Fall ist) und geschlossen werden sollte.

Selbst wenn der JDBC-Treiber implementiert werden würde finalizeEs ist möglich, dass während der Finalisierung Ausnahmen ausgelöst werden. Das resultierende Verhalten ist, dass jeder Speicher, der dem jetzt "ruhenden" Objekt zugeordnet ist, nicht zurückgewonnen wird, wie z finalize wird garantiert nur einmal aufgerufen.

Das obige Szenario, in dem während der Fertigstellung des Objekts Ausnahmen auftreten, steht in Zusammenhang mit einem anderen Szenario, das möglicherweise zu einem Speicherleck führen könnte - Object Resurrection. Objektauferstehung wird oft absichtlich ausgeführt, indem eine starke Referenz auf das Objekt, das finalisiert wird, von einem anderen Objekt erzeugt wird. Wenn die Wiederauffindung von Objekten missbraucht wird, führt dies zusammen mit anderen Quellen von Speicherlecks zu einem Speicherleck.

Es gibt viele weitere Beispiele, die Sie zaubern können

  • Verwalten eines List Instanz, wo Sie nur zur Liste hinzufügen und nicht von ihr löschen (obwohl Sie Elemente loswerden sollten, die Sie nicht mehr benötigen), oder
  • Öffnung Sockets oder Files, aber nicht schließen, wenn sie nicht mehr benötigt werden (ähnlich dem obigen Beispiel mit dem Connection Klasse).
  • Keine Singletons entladen, wenn eine Java EE-Anwendung heruntergefahren wird. Offensichtlich behält der Classloader, der die Singleton-Klasse geladen hat, einen Verweis auf die Klasse, und daher wird die Singleton-Instanz niemals gesammelt. Wenn eine neue Instanz der Anwendung bereitgestellt wird, wird in der Regel ein neuer Klassenlader erstellt, und der frühere Klassenlader wird aufgrund des Singletons weiterhin vorhanden sein.

113



Eines der einfachsten Beispiele für ein mögliches Speicherleck und seine Vermeidung ist die Implementierung von ArrayList.remove (int):

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

Wenn Sie es selbst implementiert hätten, hätten Sie daran gedacht, das Arrayelement zu löschen, das nicht mehr verwendet wird (elementData[--size] = null) Diese Referenz könnte ein riesiges Objekt am Leben erhalten ...


102