|
|||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||
|
Effective Java - Weak References
|
||||||||||||||||||||||
I n den vorangegangenen Beiträgen zum Thema Memory Leaks in Java haben wir uns angesehen, wie Memory Leaks entstehen (/MEM1/ bis /MEM3/)) und mit welchen Techniken und Tools man nach ihnen suchen kann (/ MEMLKS-4 / bis / MEMLKS-5 /). Dieses Mal wollen wir uns WeakReference s genauer ansehen. Mit ihrer Hilfe ist es möglich, Situationen zu vermeiden, in denen Memory Leaks entstehen können. Memory Leaks in Java
Gehen wir noch einmal zurück zu dem Beispiel
eines Memory Leaks, das wir ausführlich im ersten Artikel zum Thema Memory
Leaks diskutiert haben (/
MEMLKS-1
/). In dem
Beispiel haben wir einen rudimentären Server auf Basis der mit Java 7
eingeführten
AsynchronousSocketChannel
s
implementiert. Das Memory Leak entstand dadurch, dass wir im Server für
jeden neuen Client Verwaltungsinformation in einer Map gespeichert haben
und diese client-spezifischen Map-Einträge nicht nach der Beendigung der
Kommunikation mit den jeweiligen Clients wieder gelöscht haben. Es ist
offensichtlich, dass sich die Map mit jedem neuen Client vergrößert.
Erzeugt man also in einem Testprogramm viele Clients hintereinander, so
stürzt der Server irgendwann mit einem
OutOfMemoryError
ab. Der Sourcecode zu dem Beispiel sowie verschiedene alternative Korrekturen
finden sich hier (/
SRC
/).
Wie wir in dem darauf folgenden Artikel (/
MEMLKS-2
/)
diskutiert haben, ist diese Situation relativ typisch für ein Memory Leak,
das zu einem
OutOfMemoryError
führt.
Es gibt in Java häufig Verwaltungen, in die man Objekte eintragen und,
wenn sie nicht mehr benötigt werden, auch wieder löschen muss. Ein
weiteres Beispiel, das wir uns dazu angesehen haben, ist die Verwaltung
der Callbacks, die man als AWT Event Listener einhängt.
In unserer Artikelserie sind wir bisher so verblieben, dass es Aufgabe des Benutzers ist, solche Verwaltungen korrekt zu verwenden. Das heißt, er muss darauf achten, dass er jedes Objekt, das er in eine solche Verwaltung eingetragen hat, auch zum angemessenen Zeitpunkt wieder löscht. Wenn er sich nicht daran hält, hat er als Konsequenz das entstandene Memory Leak und den eventuell folgenden Programmabsturz auf Grund eines OutOfMemoryError zu verantworten. Ungewollte Referenzen und WeakReference s
In diesem Artikel wollen wir nun schauen,
ob derjenige, der die Verwaltung implementiert und bereitstellt, nicht
auch etwas tun kann, damit es erst gar nicht zu Memory Leaks kommen kann.
Toll wäre es doch, wenn der Benutzer sich gar nicht um das Löschen kümmern
müßte, weil dies automatisch erledigt wird.
Um zu verstehen, wie so etwas gehen könnte,
rufen wir uns noch mal in Erinnerung, wie es genau zu einem Memory Leak
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.
Übertragen wir diese Beschreibung auf das
Memory-Leak-Beispiel aus dem ersten Artikel: Die client-spezifischen Verwaltungsdaten
werden auch nach der Beendigung der Kommunikation mit dem Client weiter
über die Map referenziert, so dass der Garbage Collector sie nicht freigeben
kann. Die Map mit ihrer internen Datenstruktur bildet also unsere ungewollte
Referenz auf die client-spezifischen Daten.
Wie wäre es denn nun, wenn man dem Garbage
Collector explizit sagen könnte, dass die ungewollten Referenzen genau
dies sind: ungewollt. Man möchte ausdrücken, dass diese Referenzen
zwar für die Navigation im Objektgraphen der Applikation zur Verfügung
stehen, damit man die Objekte erreichen kann. Aber für den Garbage Collector
soll es nicht bedeuten, dass die Objekte, auf die sie verweisen, am Leben
gehalten werden sollen. Genau zu diesem Zweck gibt es seit dem JDK 1.2
das Package
java.lang.ref
im JDK, mit
dem neben einigen weiteren Referenztypen (siehe Textbox: „Referenztypen
im JDK“) die sogenannten
WeakReference
s
eingeführt wurden.
Die Idee ist, dass man ungewollte Referenzen innerhalb einer Datenverwaltung als WeakReference s implementiert. Damit braucht sich der Benutzer der Datenverwaltung nicht mehr um das Löschen der Objekte, die er in die Datenverwaltung eingetragen hat, kümmern. Das Löschen wird statt dessen automatisch vom Garbage Collector erledigt. Wie das Ganze im Detail geht, wollen wir weiter unten am Beispiel des Servers aus dem ersten Artikel (/ MEMLKS-1 /) diskutieren. Als erstes sehen wir uns aber die WeakReference selbst etwas genauer an. Die WeakReference im Detail
Fangen wir zuerst mit der Namensgebung
an. Da man auf ein Objekt über eine normale Referenz und über eine
WeakReference
verweisen kann, reicht ein Verb wie „referenzieren“ nicht mehr aus,
um die spezifische Art der Referenz zu beschreiben. In der englischen
Fachliteratur haben sich deshalb die Ausdrücke
strongly referenced
für das Referenzieren über eine normale Referenz und
weakly referenced
für das Referenzieren über eine
WeakReference
gebildet. In Anlehnung daran werden wir die Ausdrücke strong-referenziert
bzw. weak-referenziert verwenden. Entsprechend ergibt sich: ein Objekt,
das strong-erreichbar (also über normale Referenzen erreichbar) ist, wird
nicht vom
Garbage
Collector freigegeben wird; ein Objekt das weak-erreichbar ist, wird hingegen
trotz der Erreichbarkeit vom Garbage Collector freigegeben.
Dabei gilt ein Objekt als weak-erreichbar, wenn alle Referenzketten von den Root-Referenzen auf das fragliche Objekt mindestens eine Weak-Referenz enthalten. In der Verweiskette können durchaus auch normale Referenzen vorkommen, aber die Verweiskette muss mindestens eine Weak-Referenz enthalten, damit das referenzierte Objekt als weak-erreichbar gilt. Abbildung 1 zeigt eine solche Situation, in der ein Sting nur noch weak-erreichbar ist, obwohl es eine direkte Strong-Referenz auf den String gibt.
Abbildung 1: weak-erreichbarer String
Schauen wir uns nun an, wie man eine
WeakReference
benutzt. Beginnen wir mit einer Klasse
MyClass
,
die ein Feld vom Typ
String
hat. Das
bedeutet in der Terminologie von oben, dass eine Instanz von
MyClass
das enthaltene String-Objekt strong-referenziert. Die dazugehörende
recht rudimentäre Implementierung unserer Beispielklasse sieht folgendermaßen
aus:
public class MyClass { private String myString;
public MyClass(String s) { myString = s; }
public boolean doSomething(String o) { return myString.equalsIgnoreCase(o); }
}
Wie sieht die Implementierung von
MyClass
nun aus, wenn wir den String nicht mehr strong- sondern weak-referenzieren
wollen? Die
WeakReference
ist eine
generische Klasse, die als Weak-Referenz-Wrapper um das Originalobjekt
fungiert, so dass wir dann folgende Implementierung bekommen:
public class MyClass { private WeakReference<String> myWeakString;
public MyClass(String s) { myWeakString = new WeakReference<String>(s); }
public boolean doSomething(String o) { String s = myWeakString.get();
if (s == null) return false; else return s.equalsIgnoreCase(o); }
}
Um auf das eigentliche Objekt, das weak-referenziert
wird, zugreifen zu können, muss temporär eine Strong-Referenz genutzt
werden. In unserem Beispiel ist dies die Stackvariable
s
vom Typ
String
in der Methode
doSomething()
.
Die
WeakReference
liefert die Strong-Referenz
beim Aufruf von
get()
zurück, wenn das
referenzierte Objekt noch existiert.
get()
liefert
null
zurück, wenn das Objekt
irgendwann vorher nur noch weak-erreichbar war und deshalb bereits vom
Garbage Collector weggeräumt worden ist. Beim Zugriff auf das referenzierte
Objekt muss man also immer darauf vorbereitet sein, dass das Objekt gar
nicht mehr existiert, und entsprechend im Programm mit dieser Situation
umgehen können.
Es gibt noch ein weiteres interessantes Features von WeakReference s. Es ist möglich bei der Konstruktion einer WeakReference anzugeben, dass man informiert werden möchte, wenn das Objekt, auf das diese Referenz verweist, vom Garbage Collector weggeräumt worden ist. Zu diesem Zweck muss man als zusätzliches Konstruktor-Argument ein Queue vom Typ ReferenceQueue aus dem Package java.lang.ref mitgeben. Der Garbage Collector stellt eine so konstruierte WeakReference - Instanz in die Queue, sobald er festgestellt hat, dass das referenzierte Objekt nur noch weak-erreichbar ist. In der ReferenceQueue findet man also all die WeakReference s, deren Objekte der Garbage Collector demnächst wegräumen wird oder bereits weggeräumt hat. Dieser Mechanismus erlaubt es dem Benutzer der WeakReference nun, auf diese Situation zu reagieren. Wie das konkret aussehen kann, werden wir weiter unten sehen. Die Weak HashMap als Lösung für das Memory-Leak-Beispiel
Kommen wir wieder zurück zu unserm Memory-Leak-Beispiel
aus dem ersten Artikel der Serie (/
MEMLKS-1
/).
Wie oben bereits erwähnt, entstand das Memory Leak dadurch, dass wir für
jeden neuen Client Verwaltungsinformation in einer Map gespeichert haben
und diese clientspezifischen Map-Einträge nicht nach der Beendigung der
Kommunikation mit den jeweiligen Clients wieder gelöscht haben. Der
Key des Map-Eintrags war die
ClientSession
und das Value der
AsynchronousSocketChannel
des jeweiligen Clients. Die Map, die verwendet wurde, ist als Feld der
Server
-Klasse
folgendermaßen definiert:
private final Map<ClientSession, AsynchronousSocketChannel> sessionToChannel =
new
ConcurrentHashMap<ClientSession, AsynchronousSocketChannel>();
Die Map ist vom Typ
ConcurrentHashMap
,
weil die
completed()
-Methoden der
read()
-Callbacks
konkurrierend auf die Map zugreifen. Wie oben bereits gesagt, kann man
den vollständigen Code unter /
SRC
/
herunterladen.
Die Idee ist nun, die ungewollte Referenzen,
die unsere Map auf die
ClientSession
und den
AsynchronousSocketChannel
hält,
geschickt durch
WeakReference
s zu ersetzen
und so das Memory Leak zu vermeiden. Die Sourcecode-Änderung für diese
Korrektur ist minimal. Die Erklärung, warum sie funktioniert, ist erheblich
länger. Also schauen wir uns zuerst die Änderung an, dann kommen wir
zu den ausführlichen Erläuterungen.
Die Änderung besteht darin, dass wir die
Map nun folgendermaßen definieren:
private final Map<ClientSession, AsynchronousSocketChannel> sessionToChannel = Collections.synchronizedMap(new WeakHashMap<ClientSession, AsynchronousSocketChannel>()) ;
Die primäre Idee ist, eine
WeakHashMap
zu verwenden. Da diese aber nicht thread-sicher bei konkurrierendem Zugriff
ist, müssen wir zusätzlich einen
synchronizedMap
-Wrapper
verwenden. Zugegeben, bei hoher Last wird die
synchronizedMap
nicht so gut skalieren wie eine
ConcurrentHashMap
.
Aber mit diesem Nachteil wollen wir leben, denn immerhin haben wir so mit
minimalem Aufwand das Memory Leak behoben.
Warum das so ist und wie die
WeakHashMap
im Detail funktioniert, wollen wir uns nun ansehen. Wie die
WeakReference
kam auch die
WeakHashMap
mit der Version
1.2 zum JDK dazu. Die
WeakHashMap
implementiert
eine Hash-Map, bei der der Key nur weak-referenziert wird. Das heißt,
wenn das Key-Objekt eines Eintrags nicht von anderer Stelle im Programm
strong-referenziert wird, räumt der Garbage Collector das Key-Objekt
weg. Die
WeakHashMap
lässt sich darüber
vom Garbage Collector informieren. Dies geht über den Mechanismus mit
der
ReferenceQueue
, den wir oben bereits
beschrieben haben. Als Reaktion auf diese Notifikation löscht die
WeakHashMap
den gesamten noch verbliebenen Eintrag bestehend aus der (bereits entleerten)
WeakReference
als Key und dem dazugehörigen Value.
In unserem Beispiel löst die
WeakHashMap
das Memory-Leak-Problem. Entscheidend dafür ist, dass der Key (d.h. die
ClientSession
)
genau so lange von außerhalb der Map strong-referenziert wird, wie die
Verbindung mit dem Client besteht. Dies ist bei uns gegeben, wie ein
Blick auf die
complete
d
()
-Methode
des
read()
-
Callbacks
bestätigt:
public void completed(Integer len, ClientSession clSession ) { AsynchronousSocketChannel channel = sessionToChannel.get(clSession); if (clSession.handleInput(buf, len)) { buf.clear(); channel.read(buf, clSession , this); } else { try { channel.close(); } catch (IOException e) { /* ignore */ } }
}
Wie man sehen kann, schleusen wir die
ClientSession
immer wieder als zweiten Parameter des
read()
zum
nächsten Callback-Aufruf. Das heißt, die notwendige Strong-Referenz
auf das
ClientSession
-Objekt kommt aus
dem
AsynchronousSocketChannel
-Framework
im JDK, wo das Objekt für den nächsten Callback-Aufruf über eine Strong-Referenz
zwischengespeichert wird. Offensichtlich ist auch, dass diese Strong-Referenz
nicht mehr existiert, wenn die Kommunikation mit dem Client beendet ist.
Dann landen wir nämlich im
else
-Zweig
der
completed()
-Methode und es wird kein
erneuter
read()
-Aufruf (mit der
ClientSession
als Parameter) gemacht.
Auf den ersten Blick mag es so aussehen, dass es allein ein glücklicher Zufall ist, dass die WeakHashMap mit so geringem Aufwand unser Memory-Leak-Problem löst. Dem ist aber nicht so. Die Situation ist sogar eher typisch. Denn im allgemeinen hat man eine Strong-Referenz auf das Key-Objekt, mit dem man auf die Map zugreift, bis zu dem Zeitpunkt, wo dann die Map zur ungewollten Referenz auf das Key-Objekt und das Value-Objekt wird. Genau da hilft einem dann das spezifische Verhalten der WeakHashMap , denn Key- und Value-Objekt werden nun von der WeakHashMap in Zusammenarbeit mit dem GarbageCollector freigegeben. Die Weak HashMap im Detail
Werfen wir am Ende noch einen Blick auf
einige Implementierungsdetails der
WeakHashMap
.
Dies hilft auch zu verstehen, wie man
WeakReference
s
in eigenen Implementierungen sinnvoll einsetzt.
Typisch ist, dass man meist nicht die
WeakReference
direkt nutzt, sondern eine Klasse davon ableitet, die zusätzliche Attribute
und Funktionalität enthält. Bei der
WeakHashMap
ist diese von
WeakReference
abgeleitete
Klasse die private Inner Class
E
ntry
,
die zusätzlich das Interface
Map.Entry
implementiert:
private static class Entry<K,V> extends WeakReference<Object>
implements Map.Entry<K,V> { ... }
Ein
Entry
besteht aus dem eigentlichen Key-Value-Paar und zusätzlich eine Reihe
von weiteren Attributen. Dabei besteht das Key-Value-Paar aus einer Weak-Referenz
auf den Key und einer Strong-Referenz auf das Value.
Die Bestandteile eines
Entry
kann man gut an der Implementierung des Konstruktors von
Entry
sehen:
Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) { super(key, queue); this.value = value; this.hash = hash; this.next = next;
}
Der Key wird als erstes Argument an den Konstruktor
der Superklasse
WeakReference
übergeben.
Damit ist der Key nicht über eine normale Strong-Referenz im
Entry
abgelegt, sondern in eine Weak-Referenz verpackt. Das Value hingegen ist
über eine normale Referenz (als Attribut
value
)
im
Entry
abgelegt.
An den Konstruktor der Superklasse
WeakReference
wird
neben dem Key als zweites Argument die Queue übergeben, in der die
WeakReference
-
Instanzen
(oder genauer: die
Entry
-
Instanzen)
landen, deren weak-referenzierte Objekte vom Garbage Collector weggeräumt
werden. Diese Queue wird später von der privaten Methode
expungeStaleEntries()
verwendet, um Key-Value-Paare mit nur noch weak-erreichbarem Key aus der
Map zu entfernen (siehe unten).
Ganz wichtig ist der Hashcode, der im
Entry
abgelegt ist. Er wird von der privaten Methode
expungeStaleEntries
()
benötigt,
um die Key-Value-Paare mit weak-erreichbarem Key in der Map zu finden und
zu entfernen. Der Key ist ja nur noch weak-erreichbar (oder schon ganz
weggeräumt) und kann deshalb nicht mehr verwendet werden. Es wird dabei
ausgenutzt, dass die Positionen der Entries in der Map vom Hashcode der
Entries abhängen. Für die Berechnung des Hashcodes eines
Entry
s
wird der Hashcode des Keys benötigt. Wie bereits erwähnt, ist der Key
aber nur noch weak-erreichbar und möglicherweise sogar bereits vom Garbage
Collector weggeräumt worden. Genau deshalb wird der Hashcode des Keys
im
Entry
als
eigenes Feld gespeichert. Auf diese Weise ist der Hashcode des Keys noch
verfügbar, wenn der Key selbst bereits verschwunden ist. So kann das
Entry dann in der Map gelöscht werden.
Die private Methode expungeStaleEntries() wird in den public Methoden der WeakHashMap aufgerufen. Wir haben die WeakHashMap in einen synchronizedMap -Wrapper verpackt; der synchronizedMap -Wrapper sorgt dafür, dass alle public Methoden synchronisiert sind. Damit ist auch das interne Löschen der Entry s aus der WeakHashMap gegen konkurrierende Zugriffe von Außen auf die WeakHashMap geschützt und unsere Lösung ist thread-sicher. Zusammenfassung und Ausblick
In diesem Artikel haben wir uns angesehen,
wie
WeakReference
s helfen können, Memory
Leaks zu vermeiden. Die zentrale Idee dabei ist, die ungewollten Referenzen
in Verwaltungen, die zu Memory Leaks führen, als
WeakReference
s
zu implementieren. Handelt es sich bei der Verwaltung um eine Map, so
kann man in vielen Fällen gleich die
WeakHashMap
benutzen. Die eigentliche Änderung im Code ist dann mit sehr geringem
Aufwand verbunden, wie wir in unserem Beispiel gesehen haben.
Mit diesem Artikel schließen wir unsere
Reihe über Memory Leaks in Java. Das nächste Thema, dem wir uns widmen
werden, ist Java 8 und die dazugehörigen Neuerungen. Der erste Artikel
dazu wird in der Ausgabe 9.2013 des Java Magazin erscheinen.
LiteraturverweiseDie gesamte Serie über Memory Leaks:
|
|||||||||||||||||||||||
© Copyright 1995-2016 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/69.MemLeak.WeakRefs/69.MemLeak.WeakRefs.html> last update: 29 Nov 2016 |