Ich hatte gerade ein Interview und wurde gebeten, ein Speicherverlust 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?
Hier ist eine gute Möglichkeit, einen echten Speicherverlust (Objekte, auf die durch Ausführen von Code nicht zugegriffen werden kann, die jedoch noch im Speicher gespeichert sind) in reinem Java zu erstellen:
new byte[1000000]
), speichert einen starken Verweis darauf in einem statischen Feld und speichert dann einen Verweis auf sich selbst in einem ThreadLocal. Das Zuweisen des zusätzlichen Arbeitsspeichers ist optional (es reicht aus, die Klasseninstanz zu löschen), aber das Leck wird dadurch viel schneller ausgeführt.Dies funktioniert, weil ThreadLocal einen Verweis auf das Objekt behält, der einen Verweis auf seine Klasse behält, der wiederum einen Verweis auf seinen ClassLoader behält. Der ClassLoader speichert wiederum einen Verweis auf alle Klassen, die er geladen hat.
(In vielen JVM-Implementierungen war dies noch schlimmer, insbesondere vor Java 7, da Klassen und ClassLoader direkt in permgen zugewiesen und nie gecodet wurden. Unabhängig davon, wie die JVM das Entladen von Klassen handhabt, Ein ThreadLocal verhindert weiterhin, dass ein Class-Objekt zurückgefordert wird.)
Eine Variation dieses Musters ist, warum Anwendungscontainer (wie Tomcat) Speicher wie ein Sieb verlieren können, wenn Sie häufig Anwendungen erneut bereitstellen, die ThreadLocals auf irgendeine Weise verwenden. (Da der Anwendungscontainer Threads wie beschrieben verwendet und jedes Mal, wenn Sie die Anwendung erneut bereitstellen, wird ein neuer ClassLoader verwendet.)
Update : Da immer wieder Leute danach fragen, hier ist ein Beispielcode, der dieses Verhalten in Aktion zeigt .
Statisches Feld mit Objektreferenz [esp final field]
class MemorableClass {
static final ArrayList list = new ArrayList(100);
}
Aufruf von String.intern()
für einen längeren String
String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();
(Nicht geschlossen) 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 für den Garbage Collector von JVM nicht erreichbar sind , z. B. durch systemeigene Methoden zugewiesener Speicher
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 Option noclassgc
in IBM JDK, die die Erfassung nicht verwendeter Klassenbereinigungen verhindert
Siehe IBM JDK-Einstellungen .
Eine einfache Sache, die Sie tun können, ist, ein HashSet mit einem falschen (oder nicht existierenden) hashCode()
oder equals()
zu verwenden und dann weiterhin "Duplikate" hinzuzufügen. Anstatt Duplikate wie gewünscht zu ignorieren, wird die Menge immer größer 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.
Es wird einen nicht offensichtlichen Fall geben, in dem Java undicht ist, abgesehen vom Standardfall vergessener Listener, statischer Verweise, gefälschter/modifizierbarer Schlüssel in Hashmaps oder einfach festgefahrener Threads, ohne dass die Chance besteht, ihren Lebenszyklus zu beenden .
File.deleteOnExit()
- verliert immer die Zeichenkette, char[]
, so dass der letztere nicht zutrifft; @ Daniel, keine Notwendigkeit für Stimmen, aber.Ich werde mich auf Threads konzentrieren, um die Gefahr von nicht verwalteten Threads zu verdeutlichen. Ich möchte nicht einmal Swing berühren.
Runtime.addShutdownHook
und nicht entfernen ... und dann auch mit removeShutdownHook aufgrund eines Fehlers in der ThreadGroup-Klasse in Bezug auf nicht gestartete Threads möglicherweise nicht gesammelt, effektiv die ThreadGroup verlieren. JGroup hat das Leck in GossipRouter.
Das Erstellen, aber nicht das Starten eines Thread
erfolgt in derselben Kategorie wie oben.
Das Erstellen eines Threads erbt die ContextClassLoader
und AccessControlContext
sowie die ThreadGroup
und alle InheritedThreadLocal
. Alle diese Verweise sind potenzielle Lecks, zusammen mit den gesamten vom Klassenladeprogramm geladenen Klassen und allen statische Verweise und ja-ja. Der Effekt ist besonders sichtbar mit dem gesamten j.u.c.Executor-Framework, das eine supereinfache ThreadFactory
-Oberfläche bietet, aber die meisten Entwickler haben keine Ahnung von der lauernden Gefahr. Auch viele Bibliotheken starten Threads auf Anfrage (viel zu viele branchenübliche Bibliotheken).
ThreadLocal
Caches; das sind in vielen Fällen böse. Ich bin mir sicher, dass jeder eine Menge einfacher Caches gesehen hat, die auf ThreadLocal basieren. Nun, die schlechte Nachricht: Wenn der Thread im Kontext ClassLoader länger als erwartet läuft, ist es ein reines Nice little leak. Verwenden Sie keine ThreadLocal-Caches, es sei denn, dies wird wirklich benötigt.
Aufruf von ThreadGroup.destroy()
, wenn die ThreadGroup selbst keine Threads hat, aber immer noch untergeordnete ThreadGroups enthält. Ein fehlerhaftes Leck, das verhindert, dass die ThreadGroup von ihrem übergeordneten Element entfernt wird, aber alle untergeordneten Elemente sind nicht mehr aufzählbar.
Bei Verwendung von WeakHashMap und dem Wert (in) wird direkt auf den Schlüssel verwiesen. Dies ist schwer zu finden, ohne einen Heap-Dump. Dies gilt für alle erweiterten Weak/SoftReference
, die möglicherweise einen harten Verweis auf das geschützte Objekt enthalten.
Verwenden von Java.net.URL
mit dem HTTP (S) -Protokoll und Laden der Ressource von (!). Dies ist eine Besonderheit, die KeepAliveCache
erstellt einen neuen Thread in der System-ThreadGroup, der den Kontext-Classloader des aktuellen Threads verliert. Der Thread wird bei der ersten Anforderung erstellt, wenn kein aktiver Thread vorhanden ist, sodass Sie möglicherweise Glück haben oder einfach nur lecken. Das Leck ist in Java 7 bereits behoben, und der Code, der den Thread ordnungsgemäß erstellt, entfernt den Kontextklassenladeprogramm. Es gibt nur noch wenige Fälle (wie ImageFetcher, auch behoben) um ähnliche Threads zu erstellen.
Verwenden von InflaterInputStream
Übergeben von new Java.util.Zip.Inflater()
im Konstruktor (z. B. PNGImageDecoder
) und Aufrufen von end()
des Inflaters. Wenn Sie den Konstruktor nur mit new
übergeben, hat dies keine Chance ... Und ja, wenn Sie close()
für 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 für notwendig erachtet wird. Bis zu diesem Zeitpunkt wird der native Speicher so stark beansprucht, 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 einheimische Ressourcen frei, sobald Sie können; Der Finalizer ist einfach zu arm.
Der gleiche Fall mit Java.util.Zip.Deflater
. Dies ist weitaus schlimmer, da Deflater in Java speicherhungrig ist, d. H. Immer 15 Bit (maximal) und 8 Speicherebenen (maximal 9) verwendet, die mehrere Hundert KB nativen Speicher zuweisen. Glücklicherweise ist Deflater
nicht weit verbreitet und meines Wissens enthält JDK keine Missbräuche. Rufen Sie immer end()
auf, wenn Sie manuell Deflater
oder Inflater
erstellen. Der beste Teil der letzten beiden: Sie können sie nicht über die normalen verfügbaren Profilerstellungstools finden.
(Ich kann auf Anfrage weitere Zeitverschwender hinzufügen.)
Viel Glück und bleib in Sicherheit; Leckagen sind böse!
Die meisten Beispiele sind hier "zu komplex". Sie sind Randfälle. Mit diesen Beispielen ist dem Programmierer ein Fehler unterlaufen (wie beispielsweise, dass er Equals/Hashcode nicht neu definiert), oder er wurde von einem Eckfall der JVM/Java gebissen (Laden der Klasse mit Static ...). Ich denke, das ist nicht die Art von Beispiel, die ein Interviewer haben möchte oder auch nur der häufigste Fall.
Aber es gibt wirklich einfachere Fälle für Speicherverluste. Der Garbage Collector gibt nur das frei, was nicht mehr referenziert wird. Wir als Java Entwickler kümmern uns nicht um Speicher. Wir ordnen es bei Bedarf zu 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 Objekte grafisch darzustellen. 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 alle langlebigen gemeinsamen Zustände dazu, Speicherverluste zu verursachen, 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 entwerfen, dass wir einem langlebigen Objekt einen Listener hinzufügen, aber den Listener nicht entfernen, wenn er nicht mehr benötigt wird. Wir haben Objekte im Cache, aber keine Strategie, um sie aus dem Cache zu entfernen.
Wir haben vielleicht ein komplexes Diagramm, das den vorherigen Zustand speichert, der für eine Berechnung benötigt wird. Aber der vorherige Zustand ist selbst mit dem vorherigen Zustand verbunden und so weiter.
Als müssten wir SQL-Verbindungen oder -Dateien schließen. Wir müssen korrekte Referenzen 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, über die 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 ist perfekt vorhersehbar. Keine Notwendigkeit für spezielle Sprachfunktionen oder Eckfälle. Speicherverluste sind entweder ein Hinweis darauf, dass möglicherweise etwas fehlt, oder auch ein Hinweis auf Designprobleme.
Die Antwort hängt ganz davon ab, was der Interviewer zu fragen glaubte.
Ist es in der Praxis möglich, Java undicht zu machen? 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". Und deshalb werde ich mich auf Interviewfähigkeiten konzentrieren, anstatt auf Java. Ich glaube, Sie wiederholen eher die Situation, dass Sie die Antwort auf eine Frage in einem Interview nicht kennen, als dass Sie sich an einem Ort befinden müssen, an dem Sie wissen müssen, wie Sie Java auslaufen lassen. Hoffentlich wird dies helfen.
Eine der wichtigsten Fähigkeiten, die Sie für ein Vorstellungsgespräch entwickeln können, besteht darin, aktiv auf die Fragen zu hören und mit dem Interviewer zusammenzuarbeiten, um deren Absichten zu ermitteln. Auf diese Weise können Sie nicht nur ihre Fragen so beantworten, wie sie möchten, sondern auch zeigen, dass Sie über einige wichtige Kommunikationsfähigkeiten verfügen. Und wenn es um die Wahl zwischen vielen gleichermaßen talentierten Entwicklern geht, stelle ich denjenigen ein, der zuhört, denkt und versteht, bevor er jedes Mal reagiert.
Das Folgende ist ein ziemlich sinnloses Beispiel, wenn Sie JDBC nicht verstehen. Oder zumindest, wie JDBC erwartet, dass ein Entwickler die Instanzen Connection
, Statement
und ResultSet
schließt, bevor er sie verwirft oder Verweise auf sie verliert, anstatt sich auf die Implementierung von finalize
zu verlassen.
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 obige Problem ist, dass das Objekt Connection
nicht geschlossen wird und daher die physische Verbindung offen bleibt, bis der Garbage Collector vorbeikommt und feststellt, dass es nicht erreichbar ist. GC ruft die Methode finalize
auf, aber es gibt JDBC-Treiber, die finalize
nicht implementieren, zumindest nicht auf die gleiche Weise wie Connection.close
implementiert ist. Das resultierende Verhalten ist, dass während Speicher aufgrund von nicht erreichbaren Objekten, die gesammelt werden, zurückgefordert wird, Ressourcen (einschließlich Speicher), die dem Objekt Connection
zugeordnet sind, möglicherweise einfach nicht zurückgefordert werden.
In einem solchen Fall, in dem die Methode Connection
der Methode finalize
nicht alles bereinigt, kann es vorkommen, dass die physische Verbindung zum Datenbankserver mehrere Garbage Collection-Zyklen 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 finalize
implementieren würde, 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, in dem während der Objekt-Finalisierung Ausnahmen auftreten, hängt mit einem anderen Szenario zusammen, das möglicherweise zu einem Speicherverlust führen kann - der Auferstehung von Objekten. Objektauferstehung wird oft absichtlich durchgeführt, indem ein starker Verweis auf das Objekt erstellt wird, das von einem anderen Objekt finalisiert wird. Wenn die Auferstehung von Objekten missbraucht wird, führt dies in Kombination mit anderen Quellen von Speicherlecks zu einem Speicherverlust.
Es gibt noch viele weitere Beispiele, die Sie heraufbeschwören können
List
-Instanz, in der Sie nur der Liste hinzufügen und keine Elemente daraus löschen (obwohl Sie Elemente entfernen sollten, die Sie nicht mehr benötigen), oderSocket
s oder File
s, schließen Sie sie jedoch nicht, wenn sie nicht mehr benötigt werden (ähnlich wie im obigen Beispiel für die Klasse Connection
).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 Array-Element zu löschen, das nicht mehr verwendet wird (elementData[--size] = null
)? Diese Referenz könnte ein riesiges Objekt am Leben erhalten ...
Jedes Mal, wenn Sie Verweise auf Objekte aufbewahren, die Sie nicht mehr benötigen, tritt ein Speicherverlust auf. In Behandeln von Speicherverlusten in Java Programmen finden Sie Beispiele dafür, wie sich Speicherverluste in Java manifestieren und was Sie dagegen tun können.
Mit der Klasse Sun.misc.Unsafe kann ein Speicherverlust verursacht werden. Tatsächlich wird diese Serviceklasse in verschiedenen Standardklassen verwendet (zum Beispiel in Java.nio Klassen). Sie können keine Instanz dieser Klasse direkt erstellen, aber Sie können verwenden Sie Reflection, um dies zu tun.
Code wird in Eclipse nicht kompiliert IDE - 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: Einfachste Methode, um in Java einen Speicherverlust zu verursachen?
"Ein Speicherverlust in der Informatik (oder in diesem Zusammenhang ein Speicherverlust) 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 die automatische Speicherverwaltung durch und gibt Ressourcen frei, die für Sie nicht benötigt werden. Sie können das nicht aufhalten. Es wird IMMER in der Lage sein, die Ressourcen freizugeben. Bei Programmen mit manueller Speicherverwaltung ist dies anders. In C kann man mit malloc () etwas Speicher bekommen. Um den Speicher freizugeben, benötigen Sie den von malloc zurückgegebenen Zeiger und rufen free () auf. Wenn Sie den Zeiger jedoch nicht mehr haben (überschrieben oder die Lebensdauer überschritten), können Sie diesen Speicher leider nicht mehr freigeben, und es liegt ein Speicherverlust vor.
Alle anderen Antworten sind in meiner Definition bisher nicht wirklich Speicherlecks. Sie alle zielen darauf ab, die Erinnerung wirklich schnell mit sinnlosen Dingen zu füllen. Sie können die von Ihnen erstellten Objekte aber jederzeit dereferenzieren und so den Speicher freigeben -> NO LEAK. acconrads antwort kommt allerdings ziemlich nahe, wie ich zugeben muss, da seine lösung darin besteht, den müllsammler einfach "zum absturz zu bringen", indem er ihn in eine endlose schleife zwingt).
Die lange Antwort lautet: Sie können einen Speicherverlust bekommen, 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, führt Ihr Prozess Java zu einem Speicherverlust. Sie können auch Fehler in der JVM haben, sodass die JVM Speicherplatz verliert. Es gibt wahrscheinlich Fehler in der JVM, es kann sogar einige bekannte geben, da die Garbage Collection nicht so trivial ist, aber dann ist es immer noch ein Fehler. Konstruktionsbedingt ist dies nicht möglich. Möglicherweise fragen Sie nach einem Java Code, der von einem solchen Fehler betroffen ist. Tut mir leid, ich kenne keinen und es könnte in der nächsten Java Version sowieso kein Fehler mehr sein.
Hier ist eine einfache/finstere ü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 der Teilstring auf die interne Darstellung des ursprünglichen, viel längeren Strings bezieht, bleibt das Original im Speicher. So haben Sie, solange Sie einen StringLeaker im Spiel haben, auch die gesamte ursprüngliche Zeichenfolge im Speicher, auch wenn Sie vielleicht glauben, nur an einer Zeichenfolge festzuhalten.
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 Fehler können Sie auch die Unterzeichenfolge .intern()
eingeben:
...
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.
Nehmen Sie eine beliebige Webanwendung mit, die in einem beliebigen Servlet-Container ausgeführt wird (Tomcat, Jetty, Glassfish usw.). Stellen Sie die App 10 oder 20 Mal hintereinander erneut bereit (es kann ausreichen, einfach die WAR-Datei im Autodeploy-Verzeichnis des Servers zu berühren.
Wenn dies nicht tatsächlich von jemandem getestet wurde, ist die Wahrscheinlichkeit hoch, dass Sie nach einigen erneuten Bereitstellungen einen OutOfMemoryError erhalten, da die Anwendung sich nicht darum gekümmert hat, nach sich selbst aufzuräumen. Möglicherweise finden Sie mit 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 enthält, müllsammelbar sind.
Wenn es nur eine Referenz gibt, die die Aufhebung Ihrer Web-App überlebt, kann der entsprechende Klassenladeprogramm und folglich nicht alle Klassen Ihrer Web-App müllsammeln.
Von Ihrer Anwendung gestartete Threads, ThreadLocal-Variablen und Protokollierungs-Appender sind einige der üblichen Verdächtigen, die zu Classloader-Lecks führen können.
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, wobei der Listener dann nicht entfernt wird, wenn das Widget zerstört wird. Sie bekommen nicht nur einen Speicherverlust, sondern auch einen Leistungseinbruch, da alle Ihre alten Zuhörer auch angerufen werden, wenn Sie Ereignisse abfeuern.
Vielleicht durch die Verwendung von externem nativem Code über JNI?
Mit reinem Java ist das fast unmöglich.
Dabei handelt es sich jedoch um einen "Standard" -Speicherverlust, bei dem Sie nicht mehr auf den Speicher zugreifen können, der sich jedoch immer noch 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 ein nettes "Memory Leak" in Bezug auf PermGen und XML-Parsing. Der von uns verwendete XML-Parser (ich kann mich nicht erinnern, welcher es war) hat eine String.intern () für Tag-Namen erstellt, um den Vergleich zu beschleunigen. Einer unserer Kunden hatte die großartige Idee, Datenwerte nicht in XML-Attributen oder Text, sondern als Tag-Namen zu speichern. Daher hatten wir ein Dokument wie das Folgende:
<data>
<1>bla</1>
<2>foo</>
...
</data>
Tatsächlich verwendeten sie keine Zahlen, sondern längere Text-IDs (etwa 20 Zeichen), die eindeutig waren und mit einer Rate von 10 bis 15 Millionen pro Tag eingingen. Das sind 200 MB Müll pro Tag, der nie mehr gebraucht und nie mehr gecodet wird (da er in PermGen ist). Wir hatten permgen auf 512 MB eingestellt, so dass es ungefähr zwei Tage dauerte, 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;
}
Dein 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).
Klar, Sie können die Dinge viel komplizierter machen:
Was häufig 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 bin vor kurzem auf eine Speicherverlustsituation gestoßen, die auf eine Weise von log4j verursacht wurde.
Log4j verfügt über diesen Mechanismus mit dem Namen NDC = Nested Diagnostic Context , mit dem verschachtelte Protokollausgaben aus verschiedenen Quellen unterschieden werden können. Die Granularität, mit der NDC arbeitet, hängt von Threads ab, sodass die 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 zwar so lange, bis das NDC-Tag alle Objekte im Speicher hat, die vom Thread hängen Objekt bleiben auch in Erinnerung. In unserer Webanwendung verwenden wir NDC, um Protokollausgaben mit einer Anforderungs-ID zu kennzeichnen, um Protokolle von einer einzelnen Anforderung separat zu unterscheiden. Der Container, der das NDC-Tag mit einem Thread verknüpft, entfernt es auch, 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 einem 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 hugeList-Objekt hängt. Selbst nachdem der Thread das getan hat, was er getan hat, wurde der Verweis auf die riesige Liste durch den NDC-Kontext Hastable am Leben gehalten, wodurch ein Speicherverlust verursacht wurde.
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 letztendlich aufräumt. 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 jetzt Example1 aufrufen und Example2 verwerfen, haben Sie immer noch eine Verknüpfung zu einem Example1-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 Zeitspanne existiert; Java geht davon aus, dass es immer existiert und tatsächlich niemals versucht, es zu bereinigen, wenn es im Code nicht mehr erreichbar ist. Das ist aber völlig unbestätigt.
Der Interviewer suchte wahrscheinlich nach einem Zirkelverweis wie dem folgenden Code (der im Übrigen nur in sehr alten JVMs, die die Referenzzählung 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 mit der Referenzzählung der obige Code Speicher verlieren würde. Aber die meisten modernen JVMs verwenden keine Referenzzählung mehr, die meisten verwenden einen Sweep-Garbage-Collector, der tatsächlich diesen Speicher sammelt.
Als Nächstes könnten Sie das Erstellen eines Objekts mit einer zugrunde liegenden nativen Ressource wie folgt 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 systemeigenen Code in der JVM verursacht wird, der zugrunde liegende systemeigene Ressourcen zuweist, die nicht durch Ihren Code Java freigegeben wurden.
Letztendlich müssen Sie mit einer modernen JVM Java Code schreiben, der eine native Ressource außerhalb des normalen Bereichs der JVM-Kenntnisse zuweist.
Erstellen Sie eine statische Karte, und fügen Sie ihr weitere Referenzen hinzu. Diese werden niemals gcd werden.
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); }
}
Jeder vergisst immer die native Code-Route. Hier ist eine einfache Formel für ein Leck:
malloc
auf. Rufen Sie nicht free
an.Denken Sie daran, dass die Speicherzuweisungen im systemeigenen Code vom JVM-Heap stammen.
Sie können einen beweglichen Speicherverlust erstellen, indem Sie eine neue Instanz einer Klasse in der finalize-Methode dieser Klasse erstellen. Bonuspunkte, wenn der Finalizer mehrere Instanzen erstellt. Hier ist ein einfaches Programm, das den gesamten Heap-Speicher abhängig von Ihrer Heap-Größe in einigen Sekunden bis zu 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, das hat noch niemand gesagt: Sie können ein Objekt wiederbeleben, indem Sie die Methode finalize () so überschreiben, dass finalize () irgendwo eine Referenz darauf speichert. Der Garbage Collector wird nur einmal für das Objekt aufgerufen, sodass das Objekt danach niemals zerstört wird.
Kürzlich bin ich 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.
Das Interessante daran ist: Auf diese Weise können Sie den Heap-Speicher des zugrunde liegenden Prozesses verlieren und nicht den Heap-Speicher von JVM.
Alles, was Sie brauchen, ist eine JAR-Datei mit einer Datei, auf die aus dem Code Java verwiesen wird. Je größer die JAR-Datei ist, desto schneller wird Speicher zugewiesen.
Sie können ein solches Glas 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 es einfach in eine Datei mit dem Namen BigJarCreator.Java ein, kompilieren Sie es und führen Sie es über die Befehlszeile aus:
javac BigJarCreator.Java
java -cp . BigJarCreator
Et voilà: Sie finden ein JAR-Archiv in Ihrem aktuellen Arbeitsverzeichnis mit zwei darin enthaltenen Dateien.
Lassen Sie uns eine zweite Klasse erstellen:
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 führt im Grunde nichts aus, sondern erstellt nicht referenzierte InputStream-Objekte. Diese Objekte werden sofort mit Müll entsorgt und tragen somit nicht zur Größe des Heapspeichers bei. Für unser Beispiel ist es wichtig, eine vorhandene Ressource aus einer JAR-Datei zu laden, und hier spielt die Größe eine Rolle!
Wenn Sie Zweifel haben, versuchen Sie, die oben genannte Klasse zu kompilieren und zu starten. Achten Sie jedoch darauf, eine angemessene Heap-Größe (2 MB) zu wählen:
javac MemLeak.Java
java -Xmx2m -classpath .:big.jar MemLeak
Hier tritt kein OOM-Fehler auf, da keine Referenzen gespeichert sind. Die Anwendung wird weiterhin ausgeführt, unabhängig davon, wie groß Sie im obigen Beispiel ITERATIONS ausgewählt haben. Der Speicherverbrauch Ihres Prozesses (sichtbar oben (RES/RSS) oder Prozess-Explorers) nimmt zu, es sei denn, die Anwendung erhält den Befehl wait. In der obigen Konfiguration werden ungefähr 150 MB Arbeitsspeicher zugewiesen.
Wenn Sie möchten, dass die Anwendung auf Nummer sicher geht, schließen Sie den Eingabestream genau dort, wo er erstellt wurde:
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();
und Ihr Prozess wird 35 MB nicht überschreiten, unabhängig von der Anzahl der Iterationen.
Ganz einfach und überraschend.
Wie viele Leute bereits angedeutet haben, sind Ressourcenlecks ziemlich einfach zu verursachen - wie die JDBC-Beispiele. Tatsächliche Speicherverluste sind etwas schwieriger - vor allem, wenn Sie sich nicht darauf verlassen müssen, dass Teile der JVM dies für Sie tun ...
Die Idee, Objekte mit einer sehr großen Stellfläche zu erstellen und dann nicht darauf zugreifen zu können, ist ebenfalls kein wirklicher Speicherleck. Wenn nichts darauf zugreifen kann, wird Müll gesammelt, und wenn etwas darauf zugreifen kann, ist es kein Leck ...
Eine Möglichkeit, wie verwendet funktioniert - und ich weiß nicht, ob es immer noch funktioniert - besteht darin, eine dreiteilige Kreiskette zu haben. Wie in Objekt A hat ein Verweis auf Objekt B, hat Objekt B einen Verweis auf Objekt C und Objekt C hat einen Verweis auf Objekt A. Der GC war klug genug zu wissen, dass eine zwei tiefe Kette - wie in A <-> B - Kann sicher eingesammelt werden, wenn A und B für nichts anderes zugänglich sind, aber nicht mit der Dreiwege-Kette umgehen können ...
es gibt viele verschiedene Situationen, in denen der Speicher ausläuft. Eine, auf die ich gestoßen bin, enthüllt eine Karte, die nicht enthüllt und an anderer Stelle verwendet werden sollte.
public class ServiceFactory {
private Map<String, Service> services;
private static ServiceFactory singleton;
private ServiceFactory() {
services = new HashMap<String, Service>();
}
public static synchronized ServiceFactory getDefault() {
if (singleton == null) {
singleton = new ServiceFactory();
}
return singleton;
}
public void addService(String name, Service serv) {
services.put(name, serv);
}
public void removeService(String name) {
services.remove(name);
}
public Service getService(String name, Service serv) {
return services.get(name);
}
// the problematic api, which expose the map.
//and user can do quite a lot of thing from this api.
//for example, create service reference and forget to dispose or set it null
//in all this is a dangerous api, and should not expose
public Map<String, Service> getAllServices() {
return services;
}
}
// resource class is a heavy class
class Service {
}
Threads werden nicht gesammelt, bis sie enden. Sie dienen als Wurzeln der Speicherbereinigung. Sie sind eines der wenigen Objekte, die nicht einfach dadurch zurückgefordert werden können, dass man sie vergisst oder Verweise auf sie löscht.
Beachten Sie: Das Grundmuster 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 als volatile
deklariert ist, wird die Änderung an der Variablen möglicherweise nicht vom Thread gesehen, sodass er nicht weiß, ob sie beendet werden soll. Oder stellen Sie sich vor, einige Threads möchten ein freigegebenes Objekt aktualisieren, aber beim Versuch, es zu sperren, einen Deadlock ausführen.
Wenn Sie nur eine Handvoll Threads haben, werden diese Fehler wahrscheinlich offensichtlich sein, weil Ihr Programm nicht mehr richtig funktioniert. Wenn Sie über einen Thread-Pool verfügen, der nach Bedarf weitere Threads erstellt, werden die veralteten/blockierten 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 Sie System.gc()
auf, wie Sie möchten, aber das an leakMe
übergebene Objekt 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 gepoolt werden.
Verwenden Sie beispielsweise ThreadLocal-Variablen in Servlets, um mit anderen Webkomponenten zu kommunizieren. Dabei werden die Threads vom Container erstellt und die inaktiven in einem Pool verwaltet. ThreadLocal-Variablen werden dort gespeichert, wenn sie nicht ordnungsgemäß bereinigt wurden, bis möglicherweise dieselbe Webkomponente ihre Werte überschreibt.
Sobald das Problem erkannt wurde, kann es natürlich leicht gelöst werden.
Der Interviewer hat möglicherweise nach einer zirkulären Referenzlö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 Müllsammlern. Sie würden dann höflich erklären, dass JVMs einen viel ausgefeilteren Algorithmus verwenden, der diese Einschränkung nicht aufweist.
-Wes Tarle
Eine andere Möglichkeit, potenziell große Speicherverluste zu verursachen, besteht darin, Verweise auf Map.Entry<K,V>
eines TreeMap
zu speichern.
Es ist schwer einzuschätzen, warum dies nur für TreeMap
s gilt, aber bei Betrachtung der Implementierung könnte der Grund sein, dass: ein TreeMap.Entry
Verweise auf seine Geschwister speichert, wenn also ein TreeMap
bereit ist gesammelt werden, aber eine andere Klasse enthält einen Verweis auf einen ihrer Map.Entry
, dann wird die gesamte Map im Speicher behalten.
Reales Szenario:
Stellen Sie sich eine Datenbankabfrage vor, die eine große TreeMap
Datenstruktur zurückgibt. Normalerweise wird TreeMap
s verwendet, da die Reihenfolge für das Einfügen 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 zurückgegebene Map
) irgendwo eine Entry
speichern, wächst der Speicher ständig.
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 geschieht jedoch nicht, da mindestens eine Entry
an einer anderen Stelle gespeichert wird.
Abhängig von Ihren jvm
Einstellungen kann die Anwendung im frühen Stadium aufgrund eines OutOfMemoryError
abstürzen.
Sie können aus dieser visualvm
Grafik ersehen, wie der Speicher weiter wächst.
Das gleiche passiert nicht mit einer Hash-Datenstruktur (HashMap
).
Dies ist die Grafik bei Verwendung eines HashMap
.
Die Lösung? Speichern Sie einfach direkt den Schlüssel/Wert (wie Sie es wahrscheinlich bereits tun), anstatt den Map.Entry
zu speichern.
Ich habe einen umfangreicheren Benchmark geschrieben hier .
Ein Beispiel, das ich kürzlich behoben habe, ist das Erstellen neuer GC- und Image-Objekte, aber das Vergessen, die dispose () -Methode aufzurufen.
GC-Javadoc-Snippet:
Der Anwendungscode muss die GC.dispose () - Methode explizit aufrufen, um die von jeder Instanz verwalteten Betriebssystemressourcen freizugeben, wenn diese Instanzen nicht mehr benötigt werden. Dies ist besonders unter Windows 95 und Windows 98 wichtig, wenn für das Betriebssystem eine begrenzte Anzahl von Gerätekontexten verfügbar ist.
Bild-Javadoc-Snippet:
Der Anwendungscode muss die Image.dispose () - Methode explizit aufrufen, um die von jeder Instanz verwalteten Betriebssystemressourcen freizugeben, wenn diese Instanzen nicht mehr benötigt werden.
Ich möchte einen Rat geben, wie die Anwendung mit den in JVM verfügbaren Tools auf Speicherverluste überwacht werden kann. Es wird nicht gezeigt, wie ein Speicherverlust erzeugt wird, sondern es wird erläutert, wie der Speicherverlust mit nur wenigen verfügbaren Tools erkannt wird.
Sie müssen zuerst den Speicherverbrauch von Java überwachen.
Die einfachste Möglichkeit hierfür ist die Verwendung des mit JVM gelieferten Dienstprogramms jstat.
jstat -gcutil <process_id> <timeout>
Es wird der Speicherverbrauch für jede Generation (Jung, Alt und Alt) und die Speicherbereinigungszeiten (Jung und Voll) gemeldet.
Sobald Sie feststellen, dass die vollständige Speicherbereinigung zu oft ausgeführt wird und zu viel Zeit in Anspruch nimmt, können Sie davon ausgehen, dass bei der Anwendung Speicherplatz verloren geht.
Dann müssen Sie einen Speicherauszug mit dem Dienstprogramm jmap erstellen:
jmap -dump:live,format=b,file=heap.bin <process_id>
Dann müssen Sie die Datei heap.bin beispielsweise mit Memory Analyzer, Eclipse Memory Analyzer (MAT) analysieren.
MAT analysiert den Speicher und gibt Ihnen verdächtige Informationen zu Speicherlecks.
Theoretisch geht das nicht. Java Speichermodell verhindert es. Da jedoch Java implementiert werden muss, gibt es einige Einschränkungen, die Sie verwenden können. Hängt davon ab, was Sie verwenden können:
Wenn Sie native verwenden können, können Sie Speicher zuweisen, den Sie später nicht mehr freigeben.
Wenn das nicht verfügbar ist, gibt es ein schmutziges kleines Geheimnis über Java, das nicht viele Leute kennen. Sie können nach einem Direktzugriffsarray fragen, das nicht vom GC verwaltet wird, und es kann daher leicht verwendet werden, um ein Speicherverlust zu verursachen. Dies wird von DirectByteBuffer bereitgestellt (http://download.Oracle.com/javase/1.5.0/docs/api/Java/nio/ByteBuffer.html#allocateDirect(int)).
Wenn Sie keines davon verwenden können, können Sie trotzdem einen Speicherverlust verursachen, indem Sie den GC austricksen. Die JVM wird mithilfe einer Garbage Collection von Generational implementiert. Dies bedeutet, dass der Haufen in Bereiche unterteilt ist: Junge, Erwachsene und Älteste. Ein Objekt, wenn es im jungen Bereich entsteht. Da er immer mehr benutzt wird, entwickelt er sich zu Erwachsenen bis hin zu Ältesten. Ein Objekt, das höchstwahrscheinlich in den Bereich der älteren Bevölkerung gelangt, wird nicht als Müll gesammelt. Sie können nicht sicher sein, dass ein Objekt durchgesickert ist. Wenn Sie nach einem Stopp und einer GC-Reinigung fragen, kann dies dazu führen, dass das Objekt zwar durchgesickert ist, aber über einen längeren Zeitraum hinweg. Weitere Informationen unter (http://Java.Sun.com/docs/hotspot/gc1.4.2/faq.html)
Außerdem müssen Klassenobjekte nicht gecodet werden. Könnte mir ein Weg sein, es zu tun.
Ein Thread, der nicht beendet wird (z. B. in seiner Ausführungsmethode auf unbestimmte Zeit). Es wird kein Müll gesammelt, auch wenn wir einen Verweis darauf verlieren. Sie können Felder hinzufügen, damit das Thread-Objekt so groß wird, wie Sie möchten.
Die derzeit beste Antwort listet weitere Tricks auf, die jedoch überflüssig erscheinen.
Wirft eine nicht behandelte Ausnahme von der finalize-Methode.
Die meisten Speicherverluste, die ich in Java gesehen habe, betreffen Prozesse, die nicht mehr synchron sind.
Prozess A spricht über TCP mit B und weist Prozess B an, etwas zu erstellen. B gibt der Ressource eine ID aus, z. B. 432423, die A in einem Objekt speichert und verwendet, während er mit B spricht. Irgendwann wird das Objekt in A von der Garbage Collection zurückgefordert (möglicherweise aufgrund eines Fehlers), aber A teilt B niemals mit, dass ( vielleicht noch ein Bug).
Jetzt hat A nicht mehr die ID des Objekts, das es in Bs RAM erstellt hat, und B weiß nicht, dass A keinen Verweis mehr auf das Objekt hat. In der Tat ist das Objekt durchgesickert.
Es gibt viele Antworten zum Erstellen eines Speicherverlusts in Java. Beachten Sie jedoch den im Interview gestellten Punkt.
"Wie erstelle ich ein Speicherleck mit Java?" ist eine offene Frage, deren Zweck es ist, den Grad der Erfahrung eines Entwicklers zu bewerten.
Wenn ich Sie frage, ob Sie Erfahrung mit der Behebung von Speicherlecks in Java haben, lautet Ihre Antwort "Ja". Ich müsste dann mit "Könnten Sie mir Beispiele geben, wo Sie Speicherlecks beheben müssen?", Zu denen Sie mir ein oder zwei Beispiele geben würden.
Wenn der Interviewer jedoch fragt, wie mit Java ein Speicherverlust verursacht werden soll? Die erwartete Antwort sollte ungefähr so lauten:
Wenn der Entwickler diesen Gedanken nicht befolgt, versuche ich, ihn/sie anzuleiten und zu fragen: "Können Sie mir ein Beispiel geben, wie Java Speicher lecken kann?", Gefolgt von "Mussten Sie jemals einen Speicherleck beheben?" in Java? "
Beachten Sie, dass ich nicht nach einem Beispiel zum Speicherleck in Java frage. Das wäre albern. Wer wäre an einem Entwickler interessiert, der effektiv Code schreiben kann, der den Speicher verliert?
Wenn die maximale Heap-Größe X. Y1 .... Yn Anzahl der Instanzen ist, ist der Gesamtspeicher = Anzahl der Instanzen X Bytes pro Instanz. Wenn X1 ...... Xn Bytes pro Instanzen ist, ist der Gesamtspeicher (M) = Y1 * X1 + ..... + Yn * Xn. Wenn also M> X ist, überschreitet es den Heap-Platz. Im Folgenden können die Probleme in Code 1 aufgeführt werden. Verwenden Sie mehr Instanzen als lokale. 2. Jedes Mal Instanzen erstellen, anstatt Objekte zu bündeln. 3.Nicht Erstellen Sie das Objekt auf Anfrage. 4.Nullsetzen der Objektreferenz nach Abschluss des Vorgangs.Wiederholen, wenn es im Programm angefordert wird.
Ein paar Vorschläge:
Die oben genannten Effekte könnten durch erneutes Bereitstellen der Anwendung "verbessert" werden.
Kürzlich darauf gestoßen:
Lesen Sie http://bugs.Sun.com/bugdatabase/view_bug.do?bug_id=5072161 und verknüpfen Sie die Themen für eine ausführliche Diskussion.
Eine Möglichkeit besteht darin, einen Wrapper für eine ArrayList zu erstellen, der nur eine Methode bereitstellt: eine, die der ArrayList Dinge hinzufügt. Machen Sie die ArrayList selbst privat. Erstellen Sie nun eines dieser Wrapper-Objekte im globalen Bereich (als statisches Objekt in einer Klasse) und qualifizieren Sie es mit dem Schlüsselwort final (z. B. public static final ArrayListWrapper wrapperClass = new ArrayListWrapper()
). Daher kann die Referenz jetzt nicht geändert werden. Das heißt, wrapperClass = null
funktioniert nicht und kann nicht zum Freigeben des Speichers verwendet werden. Es gibt aber auch keine andere Möglichkeit, mit wrapperClass
etwas anderes zu tun, als Objekte hinzuzufügen. Daher können Objekte, die Sie zu wrapperClass
hinzufügen, nicht recycelt werden.
Ein Speicherverlust in Java ist kein typischer C/C++ - Speicherverlust.
Um zu verstehen, wie die JVM funktioniert, lesen Sie Grundlegendes zur Speicherverwaltung .
Grundsätzlich ist der wichtige Teil:
Das Mark and Sweep-Modell
Die JRockit-JVM verwendet das Markierungs- und Sweep-Garbage-Collection-Modell, um Garbage-Collections für den gesamten Heap durchzuführen. Eine Markierungs- und Sweep-Speicherbereinigung besteht aus zwei Phasen, der Markierungsphase und der Sweep-Phase.
Während der Markierungsphase werden alle Objekte, die über Java Threads, native Handles und andere Stammquellen erreichbar sind, ebenfalls als lebendig markiert als die Objekte, die von diesen Objekten aus erreichbar sind und so weiter. Dieser Prozess identifiziert und markiert alle Objekte, die noch verwendet werden, und der Rest kann als Müll betrachtet werden.
Während der Sweep-Phase wird der Heap durchlaufen, um die Lücken zwischen den lebenden Objekten zu finden. Diese Lücken werden in einer freien Liste erfasst und für eine neue Objektzuordnung zur Verfügung gestellt.
Die JRockit JVM verwendet zwei verbesserte Versionen des Mark- und Sweep-Modells. Eine ist meist gleichzeitig Mark und Sweep und die andere ist parallel Mark und Sweep. Sie können die beiden Strategien auch mischen, indem Sie z. B. meist gleichzeitig Markierungen und parallele Sweeps ausführen.
So erstellen Sie ein Speicherleck in Java; Der einfachste Weg, dies zu tun, besteht darin, eine Datenbankverbindung zu erstellen, etwas zu arbeiten und sie einfach nicht zu Close()
zu machen. Erstellen Sie dann eine neue Datenbankverbindung, während Sie im Gültigkeitsbereich bleiben. Dies ist zum Beispiel in einer Schleife nicht schwer zu bewerkstelligen. Wenn Sie einen Worker haben, der aus einer Warteschlange abruft und in eine Datenbank pusht, können Sie leicht einen Speicherverlust verursachen, indem Sie Close()
-Verbindungen vergessen oder sie öffnen, wenn dies nicht erforderlich ist, und so weiter.
Schließlich verbrauchen Sie den der JVM zugewiesenen Heap, indem Sie die Verbindung Close()
vergessen. Dies führt dazu, dass sich der JVM-Müll wie verrückt ansammelt. Dies kann zu Java.lang.OutOfMemoryError: Java heap space
Fehlern führen. Es ist zu beachten, dass der Fehler möglicherweise nicht bedeutet, dass ein Speicherverlust vorliegt. es könnte nur bedeuten, dass Sie nicht genug Gedächtnis haben; Datenbanken wie Cassandra und ElasticSearch können diesen Fehler auslösen, weil sie nicht über genügend Speicherplatz verfügen.
Es ist erwähnenswert, dass dies für alle GC-Sprachen gilt. Nachfolgend einige Beispiele, die ich als SRE gesehen habe:
json.Unmarshal
, übergeben Sie die Ergebnisse als Referenz und lassen Sie sie geöffnet. Letztendlich führte dies dazu, dass der gesamte Haufen durch versehentliche Refs verbraucht wurde, die ich offen hielt, um json zu dekodieren.String.substring-Methode in Java 1.6 erstellt einen Speicherverlust. Dieser Blog-Beitrag erklärt es.
http://javarevisited.blogspot.com/2011/10/how-substring-in-Java-works.html
In Java ist ein "Speicherverlust" in erster Linie, dass Sie zu viel Speicher verwenden, der sich von dem in C unterscheidet, in dem Sie den Speicher nicht mehr verwenden, sondern vergessen, ihn zurückzugeben (freizugeben). Wenn ein Interviewer nach Java Speicherlecks fragt, fragt er, ob der JVM-Speicher immer mehr genutzt wird, und stellt fest, dass ein regelmäßiger Neustart der JVM die beste Lösung ist. (es sei denn, der Interviewer ist extrem technisch versiert)
Beantworten Sie diese Frage also so, als würden Sie fragen, warum die JVM-Speichernutzung im Laufe der Zeit zunimmt. Gute Antworten wären das Speichern zu vieler Daten in einer HttpSession mit zu langer Zeitüberschreitung oder ein schlecht implementierter In-Memory-Cache (Singleton), der niemals alte Einträge löscht. Eine weitere mögliche Antwort besteht darin, viele JSPs oder dynamisch generierte Klassen zu haben. Klassen werden in einen Speicherbereich namens PermGen geladen, der normalerweise klein ist, und die meisten JVMs implementieren kein Entladen von Klassen.
Swing hat es sehr einfach mit Dialogen. Erstellen Sie einen JDialog, zeigen Sie ihn, der Benutzer schließt ihn, undicht! Sie müssen dispose()
aufrufen oder setDefaultCloseOperation(DISPOSE_ON_CLOSE)
konfigurieren
Lapsed Listerners ist ein gutes Beispiel für Speicherverluste: Object wird als Listener hinzugefügt. Alle Verweise auf das Objekt werden auf Null gesetzt, wenn das Objekt nicht mehr benötigt wird. Wenn Sie jedoch vergessen, das Objekt aus der Listener-Liste zu entfernen, bleibt das Objekt am Leben und reagiert sogar auf Ereignisse, wodurch sowohl Speicher als auch CPU verschwendet werden. Siehe http://www.drdobbs.com/jvm/Java-qa/184404011
Wenn Sie keinen Garbage Collector zum Komprimieren verwenden, kann aufgrund der Heap-Fragmentierung eine Art Speicherverlust auftreten.
nvorsichtige Verwendung einer nicht statischen inneren Klasse in einer Klasse, die einen eigenen Lebenszyklus hat.
In Java enthalten nicht statische innere und anonyme Klassen einen impliziten Verweis auf ihre äußere Klasse. Statische innere Klassen dagegen nicht.
Hier ist ein häufiges Beispiel für einen Speicherverlust in Android, der jedoch nicht offensichtlich ist:
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() { //non-static inner class, holds the reference to the SampleActivity outter class
@Override
public void handleMessage(Message msg) {
// ...
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for a long time.
mLeakyHandler.postDelayed(new Runnable() {//here, the anonymous inner class holds the reference to the SampleActivity class too
@Override
public void run() {
//....
}
}, SOME_TOME_TIME);
// Go back to the previous Activity.
finish();
}}
Dadurch wird verhindert, dass der Aktivitätskontext überflüssig wird.
Ein Echtzeit-Beispiel für einen Speicherverlust vor JDK 1.7
angenommen, Sie lesen eine Datei mit 1000 Textzeilen und behalten das String-Objekt bei
String fileText = 1000 characters from file
fileText = fileText.subString(900, fileText.length());
Im obigen Code habe ich zunächst 1000 Zeichen gelesen und dann einen Teilstring ausgeführt, um nur die letzten 100 Zeichen zu erhalten. Jetzt sollte sich fileText nur auf 100 Zeichen beziehen, und alle anderen Zeichen sollten auf Müll gesammelt werden, da ich die Referenz verloren habe. Bevor die JDK 1.7-Teilzeichenfolgefunktion jedoch indirekt auf die ursprüngliche Zeichenfolge der letzten 100 Zeichen verweist und verhindert, dass die gesamte Zeichenfolge Müll sammelt und ganze 1000 Zeichen vorhanden sind im Speicher, bis Sie die Referenz der Teilzeichenfolge verlieren.
sie können ein Speicherverlust-Beispiel wie oben erstellen
ein Speicherverlust ist eine Art von Ressourcenleck, das auftritt, wenn ein Computerprogramm die Speicherzuweisungen falsch verwaltet, sodass nicht mehr benötigter Speicher ) freigegeben wird => Wiki-Definition
Es ist eine Art relativ kontextbasiertes Thema, Sie können einfach ein Thema nach Ihrem Geschmack erstellen, solange die nicht verwendeten Referenzen nicht von Kunden verwendet werden und dennoch am Leben bleiben.
Das erste Beispiel sollte ein benutzerdefinierter Stack sein, ohne die veralteten Referenzen in effektiv Java Punkt 6 auf null zu setzen .
Natürlich gibt es noch viel mehr, so lange Sie wollen, aber wenn wir uns nur die integrierten Klassen von Java ansehen, könnte es so sein
Lassen Sie uns einen superdummen Code überprüfen, um das Leck zu erzeugen.
public class MemoryLeak {
private static final int HUGE_SIZE = 10_000;
public static void main(String... args) {
letsLeakNow();
}
private static void letsLeakNow() {
Map<Integer, Object> leakMap = new HashMap<>();
for (int i = 0; i < HUGE_SIZE; ++i) {
leakMap.put(i * 2, getListWithRandomNumber());
}
}
private static List<Integer> getListWithRandomNumber() {
List<Integer> originalHugeIntList = new ArrayList<>();
for (int i = 0; i < HUGE_SIZE; ++i) {
originalHugeIntList.add(new Random().nextInt());
}
return originalHugeIntList.subList(0, 1);
}
}
Tatsächlich gibt es einen anderen Trick, den wir mit HashMap auslösen können, indem wir den Suchprozess ausnutzen. Es gibt eigentlich zwei Arten:
hashCode()
ist immer gleich, aber equals()
sind unterschiedlich;hashCode()
und equals()
immer true;Warum?
hashCode()
-> bucket => equals()
, um das Paar zu lokalisieren
Ich wollte gerade substring()
zuerst und dann subList()
erwähnen, aber es scheint, dass dieses Problem bereits behoben ist, da seine Quelle in JDK 8 vorhanden ist.
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}