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?
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?
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:
ClassLoader
.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.ClassLoader
sie geladen wurde.Aufgrund der Art und Weise, wie ThreadLocal
es im JDK von Oracle implementiert ist, entsteht ein Speicherverlust:
Thread
hat ein privates Feld threadLocals
, in dem die threadlokalen Werte gespeichert sind.ThreadLocal
Objekt. Nachdem dieses ThreadLocal
Objekt durch Müll gesammelt wurde, wird sein Eintrag aus der Karte entfernt.ThreadLocal
Objekt 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:
Thread
Objekt → threadLocals
Karte → Instanz der Beispielklasse → Beispielklasse → statisches ThreadLocal
Feld → ThreadLocal
Objekt.
(Das ClassLoader
spielt 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 ClassLoader
s 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 ThreadLocal
s 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 .
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 noclassgc
Option in IBM JDK, die die nicht verwendete Klassenbereinigung verhindert
Siehe IBM JDK-Einstellungen .
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.
intern
Hashtabelleninhalt hat. Als solches ist es ist Müll richtig und nicht ein Leck gesammelt. (aber IANAJP) mindprod.com/jgloss/interned.html#GC
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.
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, 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.addShutdownHook
und 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 Thread
zur gleichen Kategorie wie oben.
Das Erstellen eines Threads erbt das ContextClassLoader
und AccessControlContext
sowie das ThreadGroup
und 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 ThreadFactory
Benutzeroberflä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).
ThreadLocal
Caches; 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.URL
mit dem HTTP (S) -Protokoll und Laden der Ressource von (!). Dies ist etwas Besonderes, das KeepAliveCache
einen 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 InflaterInputStream
Bestehen new java.util.zip.Inflater()
im Konstruktor ( PNGImageDecoder
zum 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 Deflater
ist es nicht weit verbreitet und meines Wissens enthält JDK keine Missbräuche. Rufen end()
Sie immer an, wenn Sie manuell ein Deflater
oder 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!
Creating but not starting a Thread...
Huch, ich wurde vor einigen Jahrhunderten von diesem schwer gebissen! (Java 1.3)
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)
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.
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.
WeakReference
) vorhanden ist von einem zum anderen. Wenn eine Objektreferenz ein
PhantomReference
) bereitstellen lassen, wenn festgestellt wurde, dass ein Objekt niemanden hat, der sich darum kümmert. WeakReference
kommt 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.
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?
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.
Das Folgende ist ein ziemlich sinnloses Beispiel, wenn Sie JDBC nicht verstehen . Oder zumindest, wie JDBC erwartet, dass ein Entwickler geschlossen wird Connection
, Statement
und ResultSet
Instanzen, 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 Connection
Objekt 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 finalize
Methode auf, aber es gibt JDBC-Treiber, die das nicht implementieren finalize
, zumindest nicht auf die gleiche Weise, wie Connection.close
es implementiert ist. Das resultierende Verhalten ist, dass, während Speicher aufgrund nicht erreichbarer Objekte zurückgewonnen wird, Ressourcen (einschließlich Speicher), die dem Connection
Objekt zugeordnet sind, möglicherweise einfach nicht zurückgefordert werden.
In einem solchen Fall, in dem Connection
die finalize
Methode 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 finalize
garantiert 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
List
Instanz, in der Sie nur zur Liste hinzufügen und nicht aus dieser löschen (obwohl Sie nicht mehr benötigte Elemente entfernen sollten), oderSocket
s oder File
s, aber nicht schließen, wenn sie nicht mehr benötigt werden (ähnlich dem obigen Beispiel für die Connection
Klasse).Connection.close
in 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.
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 ...
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.
...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.
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();
}
}
}
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.
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.
muchSmallerString
freigegeben wird (weil das StringLeaker
Objekt 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.
intern
kann 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.
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.
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.
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.
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 ...
Was ist ein Speicherverlust:
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:
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).
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.
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.
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.
Jeder vergisst immer die native Code-Route. Hier ist eine einfache Formel für ein Leck:
malloc
. Ruf nicht an free
.Denken Sie daran, dass die Speicherzuweisungen im nativen Code vom JVM-Heap stammen.
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); }
}
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());
}
}
}
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.
finalize()
wird nicht aufgerufen, aber das Objekt wird gesammelt, sobald keine Referenzen mehr vorhanden sind. Garbage Collector wird auch nicht "genannt".
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.
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.
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 ...
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 TreeMap
s gilt, aber wenn man sich die Implementierung ansieht, könnte der Grund sein, dass: a TreeMap.Entry
Verweise auf seine Geschwister speichert, daher, wenn a TreeMap
zur 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- TreeMap
Data-Struktur zurückgibt . Normalerweise wird TreeMap
s 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 Map
zurückgegebene Abfrage ) Entry
irgendwo 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 map
Instanzen zur Erfassung bereit sein, dies wird jedoch nicht geschehen, da mindestens eine Instanz an einer Entry
anderen Stelle gespeichert ist.
Abhängig von Ihren jvm
Einstellungen kann die Anwendung aufgrund von a in einem frühen Stadium abstürzen OutOfMemoryError
.
In dieser visualvm
Grafik können Sie sehen, wie der Speicher weiter wächst.
Dasselbe passiert nicht mit einer Hash-Datenstruktur ( HashMap
).
Dies ist das Diagramm bei Verwendung von a 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 .
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 leakMe
wird niemals sterben.
(* bearbeitet *)
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.
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
first
nicht 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.