|
|||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||
|
Effective Java - Memory Leaks - Referenzen ausnullen
|
||||||||||||||||||||||||||
In den vorangegangenen beiden Beiträgen (/ ML1 /, / ML2 /) haben wir uns Memory Leaks angesehen, die während des Programmablauf stetig wachsen und so zum Abbruch des Programms mit OutOfMemoryError führen. Diesmal wollen wir nicht wachsende Leaks betrachten. Auf den ersten Blick mögen sie weniger interessant aussehen, weil sie nicht zu solch dramatischen Konsequenzen wie einem Programmabbruch führen. Interessant sind sie aber trotzdem, weil sie sehr eng mit der vieldiskutierten Frage: "Ausnullen oder nicht?" verbunden sind. Referenzen "ausnullen"
Rufen wir uns noch mal in Erinnerung, wie
es zu einem Memory Leak in Java kommt. Der Garbage Collector ermittelt
ausgehend von sogenannten
Root References
, welche Objekte in einem
Java Programm referenziert und damit erreichbar sind. Alle nicht erreichbaren
Objekte räumt der Garbage Collector bei der Garbage Collection weg und
gibt ihren Speicher frei. Wenn wir nun auf ein Objekt verweisen, von
dem wir sicher sagen können, dass wir es im weiteren Kontext unseres Programms
gar nicht mehr benutzen werden, haben wir ein Memory Leak. Denn das nicht
mehr benötigte Objekt wird vom Garbage Collector nicht weggeräumt, weil
es noch referenziert wird. Diese Referenz wird in der englischsprachigen
Fachliteratur
unwanted reference
(also: ungewollte Referenz) genannt.
Bei unserem Garbage-Collection-Workshop
auf der JAX 2012 tauchte im Zusammenhang mit ungewollten Referenzen die
Frage auf, ob man eigentlich grundsätzlich alle Referenzen wenn irgend
möglich „ausnullen“ solle. Der Kollege mache das grundsätzlich
so. Ob es sinnvoll sei. (Mit „ausnullen“ ist dabei gemeint, dass
einer Referenz der Wert
null
zugewiesen
wird; danach ist das vormals referenzierte Objekt unerreichbar.) Ganz
offensichtlich ist die Frage, wo und unter welchen Umständen man Variablen
und Felder in Java ausnullen sollte, ein heiß diskutiertes Thema in agilen
Projekten mit gemeinsamer Code-Ownership. Gehen wir der Frage also nach.
Ausnullen bei Stackvariablen und Feldern
Beginnen wir mit dem Beispiel einer Stackvariablen
von einem Referenztyp, die in der
main
-Methode
definiert wird. Nehmen wir einmal an, diese Stackvariable stellt eine
ungewollte Referenz dar, weil sie ab einem bestimmten Zeitpunkt im Programmablauf
auf ein Objekt zeigt, das nicht mehr genutzt wird. Die Stackvariable
bewirkt, dass das referenzierte Objekt bis zur Beendigung des
main
-Threads
(in unserem Fall: bis zum Ende des Programms) erreichbar bleibt.
Der konkrete Beispielcode sieht so aus:
public static void main(String argv [] ) { String argMsg = "first argument: " + argv[0]; System.out.println(arg Msg); // Zeile 2
// der Rest des Programms, das noch lange laeuft }
Nach dem
println()
in Zeile 2 wird der über
arg
Msg
referenzierte String nicht mehr genutzt.
arg
Msg
ist also die ungewollte Referenz, die dafür sorgt, dass der referenzierte
String bis zum Programmende lebt.
Wie sieht nun die Situation aus, wenn wir die Variable
arg
Msg
ausnullen, nachdem wir sie in
println()
genutzt haben:
public static void main(String argv [] ) { String argMsg = "first argument: " + argv[0]; System.out.println(arg Msg); // Zeile 2 argMsg = null; // Zeile 3
// der Rest des Programms, das noch lange laeuft }
Ab der Zeile 3 verweist
arg
Msg
auf
null
und nicht mehr auf den String:
"
first
argument:
"
+ argv[0]
. Es existiert auch sonst keine Referenz auf diesen String
und der Garbage Collector kann ihn wegräumen und seinen Speicher freigeben.
Es gibt also keine ungewollte Referenz mehr und damit auch kein Memory
Leak. Das Ausnullen hat hier also einen positiven Effekt.
Ob das Ausnullen aber wirklich nötig ist,
ist eine andere Frage. Zum einen handelt sich bei dem String um ein relativ
kleines Objekt, das nur wenig Speicher verbraucht. Zum anderen gibt es
jede Menge Strings ähnlicher Größe, die aus den verschiedensten Gründen
sehr lange - unter Umständen bis zum Ende des Programms - leben, obwohl
sie gar nicht mehr gebraucht werden. Nehmen wir als Beispiel nur die
beiden Teil-Strings, aus denen unser Beispiel-String gebildet wurde. Der
erste Teil
"
first
argument:
"
wird, da es sich um
einen String-Literal handelt, im Constant Pool abgelegt. Dieser liegt
bei der Hot Spot JVM in der
Permanent Generation
. Hier findet im
Vergleich zum normalen User Heap (
Young
und
Old Generation
)
die Garbage Collection eher sporadisch statt. Deshalb lebt dieser String
vermutlich lange, möglicherweise bis zum Ende des Programms. Der zweite
Teil-String bleibt bis zum Ende des
main
-Threads
(in unserem Fall ist dies das Ende des Programms) über
argv[0
]
am Leben.
Man sieht also, in einem Java Programm gibt
es ohnehin häufig kleinere Objekte, die noch weiter am Leben gehalten
werden, obwohl sie nicht mehr gebraucht werden. Die generelle Regel bezüglich
des Ausnullens bei einer Variable wie
arg
Msg
ist deshalb: Die Referenz sollt man dann ausnullen, wenn das referenzierte
Objekt sehr groß ist und durch das Ausnullen signifikant viel Speicher
freigegeben werden kann. Dies trifft aber auf den String in unserer Situation
oben nicht zu.
Das ganze Problem kann man natürlich umgehen,
indem man auf die Variable
arg
Msg
ganz verzichtet und statt dessen folgendes schreibt:
public static void main(String argv [] ) { System.out.println( " first argument: " + argv[0]);
// der Rest des Programms, das noch lange laeuft }
Der Compiler sieht hier sofort, wie lange
er den zusammengesetzten String braucht, und kann ihn unmittelbar nach
der Benutzung bereits freigeben, ohne dass wir explizit etwas tun müssen.
So wird man es deshalb sinnvollerweise in der Praxis machen.
Schauen wir uns nun eine leichte Variante des Beispiels von oben an:
void foo(String arg ) { String argMsg = "first argument: " + arg; System.out.println(arg Msg); // Zeile 2 argMsg = null; // Zeile 3
// Rest der Methode, die kurz ist
}
Die Idee dabei ist:
foo()
wird aufgerufen,
läuft relativ kurz und kehrt danach wieder zur aufrufenden Methode zurück.
Ist in einem solchen Fall das Ausnullen in
Zeile 3 sinnvoll? Nein, denn nachdem der Kontrollfluss aus
foo()
zur aufrufenden Methode zurückgekehrt ist, sind die lokalen Variablen
(
argMsg
) und Parameter (
arg
)
nicht mehr erreichbar. Die von ihnen referenzierten Objekte können, wenn
nicht von anderen Stellen noch auf sie verwiesen wird, also freigegeben
werden. Mit dem Ausnullen in Zeile 3 wird der referenzierte String zwar
etwas früher unerreichbar; da die Freigabe des Objekts aber ohnehin erst
mit der nächsten Garbage Collection erfolgt, ist dieser zeitliche Unterschied
gar nicht relevant. Hier bläht das zusätzliche Ausnullen nur den Code
auf und stört.
Was gilt eigentlich für block-lokale Variablen? Hier ein Beispiel:
void foo(String arg) { { String argMsg = "first argument: " + arg; System.out.println(arg Msg); // Zeile 2 } // Rest der Methode, die kurz ist }
Die Variable
argMsg
ist
nun block-lokal und nach Verlassen des Blocks im Rest der Methode
foo
nicht mehr zugreifbar. Das legt nahe, das referenzierte String-Objekt
sei nach Verlassen des Blocks auch für den Garbage Collector unerreichbar.
In der Praxis ist es aber in der HotSpot-JVM so, dass block-lokale Referenzen
erst beim Verlassen der umgebenden Methode aufgegeben werden. Der von
argMsg
referenzierte
lebt daher in der HotSpot-JVM bis zum Verlassen der Methode
foo
.
Man könnte die Variable
argMsg
vor
Verlassen des Blocks explizit ausnullen, damit der referenzierte String
tatsächlich am Blockende unerreichbar wird. Aber, wie bereits im vorigen
Beispiel erläutert, würde es den Code unnötig aufblähen, da der Rest
der Methode nur noch kurz ist.
Fassen wir das bisher Gesagte noch mal kurz
zusammen. Ausnullen ist bei methoden- oder block-lokalen Variablen nicht
sinnvoll, da die Methoden nach relativ kurzer Zeit wieder verlassen werden
und ihre lokalen Variablen damit nicht mehr erreichbar sind. Ausnahmen
sind Methoden (wie
main()
,
run()
,
...), die den Ausgangspunkt für länger laufende Threads bilden. Hier
kann Ausnullen in Ausnahmefällen Sinn machen, wenn besonders große Objekte
referenziert werden, die im weiteren Programmablauf nicht mehr gebraucht
werden.
Eine ähnliche Argumentation gilt im Prinzip auch für Felder von Klassen. Sind die Instanzen langlebig und werden über ihre Felder sehr große Objekte referenziert, sollte man ausnullen. Sonst eher nicht, weil es nicht viel bringt und eher stört. Ausnullen bei der Implementierung von eigenen Datenstrukturen
Soweit die grundsätzlichen Überlegungen.
Es gibt weitere spezielle Fälle, bei denen Ausnullen sinnvoll ist, nämlich
dann wenn wir eigene Datenstrukturen (Collections, Verwaltungen, ...) implementieren.
Das liegt daran, dass wir mit der Implementierung von Datenstrukturen in
der Regel größere Mengen Speicher belegen. Deshalb ist es in solchen
Situationen wichtig, dass wir kritisch prüfen, ob wir durch Ausnullen
den Speicherverbrauch reduzieren können. Schauen wir uns dazu das Beispiel
einer generischen Stack-Collection an:
public class Stack<E> { private E[] elements = (E[]) new Object[8]; private int head = elements.length;
private void doubleCapacity() { … }
public void push(E e) { elements[--head] = e; if (head == 0) doubleCapacity(); }
public E pop() { if (head == elements.length) return null;
return (elements[head++]); }
}
Nehmen wir an, dass wir eine Instanz dieses
Stack
s
dann folgendermaßen benutzen:
Stack<String> s = new Stack<>(); s.push("1"); s.push("2"); // Zeile 3 System.out.println(s.pop);
System.out.println(s.pop)
;
// Zeile 5
Im ersten Schritt (bis einschließlich Zeile 3) "pushen" wir zwei Strings auf den Stack. Danach holen wir diese Strings wieder vom Stack und drucken sie aus. Wie sieht unsere Stack-Instanz s am Ende von Zeile 3 aus? Und wie, wenn alle Statements einschließlich Zeile 5 abgearbeitet worden sind? Abbildung 1 zeigt eine graphische Repräsentation des Stack-Objekts am Ende von Zeile 3. Der aktuelle Stackpointer ( head ) zeigt auf das Element mit Index 6 des Arrays ( elements ). Das bedeutet in unserer Implementierung, dass der Stack zwei Objekte enthält ist. Auf diese wird von den Array-Elementen unterhalb des Stackpointers verwiesen, also die Elemente mit einem Index i für den gilt i >= head . Das sind in der aktuellen Situation die Indizes 6 und 7.
Abbildung 1: Stack Instanz
s
nach Zeile 3
Kommen wir nun zu Abbildung 2. Sie zeigt die graphische Repräsentation des Stacks am Ende von Zeile 5. Der Stackpointer ( head ) steht nun auf Index 8 und zeigt damit auf kein gültiges Element des Array ( elements ), sondern hinter das Array. Das bedeutet in unserer Implementierung, dass der Stack leer ist. Das ist okay, denn nach zweimal push() und zweimal pop() sollte der Stack auch wieder leer sein. Auffällig ist, dass die Array-Elemente für Index 6 und 7 immer noch auf die Strings verweisen, die vorher auf den Stack "gepusht" worden sind. Aus Sicht der Programmlogik ist das kein Problem. Nur Array-Elemente unterhalb des Stackpointers, also mit einem Index i , für den gilt i >= head , sind relevant. Da aber head == 8 ist, gibt es gar keine relevanten Elemente.
Abbildung 2: Stack Instanz
s
nach Zeile 5
Von der Logik her ist die Implementierung
des
Stack
also in Ordnung. Wie sieht
es aber mit dem Speichermanagement? Sind die Verweise der Array-Elemente
auf ihren Inhalt nicht ungewollte Referenzen? Sie halten doch Objekte
referenziert, die sonst unter Umständen freigegeben werden könnten.
Sollten wir die Array-Elemente in
pop()
also ausnullen? Anderseits werden diese Referenzen doch sowieso überschrieben,
wenn der
Stack
wieder wächst.
In diesem Fall ist Ausnullen richtig und
wichtig. Dafür gibt es zwei Gründe. Den ersten Grund haben wir schon
oben erwähnt. Die nicht leeren Array-Elemente oberhalb des Stackpointers
sind ungewollte Referenzen, die dazu führen können, dass die von ihnen
referenzierten Objekte nicht freigegeben werden können. Bei entsprechender
Benutzung des
Stack
s kann die Summe des
so weiterhin ungewollt referenzierten Speichers recht groß werden, zum
Beispiel, weil die Elemente, auf die verwiesen wird, jeweils selbst schon
recht groß sind.
Der zweite Grund ist subtiler Art. Collection-Abstrationen
wie auch andere Verwaltungsstrukturen sollten möglichst so implementiert
sein, dass speicherneutrale Use Cases wirklich speicherneutral sind.
Wir haben in unserem Fall einen speicherneutralen Use Case: zweimal
push()
und zweimal
pop()
. Danach sollte genauso
viel Speicher von unserem
Stack
Objekt
gebraucht werden wie vor dem Ausführen des Use Cases. Das ist aber bei
unserer Implementierung nicht der Fall, da die beiden "gepushten" Objekte
weiter über den Stack referenziert werden.
Im nächsten Artikel dieser Serie wollen
wir eine Technik für die Suche von Memory Leaks besprechen, die auf solchen
speicherneutralen Uses Cases basiert. Wenn man diese Technik anwenden
will, um ein Memory Leak im eigenen Sourcecode zu suchen, dann ist es hinderlich,
wenn man dabei auf Unschönheiten in verwendeten Third-Party-Komponenten
wie unseren
Stack
stößt. Es ist äußerst
frustrierend, wenn man nach langem Suchen feststellt, dass es sich bei
dem vermeintlichen Leak nur um ein
false positive
in einer Third
-Party-Komponenten handelt.
Deshalb ist in einem Fall wie dem oben geschilderten
das Ausnullen wichtig, um es den Nutzern der von uns implementierten Abstraktion
bei der Memory-Leak-Suche nicht unnötig schwer zu machen. Die korrigierte
Version der
pop()
Methode sieht nun so
aus:
public E pop() { if (head == elements.length) return null;
E result = elements[head]; eleme n ts[head++] = null; return result;
}
Natürlich kann es Situationen geben, wo man gewollt gegen die Speicherneutralität verstößt. Auch bei unserem Stack passiert es. Wenn er voll geworden ist, wird mit doubleCapacity() ein doppelt so großes Array an elements zugewiesen und die Referenzen auf die Elemente werden umkopiert. Wenn der Stack danach wieder leerer wird, bleibt die hohe Kapazität aber erhalten. Auch wenn man zusätzlich eine Strategie für das Verringern der Kapazität vorsieht, wird diese sinnvollerweise nicht speicherneutral sein. Der Stack würde sonst bei Größenschwankungen um den Kapazitätsumstellungspunkt herum unperformant arbeiten, da das unterliegende Array immer wieder ersetzt werden müsste. Die Regel lautet deshalb ein wenig einschränkend: Soweit es ohne weitere Nachteile möglich ist, sollte die Implementierung von Datenstrukturen speicherneutral sein. Zusammenfassung und Ausblick
Dieser Artikel ist von einer Diskussion
bei unserem Garbage-Collection-Workshop auf der JAX 2012 inspiriert.
Es ging um die Frage, wo und unter welchen Umständen man Variablen und
Felder in Java ausnullen sollte. Dieser Artikel fasst unsere Sicht zusammen.
Im Allgemeinen gilt: Variablen und Felder
sollte man nur dann explizit ausnullen, wenn man signifikant viel Speicher
damit freigeben kann. Etwas anders sieht es bei Library-Abstrationen
und Frameworks aus. Hier sollte man etwas strenger sein. Zum einen
hat man es unter Umständen nicht selbst unter Kontrolle, wie groß der
unnötig referenzierte Speicher ist, wie man bei unserem generischen Stack-Beispiel
oben sehen kann. Zum anderen sollten Library-Abstraktionen möglichst
speicherneutral sein, damit ihre Benutzer bei ihrer eigenen Suche nach
Memory Leaks nicht durch
false positives
aus den Library-Abstraktionen
behindert werden.
Wie schon kurz erwähnt, werden wir uns in
unserem nächsten Artikel Strategien und Tools ansehen, mit denen man Memory
Leaks suchen kann.
LiteraturverweiseDie gesamte Serie über Memory Leaks:
|
|||||||||||||||||||||||||||
© Copyright 1995-2016 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/66.Mem.NullOut/66.Mem.NullOut.html> last update: 29 Nov 2016 |