Wie erstelle ich einen Speicherverlust in Java?

3223

Ich hatte gerade ein Interview und wurde gebeten, ein Speicherleck mit Java zu erstellen .
Unnötig zu erwähnen, dass ich mich ziemlich dumm fühlte, keine Ahnung zu haben, wie ich überhaupt anfangen sollte, eine zu erstellen.

Was wäre ein Beispiel?

Mat B.
quelle
275
Ich würde ihnen sagen, dass Java einen Garbage Collector verwendet, und sie bitten, etwas genauer über ihre Definition von "Speicherverlust" zu sprechen, und erklären, dass Java - abgesehen von JVM-Fehlern - Speicher nicht auf die gleiche Weise wie C verlieren kann / C ++ kann. Sie müssen irgendwo einen Verweis auf das Objekt haben .
Darien
371
Ich finde es lustig, dass die Leute bei den meisten Antworten nach diesen Randfällen und Tricks suchen und den Punkt (IMO) völlig verfehlen. Sie könnten nur Code anzeigen, der nutzlose Verweise auf Objekte enthält, die nie wieder verwendet werden, und gleichzeitig diese Verweise niemals löschen. Man kann sagen, dass diese Fälle keine "echten" Speicherlecks sind, weil es immer noch Verweise auf diese Objekte gibt, aber wenn das Programm diese Verweise nie wieder verwendet und sie auch nie wieder löscht, ist es völlig gleichbedeutend mit (und so schlecht wie) a " wahrer Speicherverlust ".
Ehabkost
62
Ehrlich gesagt kann ich nicht glauben, dass die ähnliche Frage, die ich zu "Go" gestellt habe, auf -1 herabgestuft wurde. Hier: stackoverflow.com/questions/4400311/… Grundsätzlich sind die Speicherlecks, über die ich gesprochen habe, diejenigen, die +200 Upvotes für das OP erhalten haben, und dennoch wurde ich angegriffen und beleidigt, weil ich gefragt habe, ob "Go" das gleiche Problem hat. Irgendwie bin ich mir nicht sicher, ob alles Wiki-Ding so gut funktioniert.
SyntaxT3rr0r
74
@ SyntaxT3rr0r - Dariens Antwort ist kein Fanboyismus. Er gab ausdrücklich zu, dass bestimmte JVMs Fehler aufweisen können, die bedeuten, dass Speicher verloren geht. Dies unterscheidet sich von der Sprachspezifikation selbst, die Speicherlecks zulässt.
Peter Recore
31
@ehabkost: Nein, sie sind nicht gleichwertig. (1) Sie haben die Möglichkeit, den Speicher zurückzugewinnen, während Ihr C / C ++ - Programm bei einem "echten Leck" den zugewiesenen Bereich vergisst. Es gibt keine sichere Möglichkeit, ihn wiederherzustellen. (2) Sie können das Problem mit der Profilerstellung sehr leicht erkennen, da Sie sehen können, um welche Objekte es sich beim "Aufblähen" handelt. (3) Ein "echtes Leck" ist ein eindeutiger Fehler, während ein Programm, das viele Objekte bis zum Beenden in der Nähe hält , ein bewusster Teil seiner Funktionsweise sein kann.
Darien

Antworten:

2309

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 lang laufenden Thread (oder verwendet einen Thread-Pool, um noch schneller zu lecken).
  2. Der Thread lädt eine Klasse über eine (optional benutzerdefinierte) ClassLoader.
  3. Die Klasse weist einen großen Teil des Speichers zu (z. B. new byte[1000000]), speichert einen starken Verweis darauf in einem statischen Feld und speichert dann einen Verweis auf sich selbst in a ThreadLocal. Das Zuweisen des zusätzlichen Speichers ist optional (es reicht aus, die Klasseninstanz zu verlieren), aber dadurch funktioniert das Leck viel schneller.
  4. Die Anwendung löscht alle Verweise auf die benutzerdefinierte Klasse oder die Klasse, aus der ClassLoadersie geladen wurde.
  5. Wiederholen.

Aufgrund der Art und Weise, wie ThreadLocales im JDK von Oracle implementiert ist, entsteht ein Speicherverlust:

  • Jedes Threadhat ein privates Feld threadLocals, in dem die threadlokalen Werte gespeichert sind.
  • Jeder Schlüssel in dieser Karte ist eine schwache Referenz auf ein ThreadLocalObjekt. Nachdem dieses ThreadLocalObjekt durch Müll gesammelt wurde, wird sein Eintrag aus der Karte entfernt.
  • Jeder Wert ist jedoch eine starke Referenz. Wenn also ein Wert (direkt oder indirekt) auf das ThreadLocalObjekt verweist, das sein Schlüssel ist , wird dieses Objekt weder durch Müll gesammelt noch aus der Karte entfernt, solange der Thread lebt.

In diesem Beispiel sieht die Kette starker Referenzen folgendermaßen aus:

ThreadObjekt → threadLocalsKarte → Instanz der Beispielklasse → Beispielklasse → statisches ThreadLocalFeld → ThreadLocalObjekt.

(Das ClassLoaderspielt beim Erstellen des Lecks keine wirkliche Rolle, es verschlimmert das Leck nur aufgrund dieser zusätzlichen Referenzkette: Beispielklasse → ClassLoader→ alle Klassen, die es geladen hat. Es war in vielen JVM-Implementierungen noch schlimmer, insbesondere vor Java 7, weil Klassen und ClassLoaders direkt in permgen zugewiesen wurden und niemals Müll gesammelt wurden.)

Eine Variation dieses Musters ist, warum Anwendungscontainer (wie Tomcat) Speicher wie ein Sieb verlieren können, wenn Sie häufig Anwendungen neu bereitstellen, die zufällig ThreadLocals verwenden, die auf irgendeine Weise auf sich selbst zurückweisen. Dies kann aus einer Reihe subtiler Gründe geschehen und ist oft schwer zu debuggen und / oder zu beheben.

Update : Da immer wieder viele Leute danach fragen, finden Sie hier einen Beispielcode, der dieses Verhalten in Aktion zeigt .

Daniel Pryden
quelle
186
+1 ClassLoader-Lecks sind einige der am häufigsten schmerzhaften Speicherlecks in der JEE-Welt, die häufig durch Bibliotheken von Drittanbietern verursacht werden, die Daten transformieren (BeanUtils, XML / JSON-Codecs). Dies kann passieren, wenn die Bibliothek außerhalb des Root-Klassenladeprogramms Ihrer Anwendung geladen wird, jedoch Verweise auf Ihre Klassen enthält (z. B. durch Caching). Wenn Sie die Bereitstellung Ihrer App aufheben / erneut bereitstellen, kann die JVM den Klassenladeprogramm der App (und damit alle von ihr geladenen Klassen) nicht mit Speicherbereinigung erfassen. Bei wiederholter Bereitstellung wird der App-Server schließlich fehlerhaft. Wenn Sie Glück haben, erhalten Sie einen Hinweis mit ClassCastException. ZxyAbc kann nicht in zxyAbc umgewandelt werden
earcam
7
tomcat verwendet Tricks und Null ALLE statischen Variablen in ALLEN geladenen Klassen, tomcat hat jedoch viele Datenbereiche und schlechte Codierung (muss etwas Zeit in Anspruch nehmen und Korrekturen einreichen) sowie die umwerfende ConcurrentLinkedQueue als Cache für interne (kleine) Objekte. so klein, dass sogar der ConcurrentLinkedQueue.Node mehr Speicher benötigt.
Bests
57
+1: Classloader-Lecks sind ein Albtraum. Ich habe Wochen damit verbracht, sie herauszufinden. Das Traurige ist, wie @earcam gesagt hat, dass sie hauptsächlich durch Bibliotheken von Drittanbietern verursacht werden und auch die meisten Profiler diese Lecks nicht erkennen können. In diesem Blog gibt es eine gute und klare Erklärung für Classloader-Lecks. blogs.oracle.com/fkieviet/entry/…
Adrian M
4
@Nicolas: Bist du sicher? JRockit führt standardmäßig GC-Klassenobjekte aus und HotSpot nicht, aber AFAIK JRockit kann eine Klasse oder einen ClassLoader, auf die / den ein ThreadLocal verweist, immer noch nicht GC.
Daniel Pryden
6
Tomcat wird versuchen, diese Lecks für Sie zu erkennen und vor ihnen zu warnen: wiki.apache.org/tomcat/MemoryLeakProtection . Die neueste Version behebt manchmal sogar das Leck für Sie.
Matthijs Bierman
1212

Statisches Feld mit Objektreferenz [besonders letztes Feld]

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

String.intern()Auf langen String anrufen

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();

(Nicht geschlossene) offene Streams (Datei, Netzwerk usw.)

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 sind , z. B. Speicher, der über native Methoden zugewiesen wurde

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

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

Falsche oder unangemessene JVM-Optionen , z. B. die noclassgcOption in IBM JDK, die die nicht verwendete Klassenbereinigung verhindert

Siehe IBM JDK-Einstellungen .

Prashant Bhate
quelle
178
Ich würde nicht zustimmen, dass Kontext- und Sitzungsattribute "Lecks" sind. Sie sind nur langlebige Variablen. Und das statische Endfeld ist mehr oder weniger nur eine Konstante. Vielleicht sollten große Konstanten vermieden werden, aber ich denke nicht, dass es fair ist, es als Speicherverlust zu bezeichnen.
Ian McLaird
80
(Nicht geschlossene) offene Streams (Datei, Netzwerk usw.) lecken nicht wirklich, während der Finalisierung (die nach dem nächsten GC-Zyklus erfolgt) wird close () geplant ( close()wird normalerweise nicht im Finalizer aufgerufen) Thread da könnte eine Blockierungsoperation sein). Es ist eine schlechte Praxis, nicht zu schließen, aber es verursacht kein Leck. Nicht geschlossene java.sql.Connection ist dieselbe.
Bests
33
In den meisten vernünftigen JVMs scheint es, als ob die String-Klasse nur einen schwachen Verweis auf ihren internHashtabelleninhalt hat. Als solches ist es ist Müll richtig und nicht ein Leck gesammelt. (aber IANAJP) mindprod.com/jgloss/interned.html#GC
Matt B.
42
Wie ist ein statisches Feld, das die Objektreferenz [insbesondere das letzte Feld] enthält, ein Speicherverlust?
Kanagavelu Sugumar
5
@cHao Richtig. Die Gefahr, auf die ich gestoßen bin, besteht nicht darin, dass Speicher durch Streams verloren geht. Das Problem ist, dass nicht genügend Speicher von ihnen verloren geht. Sie können viele Griffe auslaufen lassen, haben aber immer noch viel Speicher. Der Garbage Collector beschließt dann möglicherweise, keine vollständige Sammlung durchzuführen, da noch genügend Speicher vorhanden ist. Dies bedeutet, dass der Finalizer nicht aufgerufen wird, sodass Ihnen die Handles ausgehen. Das Problem ist, dass die Finalizer (normalerweise) ausgeführt werden, bevor Ihnen der Speicher von undichten Streams ausgeht, sie jedoch möglicherweise nicht aufgerufen werden, bevor Ihnen etwas anderes als der Speicher ausgeht.
Patrick M
460

Eine einfache Sache ist, ein HashSet mit einem falschen (oder nicht existierenden) hashCode()oder zu verwenden equals()und dann weiterhin "Duplikate" hinzuzufügen. Anstatt Duplikate zu ignorieren, wächst das Set immer nur und Sie können sie nicht entfernen.

Wenn Sie möchten, dass diese fehlerhaften 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.
Peter Lawrey
quelle
68
Tatsächlich können Sie die Elemente aus einem HashSet entfernen, selbst wenn die Elementklasse hashCode erhält und gleich falsch ist. Holen Sie sich einfach einen Iterator für die Menge und verwenden Sie die Methode remove, da der Iterator tatsächlich die zugrunde liegenden Einträge selbst und nicht die Elemente bearbeitet. (Beachten Sie, dass ein nicht implementierter hashCode / equals nicht ausreicht, um ein Leck auszulösen. Die Standardeinstellungen implementieren eine einfache Objektidentität, sodass Sie die Elemente abrufen und normal entfernen können.)
Donal Fellows
12
@Donal Ich versuche zu sagen, dass ich mit Ihrer Definition eines Speicherverlusts nicht einverstanden bin. Ich würde (um die Analogie fortzusetzen) Ihre Iteratorentfernungstechnik als eine Tropfpfanne unter einem Leck betrachten; Das Leck besteht unabhängig von der Auffangwanne.
CorsiKa
94
Ich bin damit einverstanden, dass dies kein "Speicherleck" ist, da Sie einfach Verweise auf das Hashset entfernen und warten können, bis der GC aktiviert wird, und schon! Die Erinnerung geht zurück.
user541686
12
@ SyntaxT3rr0r, ich habe Ihre Frage so interpretiert, dass sie fragt, ob die Sprache etwas enthält, das natürlich zu Speicherverlusten führt. Die Antwort ist nein. Diese Frage fragt, ob es möglich ist, eine Situation zu erfinden, um so etwas wie einen Speicherverlust zu erzeugen. Keines dieser Beispiele ist ein Speicherverlust in der Weise, wie es ein C / C ++ - Programmierer verstehen würde.
Peter Lawrey
11
@ Peter Lawrey: auch, was denken Sie darüber nach : „Es gibt nichts in der C - Sprache , die auf Speicherleck natürlich leckt , wenn Sie manuell den Speicher , den Sie zugewiesen frei nicht vergessen“ . Wie wäre das für intellektuelle Unehrlichkeit? Wie auch immer, ich bin müde: Du kannst das letzte Wort haben.
SyntaxT3rr0r
271

Im Folgenden wird es einen nicht offensichtlichen Fall geben, in dem Java-Lecks auftreten, neben dem Standardfall vergessener Listener, statischer Verweise, gefälschter / modifizierbarer Schlüssel in Hashmaps oder nur Threads, die stecken bleiben, ohne die Chance zu haben, ihren Lebenszyklus zu beenden.

  • File.deleteOnExit() - leckt immer die Schnur, Wenn der String ein Teilstring ist, ist das Leck noch schlimmer (das zugrunde liegende Zeichen [] ist ebenfalls durchgesickert).- In Java 7 kopiert der Teilstring auch den char[], sodass der spätere nicht zutrifft . @ Daniel, keine Notwendigkeit für Stimmen.

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

  • Runtime.addShutdownHookund nicht entfernen ... und dann selbst mit removeShutdownHook aufgrund eines Fehlers in der ThreadGroup-Klasse in Bezug auf nicht gestartete Threads, die möglicherweise nicht gesammelt werden, die ThreadGroup effektiv lecken. JGroup hat das Leck in GossipRouter.

  • Das Erstellen, aber nicht das Starten von a gehört Threadzur gleichen Kategorie wie oben.

  • Das Erstellen eines Threads erbt das ContextClassLoaderund AccessControlContextsowie das ThreadGroupund alle InheritedThreadLocal, alle diese Referenzen sind potenzielle Lecks, zusammen mit den gesamten vom Klassenladeprogramm geladenen Klassen und allen statischen Referenzen und ja-ja. Der Effekt ist besonders beim gesamten jucExecutor-Framework sichtbar, das über eine supereinfache ThreadFactoryBenutzeroberfläche verfügt. Die meisten Entwickler haben jedoch keine Ahnung von der lauernden Gefahr. Außerdem starten viele Bibliotheken auf Anfrage Threads (viel zu viele branchenweit beliebte Bibliotheken).

  • ThreadLocalCaches; das sind in vielen Fällen böse. Ich bin sicher, jeder hat eine Menge einfacher Caches gesehen, die auf ThreadLocal basieren, und die schlechte Nachricht: Wenn der Thread im Kontext des ClassLoader mehr als erwartet läuft, ist es ein reines nettes kleines Leck. Verwenden Sie keine ThreadLocal-Caches, es sei denn, dies wird wirklich benötigt.

  • Aufruf, ThreadGroup.destroy()wenn die ThreadGroup selbst keine Threads hat, aber weiterhin untergeordnete ThreadGroups enthält. Ein schlechtes Leck, das verhindert, dass die ThreadGroup von ihrem übergeordneten Element entfernt wird, aber alle untergeordneten Elemente nicht mehr aufgezählt werden können.

  • Die Verwendung von WeakHashMap und des Werts (in) verweist direkt auf den Schlüssel. Dies ist ohne Heap-Dump schwer zu finden. Dies gilt für alle erweiterten Weak/SoftReference, die möglicherweise einen harten Verweis auf das geschützte Objekt behalten.

  • Verwenden java.net.URLmit dem HTTP (S) -Protokoll und Laden der Ressource von (!). Dies ist etwas Besonderes, das KeepAliveCacheeinen neuen Thread in der System-ThreadGroup erstellt , der den Kontextklassenlader des aktuellen Threads verliert. Der Thread wird bei der ersten Anforderung erstellt, wenn kein lebendiger Thread vorhanden ist. Sie können also entweder Glück haben oder nur auslaufen. Das Leck ist bereits in Java 7 behoben und der Code, der den Thread ordnungsgemäß erstellt, entfernt den Kontextklassenlader. Es gibt nur noch wenige Fälle (wie ImageFetcher, auch behoben ) ähnliche Threads zu erstellen.

  • Mit InflaterInputStreamBestehen new java.util.zip.Inflater()im Konstruktor ( PNGImageDecoderzum Beispiel) und nicht Aufruf end()des inflater. Nun, wenn Sie den Konstruktor mit nur new, ohne Chance übergeben ... Und ja, close()wenn Sie den Stream aufrufen, wird der Inflater nicht geschlossen, wenn er manuell als Konstruktorparameter übergeben wird. Dies ist kein echtes Leck, da es vom Finalizer freigegeben wird ... wenn es dies für notwendig hält. Bis zu diesem Moment frisst es den nativen Speicher so stark, 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 noch schlimmer gemacht hat. Moral der Geschichte: Geben Sie native Ressourcen frei, sobald Sie können. Der Finalizer ist einfach zu schlecht.

  • Der gleiche Fall mit java.util.zip.Deflater. Dieser ist weitaus schlimmer, da Deflater in Java speicherhungrig ist, dh immer 15 Bit (maximal) und 8 Speicherebenen (maximal 9) verwendet, um mehrere hundert KB nativen Speicher zuzuweisen. Glücklicherweise Deflaterist es nicht weit verbreitet und meines Wissens enthält JDK keine Missbräuche. Rufen end()Sie immer an, wenn Sie manuell ein Deflateroder erstellen Inflater. Das Beste an den letzten beiden: Sie können sie nicht über normale verfügbare Profiling-Tools finden.

(Ich kann auf Anfrage weitere Zeitverschwender hinzufügen.)

Viel Glück und bleiben Sie sicher; Lecks sind böse!

Bests
quelle
23
Creating but not starting a Thread...Huch, ich wurde vor einigen Jahrhunderten von diesem schwer gebissen! (Java 1.3)
Leonbloy
@leonbloy, bevor es noch schlimmer wurde, als der Faden direkt zur Fadengruppe hinzugefügt wurde, bedeutete das Nichtstarten ein sehr hartes Leck. Nicht nur unstarted, dass es die Anzahl erhöht, sondern auch, dass die Thread-Gruppe nicht zerstört wird (weniger böse, aber immer noch ein Leck)
Bests
Vielen Dank! "Aufrufen, ThreadGroup.destroy()wenn die ThreadGroup selbst keine Threads hat ..." ist ein unglaublich subtiler Fehler. Ich habe dies stundenlang verfolgt und bin in die Irre geführt worden, weil das Aufzählen des Threads in meiner Kontroll-GUI nichts zeigte, aber die Thread-Gruppe und vermutlich mindestens eine untergeordnete Gruppe würden nicht verschwinden.
Lawrence Dol
1
@bestsss: Ich bin neugierig, warum sollten Sie einen Shutdown-Hook entfernen, da er beim Herunterfahren von JVM ausgeführt wird?
Lawrence Dol
203

Die meisten Beispiele hier sind "zu komplex". Sie sind Randfälle. Mit diesen Beispielen hat der Programmierer einen Fehler gemacht (z. B. Gleichheit / Hashcode nicht neu definieren) oder wurde von einem Eckfall der JVM / JAVA gebissen (Last der Klasse mit statischer ...). Ich denke, das ist nicht die Art von Beispiel, die ein Interviewer möchte, oder sogar der häufigste Fall.

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

Aber jede langlebige Anwendung hat in der Regel einen gemeinsamen Status. Es kann alles sein, Statik, Singletons ... Oft neigen nicht triviale Anwendungen dazu, komplexe Objektgraphen zu erstellen. Nur zu vergessen, einen Verweis auf null zu setzen oder öfter zu vergessen, ein Objekt aus einer Sammlung zu entfernen, reicht aus, um einen Speicherverlust zu verursachen.

Natürlich neigen alle Arten von Listenern (wie UI-Listener), Caches oder langlebige gemeinsame Zustände dazu, Speicherlecks zu erzeugen, wenn sie nicht richtig behandelt werden. Es versteht sich, dass dies kein Java-Eckfall oder ein Problem mit dem Garbage Collector ist. Es ist ein Designproblem. Wir planen, einem langlebigen Objekt einen Listener hinzuzufügen, entfernen den Listener jedoch nicht, wenn er nicht mehr benötigt wird. Wir zwischenspeichern Objekte, haben aber keine Strategie, um sie aus dem Cache zu entfernen.

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

Als müssten wir SQL-Verbindungen oder -Dateien schließen. Wir müssen die richtigen 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, mit denen ein Listener benachrichtigt werden kann, müssen sowohl eine addListener- als auch eine removeListener-Methode bereitstellen. Und wenn diese Benachrichtiger nicht mehr verwendet werden, müssen sie ihre Listener-Liste löschen.

Ein Speicherverlust ist in der Tat wirklich möglich und perfekt vorhersehbar. Keine Notwendigkeit für spezielle Sprachfunktionen oder Eckfälle. Speicherlecks sind entweder ein Hinweis darauf, dass möglicherweise etwas fehlt, oder sogar auf Designprobleme.

Nicolas Bousquet
quelle
24
Ich finde es lustig, dass die Leute bei anderen Antworten nach diesen Randfällen und Tricks suchen und den Punkt völlig verfehlen. Sie könnten nur Code anzeigen, der nutzlose Verweise auf Objekte enthält, die nie wieder verwendet werden, und diese Verweise niemals entfernen. Man kann sagen, dass diese Fälle keine "echten" Speicherlecks sind, weil es immer noch Verweise auf diese Objekte gibt, aber wenn das Programm diese Verweise nie wieder verwendet und sie auch nie wieder löscht, ist es völlig gleichbedeutend mit (und so schlecht wie) a " wahrer Speicherverlust ".
Ehabkost
@Nicolas Bousquet: "Speicherverlust ist in der Tat durchaus möglich" Vielen Dank. +15 positive Stimmen. Nett. Ich wurde hier angeschrien , weil ich diese Tatsache als Prämisse einer Frage zur Go-Sprache angegeben habe: stackoverflow.com/questions/4400311 Diese Frage hat immer noch negative Downvotes :(
SyntaxT3rr0r
Der GC in Java und .NET basiert in gewissem Sinne auf dem Annahme-Diagramm von Objekten, die Verweise auf andere Objekte enthalten, und dem Diagramm von Objekten, die sich um andere Objekte "kümmern". In der Realität ist es möglich, dass Kanten im Referenzdiagramm vorhanden sind, die keine "fürsorglichen" Beziehungen darstellen, und es ist möglich, dass sich ein Objekt um die Existenz eines anderen Objekts kümmert, selbst wenn kein direkter oder indirekter Referenzpfad (auch nicht verwendet WeakReference) vorhanden ist von einem zum anderen. Wenn eine Objektreferenz ein
Ersatzbit
... und das System Benachrichtigungen (über ähnliche Mittel wie PhantomReference) bereitstellen lassen, wenn festgestellt wurde, dass ein Objekt niemanden hat, der sich darum kümmert. WeakReferencekommt etwas nahe, muss aber in eine starke Referenz umgewandelt werden, bevor es verwendet werden kann; Wenn ein GC-Zyklus auftritt, während die starke Referenz vorhanden ist, wird angenommen, dass das Ziel nützlich ist.
Supercat
Dies ist meiner Meinung nach die richtige Antwort. Wir haben vor Jahren eine Simulation geschrieben. Irgendwie haben wir versehentlich den vorherigen Status mit dem aktuellen Status verknüpft, wodurch ein Speicherverlust verursacht wurde. Aufgrund einer Frist haben wir das Speicherleck nie behoben, sondern es durch Dokumentation zu einem «Feature» gemacht.
Nalply
163

Die Antwort hängt ganz davon ab, was der Interviewer zu fragen glaubte.

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

Aber es gibt mehrere Meta-Fragen, die möglicherweise 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 Speicherbereinigung funktioniert?
  • Oder wie soll die Speicherbereinigung 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 Speicherverwaltung ist und was in Java hinter den Kulissen vor sich geht?

Ich lese Ihre Meta-Frage als "Was ist eine Antwort, die ich in dieser Interview-Situation hätte verwenden können". Daher werde ich mich auf Interviewfähigkeiten anstatt auf Java konzentrieren. Ich glaube, Sie wiederholen eher die Situation, dass Sie die Antwort auf eine Frage in einem Interview nicht kennen, als dass Sie an einem Ort sein müssen, an dem Sie wissen müssen, wie Java ausläuft. Hoffentlich hilft das.

Eine der wichtigsten Fähigkeiten, die Sie für das Interview entwickeln können, ist das Lernen, aktiv auf die Fragen zu hören und mit dem Interviewer zusammenzuarbeiten, um deren Absicht zu extrahieren. Auf diese Weise 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 gleichermaßen talentierten Entwicklern geht, werde ich denjenigen einstellen, der zuhört, denkt und versteht, bevor sie jedes Mal antworten.

PlayTank
quelle
22
Wann immer ich diese Frage gestellt habe, suche ich nach einer ziemlich einfachen Antwort. Kommt auf den Job an, für den du interviewst, denke ich.
DaveC
Bitte werfen Sie einen Blick auf meine Frage, danke stackoverflow.com/questions/31108772/…
Daniel Newtown
130

Das Folgende ist ein ziemlich sinnloses Beispiel, wenn Sie JDBC nicht verstehen . Oder zumindest, wie JDBC erwartet, dass ein Entwickler geschlossen wird Connection, Statementund ResultSetInstanzen, bevor sie verworfen werden oder Verweise auf sie verloren gehen, anstatt sich auf die Implementierung von zu verlassen 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 das ConnectionObjekt nicht geschlossen ist und daher die physische Verbindung offen bleibt, bis der Garbage Collector vorbeikommt und feststellt, dass es nicht erreichbar ist. GC ruft die finalizeMethode auf, aber es gibt JDBC-Treiber, die das nicht implementieren finalize, zumindest nicht auf die gleiche Weise, wie Connection.closees implementiert ist. Das resultierende Verhalten ist, dass, während Speicher aufgrund nicht erreichbarer Objekte zurückgewonnen wird, Ressourcen (einschließlich Speicher), die dem ConnectionObjekt zugeordnet sind, möglicherweise einfach nicht zurückgefordert werden.

In einem solchen Fall, in dem Connectiondie finalizeMethode von ' nicht alles bereinigt, kann es tatsächlich vorkommen, dass die physische Verbindung zum Datenbankserver mehrere Speicherbereinigungszyklen dauert, bis der Datenbankserver schließlich feststellt, dass die Verbindung nicht aktiv ist (falls dies der Fall ist) tut) und sollte geschlossen werden.

Selbst wenn der JDBC-Treiber implementiert wird finalize, können während der Finalisierung Ausnahmen ausgelöst werden. Das resultierende Verhalten ist, dass jeglicher Speicher, der dem jetzt "ruhenden" Objekt zugeordnet ist, nicht zurückgefordert wird, da finalizegarantiert nur einmal aufgerufen wird.

Das obige Szenario, bei dem während der Objektfinalisierung Ausnahmen auftreten, hängt mit einem anderen Szenario zusammen, das möglicherweise zu einem Speicherverlust führen kann - der Objektauferstehung. Die Objektauferstehung erfolgt häufig absichtlich, indem ein starker Verweis auf das Objekt erstellt wird, das von einem anderen Objekt finalisiert wird. Wenn die Objektauferstehung missbraucht wird, führt dies in Kombination mit anderen Ursachen für Speicherverluste zu einem Speicherverlust.

Es gibt noch viele weitere Beispiele, die Sie heraufbeschwören können

  • Verwalten einer ListInstanz, in der Sie nur zur Liste hinzufügen und nicht aus dieser löschen (obwohl Sie nicht mehr benötigte Elemente entfernen sollten), oder
  • Öffnen von Sockets oder Files, aber nicht schließen, wenn sie nicht mehr benötigt werden (ähnlich dem obigen Beispiel für die ConnectionKlasse).
  • Singletons werden nicht entladen, wenn eine Java EE-Anwendung heruntergefahren wird. Anscheinend behält der Classloader, der die Singleton-Klasse geladen hat, einen Verweis auf die Klasse bei, und daher wird die Singleton-Instanz niemals gesammelt. Wenn eine neue Instanz der Anwendung bereitgestellt wird, wird normalerweise ein neuer Klassenlader erstellt, und der frühere Klassenlader bleibt aufgrund des Singletons weiterhin vorhanden.
Vineet Reynolds
quelle
98
Sie erreichen das maximale offene Verbindungslimit, bevor Sie normalerweise das Speicherlimit erreichen. Fragen Sie mich nicht, warum ich weiß ...
Hardwareguy
Der Oracle JDBC-Treiber ist dafür berüchtigt.
Chotchki
@Hardwareguy Ich habe die Verbindungsgrenzen der SQL-Datenbanken oft erreicht, bis ich sie Connection.closein den finally-Block aller meiner SQL-Aufrufe eingefügt habe . Für zusätzlichen Spaß habe ich einige lang laufende gespeicherte Oracle-Prozeduren aufgerufen, für die Sperren auf der Java-Seite erforderlich waren, um zu viele Aufrufe der Datenbank zu verhindern.
Michael Shopsin
@Hardwareguy Das ist interessant, aber es ist nicht wirklich notwendig, dass die tatsächlichen Verbindungslimits für alle Umgebungen erreicht werden. Zum Beispiel habe ich für eine Anwendung, die auf dem Weblogic-App-Server 11g bereitgestellt wurde, Verbindungslecks in großem Umfang festgestellt. Aufgrund der Möglichkeit, durchgesickerte Verbindungen zu sammeln, blieben Datenbankverbindungen verfügbar, während Speicherlecks eingeführt wurden. Ich bin mir nicht sicher über alle Umgebungen.
Aseem Bansal
Nach meiner Erfahrung tritt ein Speicherverlust auf, selbst wenn Sie die Verbindung schließen. Sie müssen zuerst ResultSet und PreparedStatement schließen. Hatte einen Server, der aufgrund von OutOfMemoryErrors nach Stunden oder sogar Tagen, an denen ich einwandfrei funktionierte, wiederholt abstürzte, bis ich damit anfing.
Bjørn Stenfeldt
119

Wahrscheinlich eines der einfachsten Beispiele für einen möglichen Speicherverlust und wie man ihn vermeidet, 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 gedacht, das nicht mehr verwendete Array-Element zu löschen ( elementData[--size] = null)? Diese Referenz könnte ein riesiges Objekt am Leben erhalten ...

meriton
quelle
5
Und wo ist das Speicherleck hier?
rds
28
@maniek: Ich wollte nicht implizieren, dass dieser Code einen Speicherverlust aufweist. Ich habe darauf zitiert, um zu zeigen, dass manchmal nicht offensichtlicher Code erforderlich ist, um eine versehentliche Objektaufbewahrung zu vermeiden.
Meriton
Was ist RangeCheck (Index)? ?
Koray Tugay
6
Joshua Bloch gab dieses Beispiel in Effective Java und zeigte eine einfache Implementierung von Stacks. Eine sehr gute Antwort.
mietet
Aber das wäre kein ECHTER Speicherverlust, selbst wenn er vergessen würde. Auf das Element würde weiterhin mit Reflection SICHER zugegriffen werden. Es wäre nur nicht offensichtlich und direkt über die List-Schnittstelle zugänglich, aber das Objekt und die Referenz sind immer noch vorhanden und können sicher aufgerufen werden.
DGoiko
68

Jedes Mal, wenn Sie Verweise auf Objekte behalten, die Sie nicht mehr benötigen, tritt ein Speicherverlust auf. Unter Umgang mit Speicherverlusten in Java-Programmen finden Sie Beispiele dafür, wie sich Speicherverluste in Java manifestieren und was Sie dagegen tun können.

Bill die Eidechse
quelle
14
Ich glaube nicht, dass dies ein "Leck" ist. Es ist ein Fehler, und es ist aufgrund des Programms und der Sprache. Ein Leck wäre ein herumhängender Gegenstand ohne Hinweise darauf.
user541686
29
@Mehrdad: Das ist nur eine enge Definition, die nicht für alle Sprachen gilt. Ich würde argumentieren, dass jeder Speicherverlust ein Fehler ist, der durch ein schlechtes Design des Programms verursacht wird.
Bill the Lizard
9
@Mehrdad: ...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. Ich verstehe nicht, wie Sie diese Schlussfolgerung ziehen. Es gibt weniger Möglichkeiten, einen Speicherverlust in Java per Definition zu erstellen. Es ist definitiv immer noch eine gültige Frage.
Bill the Lizard
7
@ 31eee384: Wenn Ihr Programm Objekte im Speicher behält, die es niemals verwenden kann, ist es technisch gesehen ein Speicherverlust. Die Tatsache, dass Sie größere Probleme haben, ändert daran nichts.
Bill the Lizard
8
@ 31eee384: Wenn Sie sicher sind, dass dies nicht der Fall ist, kann dies nicht der Fall sein. Das Programm wird, wie geschrieben, niemals auf die Daten zugreifen.
Bill the Lizard
51

Mit der Klasse sun.misc.Unsafe können Sie Speicherverluste verursachen . Tatsächlich wird diese Serviceklasse in verschiedenen Standardklassen verwendet (z. B. in java.nio- Klassen). Sie können keine Instanz dieser Klasse direkt erstellen , aber Sie können Reflection verwenden, um dies zu tun .

Code wird in Eclipse IDE nicht kompiliert - kompilieren Sie ihn mit dem Befehl javac(während der Kompilierung erhalten Sie Warnungen).

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}
Stamm
quelle
1
Der zugewiesene Speicher ist für den Garbage Collector unsichtbar
Stamm
4
Der zugewiesene Speicher gehört auch nicht zu Java.
Bests
Ist diese Sonne / Orakel jvm spezifisch? Wird dies beispielsweise bei IBM funktionieren?
Berlin Brown
2
Der Speicher gehört sicherlich "zu Java", zumindest in dem Sinne, dass i) er für niemanden verfügbar ist, ii) wenn die Java-Anwendung beendet wird, wird er an das System zurückgegeben. Es liegt direkt außerhalb der JVM.
Michael Anderson
3
Dies wird in Eclipse integriert (zumindest in neueren Versionen), aber Sie müssen die Compilereinstellungen ändern: In Fenster> Einstellungen> Java> Compiler> Fehler / Warnung> Veraltete und eingeschränkte API Verbotene Referenz (Zugriffsregeln) auf "Warnung" setzen ".
Michael Anderson
43

Ich kann meine Antwort von hier kopieren: Der einfachste Weg, um einen Speicherverlust in Java zu verursachen?

"Ein Speicherverlust in der Informatik (oder in diesem Zusammenhang ein Verlust) tritt auf, wenn ein Computerprogramm Speicher verbraucht, ihn jedoch nicht an das Betriebssystem zurückgeben kann." (Wikipedia)

Die einfache Antwort lautet: Sie können nicht. Java führt eine automatische Speicherverwaltung durch und gibt Ressourcen frei, die für Sie nicht benötigt werden. Sie können dies nicht verhindern. Es wird IMMER in der Lage sein, die Ressourcen freizugeben. Bei Programmen mit manueller Speicherverwaltung ist dies anders. Mit malloc () können Sie in C etwas Speicher abrufen. Um den Speicher freizugeben, benötigen Sie den Zeiger, den malloc zurückgegeben hat, und rufen Sie free () auf. Wenn Sie den Zeiger jedoch nicht mehr haben (überschrieben oder die Lebensdauer überschritten), können Sie diesen Speicher leider nicht freigeben, sodass ein Speicherverlust auftritt.

Alle anderen Antworten sind in meiner Definition nicht wirklich Speicherlecks. Sie alle zielen darauf ab, die Erinnerung sehr schnell mit sinnlosen Dingen zu füllen. Sie können die von Ihnen erstellten Objekte jedoch jederzeit dereferenzieren und so den Speicher freigeben -> NO LEAK. Die Antwort von acconrad kommt jedoch ziemlich nahe, wie ich zugeben muss, da seine Lösung darin besteht, den Müllsammler einfach "zum Absturz zu bringen", indem er in eine Endlosschleife gezwungen wird.

Die lange Antwort lautet: Sie können einen Speicherverlust erhalten, indem Sie eine Bibliothek für Java mit der JNI schreiben, die eine manuelle Speicherverwaltung und damit Speicherverluste aufweisen kann. Wenn Sie diese Bibliothek aufrufen, verliert Ihr Java-Prozess Speicher. Oder Sie können Fehler in der JVM haben, so dass die JVM Speicher verliert. Es gibt wahrscheinlich Fehler in der JVM, es kann sogar einige bekannte geben, da die Speicherbereinigung nicht so trivial ist, aber dann ist es immer noch ein Fehler. Dies ist konstruktionsbedingt nicht möglich. Möglicherweise fragen Sie nach Java-Code, der von einem solchen Fehler betroffen ist. Entschuldigung, ich kenne keinen und es könnte in der nächsten Java-Version sowieso kein Fehler mehr sein.

Yankee
quelle
12
Dies ist eine äußerst eingeschränkte (und nicht sehr nützliche) Definition von Speicherlecks. Die einzige Definition, die aus praktischen Gründen sinnvoll ist, lautet: "Ein Speicherverlust ist ein Zustand, in dem das Programm weiterhin den zugewiesenen Speicher hält, nachdem die darin enthaltenen Daten nicht mehr benötigt werden."
Mason Wheeler
1
Die Antwort des genannten Acconrad wurde gelöscht?
Tomáš Zato - Wiedereinsetzung Monica
1
@ TomášZato: Nein, ist es nicht. Ich habe die obige Referenz jetzt auf Link verlinkt, damit Sie sie leicht finden können.
Yankee
Was ist Objektauferstehung? Wie oft wird ein Destruktor gerufen? Wie widerlegen diese Fragen diese Antwort?
autistische
1
Sicher, Sie können trotz GC trotz Java einen Speicherverlust in Java verursachen und trotzdem die obige Definition erfüllen. Haben Sie nur eine Nur-Anhängen-Datenstruktur, die gegen externen Zugriff geschützt ist, damit kein anderer Code sie entfernt - das Programm kann den Speicher nicht freigeben, da es nicht über den Code verfügt.
Werkzeugschmiede
39

Hier ist eine einfache / unheimliche über http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 .

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

Da sich die Teilzeichenfolge auf die interne Darstellung der ursprünglichen, viel längeren Zeichenfolge bezieht, bleibt das Original im Speicher. Solange Sie also einen StringLeaker im Spiel haben, haben Sie auch die gesamte Originalzeichenfolge im Speicher, auch wenn Sie vielleicht denken, dass Sie nur an einer Zeichenfolge mit einem Zeichen festhalten.

Um zu vermeiden, dass ein unerwünschter Verweis auf die ursprüngliche Zeichenfolge gespeichert wird, gehen Sie folgendermaßen vor:

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

Für zusätzliche Schlechtigkeit könnten Sie auch .intern()den Teilstring verwenden:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

Dadurch bleiben sowohl die ursprüngliche lange Zeichenfolge als auch die abgeleitete Teilzeichenfolge im Speicher, auch nachdem die StringLeaker-Instanz verworfen wurde.

Jon Chambers
quelle
4
Ich würde das per se nicht als Speicherverlust bezeichnen . Wenn muchSmallerStringfreigegeben wird (weil das StringLeakerObjekt zerstört ist), wird auch die lange Zeichenfolge freigegeben. Was ich als Speicherverlust bezeichne, ist Speicher, der in dieser Instanz von JVM niemals freigegeben werden kann. Sie haben sich jedoch gezeigt, wie Sie den Speicher freigeben können : this.muchSmallerString=new String(this.muchSmallerString). Mit einem echten Speicherverlust können Sie nichts tun.
rds
2
@rds, das ist ein fairer Punkt. Der Nichtfall internkann eher eine "Speicherüberraschung" als ein "Speicherverlust" sein. .intern()Durch die Verwendung des Teilstrings entsteht jedoch mit Sicherheit eine Situation, in der der Verweis auf den längeren String erhalten bleibt und nicht freigegeben werden kann.
Jon Chambers
15
Die Methode substring () erstellt einen neuen String in Java7 (es ist ein neues Verhalten)
anstarovoyt
Sie müssen den Teilstring () nicht einmal selbst ausführen: Verwenden Sie einen Regex-Matcher, um einen winzigen Teil einer großen Eingabe abzugleichen, und tragen Sie den "extrahierten" String lange herum. Der riesige Input bleibt bis Java 6 am Leben.
Bananeweizen
37

Ein häufiges Beispiel hierfür im GUI-Code ist das Erstellen eines Widgets / einer Komponente und das Hinzufügen eines Listeners zu einem statischen / anwendungsbezogenen Objekt und das anschließende Entfernen des Listeners, wenn das Widget zerstört wird. Sie erhalten nicht nur einen Speicherverlust, sondern auch einen Leistungseinbruch, wenn alle Ihre alten Zuhörer aufgerufen werden, wenn Sie Ereignisse auslösen.

pauli
quelle
1
Die Android-Plattform bietet das Beispiel eines Speicherverlusts, der durch das Zwischenspeichern einer Bitmap im statischen Feld einer Ansicht entsteht .
rds
36

Nehmen Sie eine beliebige Webanwendung, die in einem beliebigen Servlet-Container ausgeführt wird (Tomcat, Jetty, Glassfish, was auch immer ...). Stellen Sie die App 10 oder 20 Mal hintereinander erneut bereit (es kann ausreichen, einfach die WAR im Autodeploy-Verzeichnis des Servers zu berühren.

Wenn dies nicht tatsächlich getestet wurde, ist die Wahrscheinlichkeit hoch, dass Sie nach einigen erneuten Bereitstellungen einen OutOfMemoryError erhalten, da die Anwendung nicht darauf geachtet hat, nach sich selbst zu bereinigen. Möglicherweise finden Sie bei diesem Test sogar einen Fehler auf Ihrem Server.

Das Problem ist, dass die Lebensdauer des Containers länger ist als die Lebensdauer Ihrer Anwendung. Sie müssen sicherstellen, dass alle Verweise, die der Container möglicherweise auf Objekte oder Klassen Ihrer Anwendung hat, durch Müll gesammelt werden können.

Wenn es nur eine Referenz gibt, die die Nichtbereitstellung Ihrer Webanwendung überlebt, kann der entsprechende Klassenladeprogramm und folglich alle Klassen Ihrer Webanwendung nicht durch Müll gesammelt werden.

Von Ihrer Anwendung gestartete Threads, ThreadLocal-Variablen und Protokollierungs-Appender sind einige der üblichen Verdächtigen, die zu Classloader-Lecks führen.

Harald Wellmann
quelle
1
Dies liegt nicht an einem Speicherverlust, sondern daran, dass der Klassenlader den vorherigen Satz von Klassen nicht entlädt. Daher wird nicht empfohlen, einen Anwendungsserver erneut bereitzustellen, ohne den Server neu zu starten (nicht den physischen Computer, sondern den App-Server). Ich habe das gleiche Problem mit WebSphere gesehen.
Sven
35

Vielleicht durch Verwendung von externem nativem Code über JNI?

Mit reinem Java ist das fast unmöglich.

Es handelt sich jedoch um einen "Standard" -Speicherverlust, bei dem Sie nicht mehr auf den Speicher zugreifen können, der sich jedoch weiterhin im Besitz der Anwendung befindet. Sie können stattdessen Verweise auf nicht verwendete Objekte beibehalten oder Streams öffnen, ohne sie anschließend zu schließen.

Rogach
quelle
22
Das hängt von der Definition von "Speicherverlust" ab. Wenn "Speicher, der festgehalten wird, aber nicht mehr benötigt wird", ist dies in Java einfach. Wenn es sich um "Speicher handelt, der zugewiesen ist, auf den der Code jedoch überhaupt nicht zugreifen kann", wird es etwas schwieriger.
Joachim Sauer
@ Joachim Sauer - ich meinte den zweiten Typ. Das erste ist ziemlich einfach zu machen :)
Rogach
6
"Mit reinem Java ist das fast unmöglich." Nun, meine Erfahrung ist eine andere, insbesondere wenn es darum geht, Caches von Leuten zu implementieren, die sich der Fallstricke hier nicht bewusst sind.
Fabian Barney
4
@ Rogach: Grundsätzlich gibt es +400 Upvotes zu verschiedenen Antworten von Personen mit +10 000 Wiederholungen, was zeigt, dass Joachim Sauer in beiden Fällen kommentierte, dass dies sehr gut möglich ist. Ihr "fast unmöglich" macht also keinen Sinn.
SyntaxT3rr0r
32

Ich hatte einmal einen schönen "Speicherverlust" in Bezug auf das Parsen von PermGen und XML. Der von uns verwendete XML-Parser (ich kann mich nicht erinnern, welcher es war) hat eine String.intern () für Tag-Namen ausgeführt, um den Vergleich zu beschleunigen. Einer unserer Kunden hatte die großartige Idee, Datenwerte nicht in XML-Attributen oder Text, sondern als Tagnamen zu speichern. Wir hatten also ein Dokument wie:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

Tatsächlich verwendeten sie keine Zahlen, sondern längere Text-IDs (ca. 20 Zeichen), die eindeutig waren und mit einer Rate von 10 bis 15 Millionen pro Tag eingingen. Das macht 200 MB Müll pro Tag, der nie wieder benötigt und nie GCed wird (da es in PermGen ist). Wir hatten Permgen auf 512 MB eingestellt, daher dauerte es ungefähr zwei Tage, bis die Out-of-Memory-Ausnahme (OOME) eintraf ...

Ron
quelle
4
Nur um Ihren Beispielcode nicht auszuwählen: Ich denke, Zahlen (oder Zeichenfolgen, die mit Zahlen beginnen) sind in XML nicht als Elementnamen zulässig.
Paŭlo Ebermann
Beachten Sie, dass dies für JDK 7+ nicht mehr gilt, bei dem die String-Internierung auf dem Heap erfolgt. In diesem Artikel finden Sie eine ausführliche Beschreibung: java-performance.info/string-intern-in-java-6-7-8
jmiserez
Ich denke also, dass die Verwendung von StringBuffer anstelle von String dieses Problem lösen würde? wird es nicht?
vor
24

Was ist ein Speicherverlust:

  • Es ist durch einen Fehler oder ein schlechtes Design verursacht.
  • Es ist eine Verschwendung von Erinnerung.
  • Mit der Zeit wird es schlimmer.
  • Der Garbage Collector kann es nicht reinigen.

Typisches Beispiel:

Ein Cache mit Objekten ist ein guter Ausgangspunkt, um Dinge durcheinander zu bringen.

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

Ihr Cache wächst und wächst. Und ziemlich bald wird die gesamte Datenbank in den Speicher gesaugt. Ein besseres Design verwendet eine LRUMap (hält nur kürzlich verwendete Objekte im Cache).

Sicher, Sie können die Dinge viel komplizierter machen:

  • Verwenden von ThreadLocal- Konstruktionen.
  • Hinzufügen komplexerer Referenzbäume .
  • oder Lecks, die durch Bibliotheken von Drittanbietern verursacht wurden .

Was oft passiert:

Wenn dieses Info-Objekt Verweise auf andere Objekte enthält, die wiederum Verweise auf andere Objekte enthalten. In gewisser Weise könnte man dies auch als eine Art Speicherverlust betrachten (verursacht durch schlechtes Design).

bvdb
quelle
22

Ich fand es interessant, dass niemand die internen Klassenbeispiele verwendete. Wenn Sie eine interne Klasse haben; Es enthält von Natur aus einen Verweis auf die enthaltende Klasse. Natürlich handelt es sich technisch gesehen nicht um einen Speicherverlust, da Java ihn schließlich bereinigen wird. Dies kann jedoch dazu führen, dass Klassen länger als erwartet herumhängen.

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

Wenn Sie nun Beispiel1 aufrufen und ein Beispiel2 erhalten, das Beispiel1 verwirft, haben Sie von Natur aus immer noch einen Link zu einem Beispiel1-Objekt.

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

Ich habe auch ein Gerücht gehört, dass wenn Sie eine Variable haben, die länger als eine bestimmte Zeit existiert; Java geht davon aus, dass es immer existiert und niemals versucht, es zu bereinigen, wenn es im Code nicht mehr erreichbar ist. Das ist aber völlig unbestätigt.

Suroot
quelle
2
innere Klassen sind selten ein Thema. Sie sind ein einfacher Fall und sehr leicht zu erkennen. Das Gerücht ist auch nur ein Gerücht.
Bests
2
Das "Gerücht" klingt so, als hätte jemand halb gelesen, wie Generations-GC funktioniert. Langlebige, aber jetzt nicht erreichbare Objekte können tatsächlich eine Weile in der Nähe bleiben und Platz beanspruchen, da die JVM sie aus den jüngeren Generationen heraus befördert hat, sodass sie nicht mehr bei jedem Durchgang überprüft werden müssen. Sie werden den piddly "bereinigen meine 5000 temporären Saiten" Pässe von Design aus entgehen. Aber sie sind nicht unsterblich. Sie können weiterhin erfasst werden. Wenn die VM auf RAM beschränkt ist, wird sie schließlich einen vollständigen GC-Sweep ausführen und diesen Speicher wieder in Anspruch nehmen.
CHao
22

Ich habe kürzlich eine Speicherverlustsituation festgestellt, die in gewisser Weise durch log4j verursacht wurde.

Log4j verfügt über diesen Mechanismus namens Nested Diagnostic Context (NDC) , ein Instrument zur Unterscheidung von verschachtelten Protokollausgaben aus verschiedenen Quellen. Die Granularität, mit der NDC arbeitet, sind Threads, sodass Protokollausgaben von verschiedenen Threads separat unterschieden werden.

Um threadspezifische Tags zu speichern, verwendet die NDC-Klasse von log4j eine Hashtable, die vom Thread-Objekt selbst (im Gegensatz zur Thread-ID) verschlüsselt wird, und somit alle Objekte, die vom Thread abhängen, bis das NDC-Tag im Speicher bleibt Objekt bleiben auch im Speicher. In unserer Webanwendung verwenden wir NDC, um Protokollausgänge mit einer Anforderungs-ID zu kennzeichnen, um Protokolle von einer einzelnen Anforderung separat zu unterscheiden. Der Container, der das NDC-Tag einem Thread zuordnet, entfernt es ebenfalls, während die Antwort von einer Anforderung zurückgegeben wird. Das Problem trat auf, als während der Verarbeitung einer Anforderung ein untergeordneter Thread erzeugt wurde, etwa der folgende Code:

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

Daher wurde ein NDC-Kontext mit dem Inline-Thread verknüpft, der erzeugt wurde. Das Thread-Objekt, das der Schlüssel für diesen NDC-Kontext war, ist der Inline-Thread, an dem das riesigeList-Objekt hängt. Selbst nachdem der Thread das getan hatte, was er tat, wurde der Verweis auf die riesige Liste durch den NDC-Kontext Hastable am Leben erhalten, was zu einem Speicherverlust führte.

Puneet
quelle
Das ist Scheiße. Sie sollten diese Protokollierungsbibliothek überprüfen, die während der Protokollierung in einer Datei NULL
TraderJoeChicago
+1 Wissen Sie sofort, ob es ein ähnliches Problem mit dem MDC in slf4j / logback gibt (Nachfolgeprodukte desselben Autors)? Ich bin kurz davor, tief in die Quelle einzutauchen, wollte es aber zuerst überprüfen. Wie auch immer, danke, dass du das gepostet hast.
sparc_spread
20

Der Interviewer suchte wahrscheinlich nach einem Zirkelverweis wie dem folgenden Code (der übrigens nur in sehr alten JVMs, die Referenzzählungen verwendeten, Speicher verliert, was nicht mehr der Fall ist). Aber es ist eine ziemlich vage Frage, daher ist es eine hervorragende Gelegenheit, Ihr Verständnis der JVM-Speicherverwaltung zu demonstrieren.

class A {
    B bRef;
}

class B {
    A aRef;
}

public class Main {
    public static void main(String args[]) {
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
    } /* main */
}

Dann können Sie erklären, dass beim Referenzzählen der obige Code Speicher verlieren würde. Die meisten modernen JVMs verwenden jedoch keine Referenzzählung mehr. Die meisten verwenden einen Sweep-Garbage-Collector, der diesen Speicher tatsächlich sammelt.

Als Nächstes können Sie das Erstellen eines Objekts mit einer zugrunde liegenden nativen Ressource folgendermaßen erläutern:

public class Main {
    public static void main(String args[]) {
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    }
}

Dann können Sie erklären, dass dies technisch gesehen ein Speicherverlust ist, der jedoch tatsächlich durch nativen Code in der JVM verursacht wird, der zugrunde liegende native Ressourcen zuweist, die von Ihrem Java-Code nicht freigegeben wurden.

Letztendlich müssen Sie mit einer modernen JVM Java-Code schreiben, der eine native Ressource außerhalb des normalen Bereichs der Kenntnis der JVM zuweist.

deltamind106
quelle
19

Jeder vergisst immer die native Code-Route. Hier ist eine einfache Formel für ein Leck:

  1. Deklarieren Sie die native Methode.
  2. Rufen Sie in der nativen Methode auf malloc. Ruf nicht an free.
  3. Rufen Sie die native Methode auf.

Denken Sie daran, dass die Speicherzuweisungen im nativen Code vom JVM-Heap stammen.

Paul Morie
quelle
1
Basierend auf einer wahren Geschichte.
Reg
18

Erstellen Sie eine statische Karte und fügen Sie immer wieder harte Referenzen hinzu. Diese werden niemals GC'd sein.

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}
Duffymo
quelle
87
Wie ist das ein Leck? Es macht genau das, was Sie von ihm verlangen. Wenn dies ein Leck ist, ist das Erstellen und Speichern von Objekten überall ein Leck.
Falmarri
3
Ich stimme @Falmarri zu. Ich sehe dort kein Leck, Sie erstellen nur Objekte. Sie könnten den Speicher, den Sie gerade zugewiesen haben, mit einer anderen Methode namens "removeFromCache" "zurückfordern". Ein Leck liegt vor, wenn Sie den Speicher nicht zurückfordern können.
Kyle
3
Mein Punkt ist, dass jemand, der ständig Objekte erstellt und sie möglicherweise in einen Cache legt, einen OOM-Fehler erleiden kann, wenn er nicht vorsichtig ist.
Duffymo
8
@duffymo: Aber das war nicht wirklich das, was die Frage stellte. Es hat nichts damit zu tun, einfach den gesamten Speicher zu verbrauchen.
Falmarri
3
Absolut ungültig. Sie sammeln nur eine Reihe von Objekten in einer Kartensammlung. Ihre Referenzen werden beibehalten, da die Karte sie enthält.
Gyorgyabraham
16

Sie können einen beweglichen Speicherverlust erstellen, indem Sie eine neue Instanz einer Klasse in der Finalisierungsmethode dieser Klasse erstellen. Bonuspunkte, wenn der Finalizer mehrere Instanzen erstellt. Hier ist ein einfaches Programm, das den gesamten Heap in Abhängigkeit von Ihrer Heap-Größe zwischen einigen Sekunden und einigen Minuten verliert:

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}
Sethobrien
quelle
15

Ich glaube, noch hat niemand dies gesagt: Sie können ein Objekt wiederbeleben, indem Sie die Methode finalize () überschreiben, sodass finalize () irgendwo eine Referenz darauf speichert. Der Garbage Collector wird nur einmal für das Objekt aufgerufen, sodass das Objekt danach nie mehr zerstört wird.

Ben
quelle
10
Das ist falsch. finalize()wird nicht aufgerufen, aber das Objekt wird gesammelt, sobald keine Referenzen mehr vorhanden sind. Garbage Collector wird auch nicht "genannt".
Bests
1
Diese Antwort ist irreführend. Die finalize()Methode kann von der JVM nur einmal aufgerufen werden. Dies bedeutet jedoch nicht, dass sie nicht erneut gesammelt werden kann, wenn das Objekt wiederbelebt und dann erneut dereferenziert wird. Wenn die finalize()Methode einen Code zum Schließen von Ressourcen enthält, wird dieser Code nicht erneut ausgeführt. Dies kann zu einem Speicherverlust führen.
Tom Cammann
15

Ich bin kürzlich auf eine subtilere Art von Ressourcenleck gestoßen. Wir öffnen Ressourcen über getResourceAsStream des Klassenladers und es kam vor, dass die Eingabestream-Handles nicht geschlossen wurden.

Ähm, könnte man sagen, was für ein Idiot.

Nun, was dies interessant macht, ist: Auf diese Weise können Sie den Heap-Speicher des zugrunde liegenden Prozesses und nicht den Heap von JVM verlieren.

Sie benötigen lediglich eine JAR-Datei mit einer Datei, auf die aus Java-Code verwiesen wird. Je größer die JAR-Datei ist, desto schneller wird Speicher zugewiesen.

Sie können ein solches Glas ganz einfach mit der folgenden Klasse erstellen:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

Fügen Sie einfach eine Datei mit dem Namen BigJarCreator.java ein, kompilieren Sie sie und führen Sie sie über die Befehlszeile aus:

javac BigJarCreator.java
java -cp . BigJarCreator

Et voilà: Sie finden in Ihrem aktuellen Arbeitsverzeichnis ein JAR-Archiv mit zwei Dateien.

Erstellen wir eine zweite Klasse:

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

Diese Klasse macht im Grunde nichts anderes, als nicht referenzierte InputStream-Objekte zu erstellen. Diese Objekte werden sofort als Müll gesammelt und tragen somit nicht zur Größe des Heapspeichers bei. In unserem Beispiel ist es wichtig, eine vorhandene Ressource aus einer JAR-Datei zu laden, und die Größe spielt hier eine Rolle!

Wenn Sie Zweifel haben, versuchen Sie, die obige Klasse zu kompilieren und zu starten, wählen Sie jedoch eine angemessene Heap-Größe (2 MB):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

Hier tritt kein OOM-Fehler auf, da keine Referenzen gespeichert werden und die Anwendung weiterhin ausgeführt wird, unabhängig davon, wie groß Sie im obigen Beispiel ITERATIONS ausgewählt haben. Der Speicherverbrauch Ihres Prozesses (oben sichtbar (RES / RSS) oder Prozess-Explorer) steigt, es sei denn, die Anwendung erhält den Befehl wait. Im obigen Setup werden ca. 150 MB Speicher zugewiesen.

Wenn die Anwendung auf Nummer sicher gehen soll, schließen Sie den Eingabestream genau dort, wo er erstellt wurde:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

und Ihr Prozess wird unabhängig von der Anzahl der Iterationen 35 MB nicht überschreiten.

Ganz einfach und überraschend.

Jay
quelle
14

Wie viele Leute vorgeschlagen haben, sind Ressourcenlecks ziemlich leicht zu verursachen - wie die JDBC-Beispiele. Tatsächliche Speicherverluste sind etwas schwieriger - insbesondere, wenn Sie sich nicht auf defekte Teile der JVM verlassen, um dies für Sie zu tun ...

Die Idee, Objekte mit einem sehr großen Platzbedarf zu erstellen und dann nicht darauf zugreifen zu können, ist ebenfalls kein wirklicher Speicherverlust. Wenn nichts darauf zugreifen kann, wird Müll gesammelt, und wenn etwas darauf zugreifen kann, ist es kein Leck ...

Eine Möglichkeit, die früher funktioniert hat - und ich weiß nicht, ob dies immer noch der Fall ist -, ist eine dreifache kreisförmige Kette. Wie in Objekt A eine Referenz auf Objekt B hat, hat Objekt B eine Referenz auf Objekt C und Objekt C hat eine Referenz auf Objekt A. Der GC war klug genug zu wissen, dass eine zwei tiefe Kette - wie in A - B - kann sicher gesammelt werden, wenn A und B für nichts anderes zugänglich sind, aber die Dreiwegekette nicht handhaben können ...

Graham
quelle
7
Ist seit einiger Zeit nicht mehr der Fall. Moderne GCs wissen, wie man mit Zirkelverweisen umgeht.
Assylias
13

Eine andere Möglichkeit, potenziell große Speicherlecks zu erstellen, besteht darin, Verweise auf Map.Entry<K,V>a zu speichern TreeMap.

Es ist schwer zu beurteilen, warum dies nur für TreeMaps gilt, aber wenn man sich die Implementierung ansieht, könnte der Grund sein, dass: a TreeMap.EntryVerweise auf seine Geschwister speichert, daher, wenn a TreeMapzur Abholung bereit ist, aber eine andere Klasse einen Verweis auf eine von enthält sein Map.Entry, dann ist die gesamte wird Karte beibehalten in den Speicher werden.


Reales Szenario:

Stellen Sie sich eine Datenbankabfrage vor, die eine Big- TreeMapData-Struktur zurückgibt . Normalerweise wird TreeMaps verwendet, da die Reihenfolge des Einfügens von Elementen beibehalten wird.

public static Map<String, Integer> pseudoQueryDatabase();

Wenn die Abfrage viele Male aufgerufen wurde und Sie für jede Abfrage (also für jede Mapzurückgegebene Abfrage ) Entryirgendwo etwas speichern, wächst der Speicher ständig weiter.

Betrachten Sie die folgende Wrapper-Klasse:

class EntryHolder {
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) {
        this.entry = entry;
    }
}

Anwendung:

public class LeakTest {

    private final List<EntryHolder> holdersCache = new ArrayList<>();
    private static final int MAP_SIZE = 100_000;

    public void run() {
        // create 500 entries each holding a reference to an Entry of a TreeMap
        IntStream.range(0, 500).forEach(value -> {
            // create map
            final Map<String, Integer> map = pseudoQueryDatabase();

            final int index = new Random().nextInt(MAP_SIZE);

            // get random entry from map
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue().equals(index)) {
                    holdersCache.add(new EntryHolder(entry));
                    break;
                }
            }
            // to observe behavior in visualvm
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    public static Map<String, Integer> pseudoQueryDatabase() {
        final Map<String, Integer> map = new TreeMap<>();
        IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
        return map;
    }

    public static void main(String[] args) throws Exception {
        new LeakTest().run();
    }
}

Nach jedem pseudoQueryDatabase()Aufruf sollten die mapInstanzen zur Erfassung bereit sein, dies wird jedoch nicht geschehen, da mindestens eine Instanz an einer Entryanderen Stelle gespeichert ist.

Abhängig von Ihren jvmEinstellungen kann die Anwendung aufgrund von a in einem frühen Stadium abstürzen OutOfMemoryError.

In dieser visualvmGrafik können Sie sehen, wie der Speicher weiter wächst.

Speicherauszug - TreeMap

Dasselbe passiert nicht mit einer Hash-Datenstruktur ( HashMap).

Dies ist das Diagramm bei Verwendung von a HashMap.

Speicherauszug - HashMap

Die Lösung? Speichern Sie einfach direkt den Schlüssel / Wert (wie Sie es wahrscheinlich bereits tun), anstatt den zu speichern Map.Entry.


Ich habe eine umfangreiche Benchmark geschrieben hier .

Marko Pacak
quelle
11

Threads werden erst gesammelt, wenn sie beendet sind. Sie dienen als Wurzeln der Müllabfuhr. Sie sind eines der wenigen Objekte, die nicht einfach durch Vergessen oder Löschen von Verweisen auf sie zurückgefordert werden.

Bedenken Sie: Das grundlegende Muster zum Beenden eines Arbeitsthreads besteht darin, eine Bedingungsvariable festzulegen, die vom Thread gesehen wird. Der Thread kann die Variable regelmäßig überprüfen und als Signal zum Beenden verwenden. Wenn die Variable nicht deklariert ist volatile, wird die Änderung der Variablen möglicherweise vom Thread nicht gesehen, sodass er nicht wissen kann, ob sie beendet werden soll. Oder stellen Sie sich vor, einige Threads möchten ein freigegebenes Objekt aktualisieren, aber beim Versuch, es zu sperren, blockieren.

Wenn Sie nur eine Handvoll Threads haben, sind diese Fehler wahrscheinlich offensichtlich, da Ihr Programm nicht mehr richtig funktioniert. Wenn Sie über einen Thread-Pool verfügen, der nach Bedarf weitere Threads erstellt, werden die veralteten / feststeckenden Threads möglicherweise nicht bemerkt und sammeln sich auf unbestimmte Zeit an, was zu einem Speicherverlust führt. Threads verwenden wahrscheinlich andere Daten in Ihrer Anwendung, sodass auch verhindert wird, dass Daten, auf die sie direkt verweisen, jemals erfasst werden.

Als Spielzeugbeispiel:

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

Rufen System.gc()Sie alles an, was Sie möchten, aber das übergebene Objekt leakMewird niemals sterben.

(* bearbeitet *)

Boann
quelle
1
@Spidey Nichts steckt "fest". Die aufrufende Methode wird sofort zurückgegeben, und das übergebene Objekt wird niemals zurückgefordert. Das ist genau ein Leck.
Boann
1
Sie haben einen Thread, der während der gesamten Lebensdauer Ihres Programms "läuft" (oder schläft, was auch immer). Das zählt für mich nicht als Leck. Ebenso zählt ein Pool nicht als Leck, auch wenn Sie ihn nicht vollständig nutzen.
Spidey
1
@Spidey "Sie haben ein [Ding] für die Lebensdauer Ihres Programms. Das zählt für mich nicht als Leck." Hörst du dich
Boann
3
@Spidey Wenn Sie Speicher zählen würden, von dem der Prozess weiß, dass er nicht durchgesickert ist, sind alle Antworten hier falsch, da der Prozess immer verfolgt, welche Seiten in seinem virtuellen Adressraum zugeordnet sind. Wenn der Prozess beendet ist, bereinigt das Betriebssystem alle Lecks, indem die Seiten wieder auf den freien Seitenstapel gelegt werden. Um dies auf das nächste Extrem zu bringen, könnte man jedes argumentierte Leck zu Tode schlagen, indem man darauf hinweist, dass keines der physischen Bits in den RAM-Chips oder im Swap-Bereich auf der Festplatte physisch verlegt oder zerstört wurde, sodass Sie den Computer ausschalten können und wieder an, um eventuelle Undichtigkeiten zu beseitigen.
Boann
1
Die praktische Definition eines Lecks ist, dass sein Speicher den Überblick verloren hat, sodass wir ihn nicht kennen und daher nicht das Verfahren ausführen können, das erforderlich ist, um nur ihn zurückzugewinnen. Wir müssten den gesamten Speicherplatz abreißen und neu aufbauen. Ein solcher Rogue-Thread kann auf natürliche Weise durch einen Deadlock oder eine zweifelhafte Threadpool-Implementierung entstehen. Objekte, auf die von solchen Threads auch indirekt verwiesen wird, können jetzt nicht mehr erfasst werden, sodass wir über Speicher verfügen, der während der Laufzeit des Programms nicht auf natürliche Weise zurückgefordert oder wiederverwendet werden kann. Ich würde das ein Problem nennen; Insbesondere ist es ein Speicherverlust.
Boann
10

Ich denke, dass ein gültiges Beispiel die Verwendung von ThreadLocal-Variablen in einer Umgebung sein könnte, in der Threads zusammengefasst werden.

Verwenden Sie beispielsweise ThreadLocal-Variablen in Servlets, um mit anderen Webkomponenten zu kommunizieren, wobei die Threads vom Container erstellt werden und die inaktiven Threads in einem Pool verwaltet werden. ThreadLocal-Variablen werden dort gespeichert, wenn sie nicht korrekt bereinigt wurden, bis möglicherweise dieselbe Webkomponente ihre Werte überschreibt.

Einmal identifiziert, kann das Problem natürlich leicht gelöst werden.

mschonaker
quelle
10

Der Interviewer hat möglicherweise nach einer Zirkelreferenzlösung gesucht:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

Dies ist ein klassisches Problem bei der Referenzzählung von Garbage Collectors. Sie würden dann höflich erklären, dass JVMs einen viel ausgefeilteren Algorithmus verwenden, der diese Einschränkung nicht aufweist.

-Wes Tarle

Wesley Tarle
quelle
12
Dies ist ein klassisches Problem bei der Referenzzählung von Garbage Collectors. Noch vor 15 Jahren verwendete Java keine Ref-Zählung. Ref. Zählen ist auch langsamer als GC.
Bests
4
Kein Speicherverlust. Nur eine Endlosschleife.
Esben Skov Pedersen
2
@Esben Bei jeder Iteration ist die vorherige firstnicht nützlich und sollte durch Müll gesammelt werden. Bei der Referenzzählung von Garbage Collectors wird das Objekt nicht freigegeben, da eine aktive Referenz darauf (für sich selbst) vorhanden ist. Die Endlosschleife dient dazu, das Leck zu demonstrieren: Wenn Sie das Programm ausführen, wird der Speicher auf unbestimmte Zeit erhöht.
rds
@rds @ Wesley Tarle Angenommen, die Schleife war nicht unendlich. Würde es immer noch einen Speicherverlust geben?
nz_21