|
||||||||||||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | ||||||||||||||||||||||||||||||||||||||||||
|
Effective Java - Memory Leaks - Ein Beispiel
|
|||||||||||||||||||||||||||||||||||||||||
Unsere Reihe über Java 7 Neuerungen haben wir mit dem letzten Artikel abgeschlossen. Da sich Oracle mit Java 8 noch ein wenig Zeit lässt (als anvisierter Freigabe-Termin wird im Augenblick „Sommer 2013“ genannt), wollen wir die Gelegenheit nutzten, um noch einmal an die Artikelserie über Garbage Collection aus den Jahren 2010/2011 anzuschließen. Wir hatten damals die Garbage Collection in der HotSpot-JVM von Sun/Oracle sowie ihr Tuning detailliert besprochen. Bei Interesse kann man diese Artikel weiterhin auf unserer Webseite (/ ARGC /) oder zusammengefasst als Buch (/ JCP /) finden. Wir wollen anknüpfend an dieses zurückliegende Thema diskutieren, wie es zu Memory Leaks in Java kommen kann und wie man sie finden oder besser schon von vornherein vermeiden kann. Was ist in Java ein Memory Leak?
Bei der Erwähnung von Memory Leaks mag
sich der eine oder andere Leser fragen, was denn das sein soll – ein
Memory Leak in Java. Gibt es so etwas in Java überhaupt? Eigentlich
wurde doch beim Wechsel hin zu Java und weg von C bzw. C++ (also Programmiersprachen
mit explizitem Memory Management durch den Programmierer) versprochen,
dass Probleme wie Memory Leaks wegen dem automatischen Memory Management
mittels Garbage Collector ein für allemal passé seien.
Was macht der Garbage Collector?
Der Ausgangspunkt für Memory Leaks in
Java ist die automatische Garbage Collection, denn allein der Garbage Collector
entscheidet, wann Objekte bzw. ihr Speicher freigegeben wird. Wie macht
er das? Bei allen Sun/Oracle Garbage Collectoren (außer dem Garbage
First („G1“) Collector, siehe /
GC4
/) wird dazu ein
Marking
angewandt. Vereinfacht beschrieben sieht das Marking so aus, dass ausgehend
von den sogenannten
Root References
alle erreichbaren Objekte markiert
werden. Danach weiß der Garbage Collector, dass die markierten Objekte
erreichbar sind und geht davon aus, dass diese weiter im Programm genutzt
werden. Umgekehrt weiß er auch, dass er alle nichtmarkierten Objekte
freigegeben kann. Im Detail ist das Marking, je nach Garbage Collector
Algorithmus, eine ziemlich komplizierte Angelegenheit, die aus mehreren
Phasen bestehen kann. Wer sich dafür interessiert findet die Details
hier (/
GC1
/, /
GC2
/, /
GC3
/).
Die Root References sind, wie der Name
schon andeutet, die Ausgangsreferenzen zu allen Objekten in einer Java
Virtual Machine (JVM). Beispiele für Root References sind die Stackpointer
zu allen Thread-Stacks. Variablen von Referenztypen, die als Methoden-Parameter
oder methodenlokale Variablen auf dem Stack angelegt werden, sind über
diese Root References erreichbar und folglich markiert.
Wie passt die Garbage-Collection-Strategie mit dem Programm zusammen?
Fassen wir das eben Gesagte noch mal kurz
zusammen: mit dem
Marking
bestimmt der Garbage Collector, welche
Objekte erreichbar sind und noch verwendet werden (können). Diese Sicht
des Garbage Collectors („erreichbar“ = „verwendet“) stimmt mit
der Sicht unseres Programms im Allgemeinen überein; es kann aber auch
zu Abweichungen kommen. So kann es Objekte geben, die nicht mehr im weiteren
Ablauf vom Code unseres Programms verwendet werden, die aber trotzdem weiterhin
über eine Verweiskette von Root Referenences aus erreichbar sind. Diese
Objekte sind sozusagen in der Programmlogik in Vergessenheit geraten, der
Garbage Collector erkennt sie aber weiterhin als „erreichbar“. Er betrachtet
sie also als „benutzt“ und gibt sie nicht frei.
Im englischen Sprachgebrauch wurde für
solche Objekte der Begriff
loitering objects
geprägt. Die Referenz
auf ein solches Objekt, das in der Programmlogik in Vergessenheit geraten
ist, aber vom Gargabe Collector weiterhin beim
Marking
erreicht
werden kann, nennt sich
unwanted reference
. Die Namensgebung ist
wohl in beiden Fällen hinreichend selbsterklärend, so dass sie keiner
weiteren Erläuterung bedarf.
Jetzt ist natürlich ein einzelnes
loitering
object
nicht unbedingt ein wirklich relevantes Memory Leak. Zum Memory
Leak kommt es erst dadurch, dass der Vorgang des Objekterzeugens und -vergessens
im Programm wiederholt wird. Mit der Zeit bleiben dann immer mehr Objekte
über das Marking erreichbar, obwohl sie für die Programmlogik nicht mehr
relevant sind. Die Folge ist, dass der Heap der JVM zu einem großen
Teil aus „vergessenen“ Objekten besteht. Das wiederum kann dazu führen,
dass die JVM zu einem späteren Zeitpunkt mit einem
OutOfMemoryError
abstürzt - nämlich dann, wenn der Heap völlig ausgeschöpft ist und
kein weiterer Speicher mehr angefordert werden kann.
Ein Beispiel für ein Memory Leak
Die wirklich entscheidende Frage haben
wir bisher noch nicht angesprochen: wie kann es sein, dass Objekte von
der Programmlogik einfach vergessen werden? Dafür gibt es in der Praxis
viele Gründe. Wir wollen uns hier ein konkretes Beispiel ansehen.
Leider benötigt man zur Demonstration
eines Memory Leaks ein Programmbeispiel mit einem gewissen Maß an Komplexität.
Wenn das Beispiel zu einfach ist, dann fragt sich der Betrachter: „Wie
kann man einen derart offensichtlichen Fehler machen?“ Andererseits wollen
wir hier aus didaktischen Gründen auch nicht allzu weit ausholen. Das
folgende Beispiel ist in diesem Sinne ein Kompromiss: es ist nicht völlig
trivial, sondern ein kleines bisschen komplex, aber nicht unbedingt in
dem Ausmaß, wie man es in realen Softwareprojekten vorfindet. Wir haben
als Beispiel die Implementierung es rudimentären Servers auf Basis der
mit Java 7 eingeführten
AsynchronousSocketChannel
s
gewählt.
Im Folgenden werden wir das Beispiel vorstellen
und erläutern. Der Sourcecode dazu ist übrigens unter /
SRC
/
zu finden. Der Leser ist eingeladen, im Verlauf der Erläuterung nach
dem Memory Leak Ausschau zu halten.
Vorbemerkung: Client-Server-Kommunikation
Vorab eine kurze Erläuterung zum Prinzip
der Client-Server-Kommunikation und der Architektur des Servers (siehe
Abbildung 1): Ein Server bietet seinen Service an einem Serversocket
an. Der Client verbindet sich mit dem Serversocket. Der Server akzeptiert
den Client und erzeugt dabei intern einen neuen Socket, über den dann
der Datenaustausch mit dem Client stattfindet. Wir wollen uns in unserem
rudimentären Server auf das Empfangen von Client-Daten beschränken; zurücksenden
wollen wir nichts. Deshalb hat der Server zwei wesentliche Aktionen auszuführen:
das Akzeptieren von neuen Clients (
accept
) und das Empfangen von
Client-Daten (
read
).
Abbildung 1: Client-Server-Kommunikation
Wenn man Asynchronous Channels verwendet,
geht es im Prinzip genauso. Anstelle des Serversocket benutzt man einen
AsynchronousServerSocketChannel
und statt des Socket einen
AsynchronousSocketChannel
.
Statt die
accept
- und
read
-Aktionen
synchron zu machen, werden sie asynchron abgearbeitet von Callbacks, die
in die Channels eingehängt werden, und vom Framework aufgerufen werden,
sobald Ereignisse/Daten am Channel anliegen.
Der Vorteil der Asynchronous Channels gegenüber
den „normalen“ Sockets ist, dass der Server nicht mit synchronen Methodenaufrufen
der
Socket
-
bzw.
ServerSocket
-Klasse
auf die Aktionen des Clients warten muss. So muss der Server beispielsweise
nicht darauf warten, dass ein Client sich am Serversocket meldet, um akzeptiert
zu werden; er muss auch nicht auf Daten warten, die vom Client gesendet
werden. Die Asynchronous Channels erlauben es, auf diese Ereignisse mit
Callbacks zu reagieren. Dabei nimmt einem der Asynchronous-Channel-Framework
viel Arbeit ab und es ergibt sich trotz Asynchronität ein relativ einfaches
Programmiermodell für den Server. Weitere Details zu den Asynchronous
Channels finden sich in einem unserer zurückliegenden Artikel über die
NIO2 Erweiterungen in Java 7 (/
NIO2
/).
Schauen wir uns nun die Implementierung an.
Den Client-Code werden wir nicht betrachten, weil unser Memory Leak im
Server entsteht. Wir konzentrieren uns deshalb allein auf die Server-Implementierung.
Der gesamte Sourcecode inklusive Test-Clients ist aber unter /
SRC
/
zu finden.
Die Server-Konstruktion
Die
Server
-Klasse
hat zwei Methoden: einen Konstruktor und eine public Methode
doAccepting()
.
Der Konstruktor sieht dabei folgendermaßen aus:
public
Server()
throws IOException {
serverSocket Channel = AsynchronousServ erSocketChannel.open(); serverSocket Channel .bind(new InetSocketAddress(SERVER_PORT));
}
Der Konstruktor öffnet den
AsynchronousServerSocketChannel
am
SERVER_PORT
.
Damit steht der Serversocket bereit, mit dem sich der Client verbindet
und an dem der Server Clients akzeptiert.
Accept-Callback einhängen
Für das Akzeptieren wird ein
accept
-
Callback
eingehängt. Dafür ist die
doAccepting()
-Methode
des Servers zuständig. Sie hängt einen
accept
-Callback
vom Typ
CompletionHan
dler<AsynchronousSocketChannel,
Objekt>
in den
Asynchronous
S
erverSocketChannel
ein. Dieser Callback behandelt nun das Akzeptieren der Clients. Die
doAccepting()
-Methode
sieht so aus:
public void doAccepting() {
serverSocketChannel. accept( null, // Zeile 1 new CompletionHandler<AsynchronousSocketChannel,Object>() { /* … later … */ } ); // Zeile 2
}
Die Implementierung des Callbacks (siehe
// Zeile 2) als Anonymous Inner Class schauen wir uns gleich genauer an.
Vorher noch eine Bemerkung zur Semantik der Callbacks bei Asynchronous
Channels: jeder eingehängte Callback bleibt nur so lange aktiv, bis
er aufgerufen wurde. Das heißt, er muss immer wieder neu eingehängt
werden - am Besten gleich als letztes Statement des Callbacks selbst.
Und genau so werden wir es später implementieren (siehe z.B. // Zeile
11).
Der eingehängte Accept-CallbackFür unseren accept - Callback vom Typ CompletionHan dler<AsynchronousSocketChannel, Objekt> (siehe // Zeile 2) müssen zwei Methoden implementiert werden:public void completed(AsynchronousSocketChannel channel, Object attachment) // Zeile 3
public void fail
ed
(Throwable
t, Object attachment)
//
Zeile
4
Die erste Methode
completed()
(siehe // Zeile 3) wird aufgerufen, wenn das Akzeptieren eines Clients
funktioniert. Bei einem erfolgreichen Akzeptieren wird ein neuer Socket
geöffnet, über den anschließend die weitere Kommunikation mit dem Client
geht, der gerade akzeptiert wurde. Bei unserer asynchronen Arbeitsweise
über Callbacks wird der neue Socket vom Typ
AsynchronousSocketChannel
als erster Parameter der
completed()
-Methode
des
accept
-Callbacks zur Verfügung gestellt.
Auf den zweiten Parameter kommen wir gleich noch zu sprechen.
Die zweite Methode
fail
ed
()
(siehe // Zeile 4) wird im Fehlerfall aufgerufen. Ihr erster Parameter
vom Typ
Throwable
beschreibt die genaueren
Umstände des Fehlers. Auf die Fehlerbehandlung wollen wir hier nicht
weiter eingehen. Für unser Problem - das Memory Leak - ist die Fehlerbehandlung
nicht relevant.
Der Vollständigkeit halber sei noch erwähnt,
dass der zweite Parameter beider Methoden (d.h. das
attachment
)
das Objekt ist, das oben als erster Parameter beim Einhängen des Callbacks
mit
accept()
(siehe // Zeile 1) mitgegeben
wurde. Dieses Objekt wird beim Ausführen des Callbacks dann wieder als
zweiter Parameter den Callback-Methoden mitgegeben. Wir haben für dieses
Durchschleusen in unserer Implementierung keine Verwendung, deshalb haben
wir beim
accept()
als ersten Parameter
null
übergeben (siehe // Zeile 1). Das bedeutet, wir können diesen Parameter
im Callback ignorieren.
Die Implementierung des eingehängten Accept-CallbackKommen wir also nun zur Implementierung der completed() -Methode aus // Zeile 3.public void completed(AsynchronousSocketChannel channel, Object attachment) { // Zeile 5 ClientSession session = new ClientSession(); // Zeile 6 sessionToChannel.put(session, channel); // Zeile 7
final ByteBuffer buf = ByteBuffer.allocateDirect(256); // Zeile 8 channel.read(buf, session, // Zeile 9 new CompletionHandler<Integer, ClientSession>() // Zeile 10 { /* … later … */ }); serverSocketChannel.accept(null, this); // Zeile 1 1
}
Als erstes erzeugen wir uns ein
ClientSession
-Objekt
für den neuen Client (siehe // Zeile 6). Wie die Klasse
ClientSession
genau aussieht, schauen wir uns gleich weiter unten an. Die Client-Session
ist dafür zuständig, die vom Client empfangenen Daten zu verarbeiten.
Dann merken wir uns in einer Map die Beziehung
zwischen
session
und
channel
(siehe
// Zeile 7). Diese Information brauchen wir später bei der Implementierung
des
read
-Callbacks, der das Empfangen
der Clientdaten behandelt. Die Map
sessionToChannel
ist ein Attribut der
Server
-Klasse, das
so definiert ist:
private final Map<ClientSession, AsynchronousSocketChannel>
sessionToChannel =
Unsere Map ist vom Type
ConcurrentHashMap,
damit die Callbacks von verschiedenen Clients problemlos konkurrierend
auf sie zugreifen können.
Wie oben bereits erwähnt, wollen wir in
unserem einfachen Beispiel nur Daten vom Client zum Server senden; die
umgekehrte Richtung vom Server zum Client wollen wir nicht implementieren.
Das heißt, was uns nun noch im Server fehlt, ist die Funktionalität für
das Empfangen der Clientdaten (
read
). Das geschieht in // Zeile
8 und // Zeile 9.
Dazu erzeugen wir uns einen
ByteBuffer
,
in den die empfangenen Daten geschrieben werden sollen (siehe // Zeile
8).
Wir hängen diesen
ByteBuffer
zusammen mit der
session
und dem
read
-Callback,
der beim Empfangen der Daten ausgeführt werden soll, in den
channel
zum Client ein; das geschieht über den Aufruf der Methode
read()
(siehe
// Zeile 9). Die Implementierung des
read
-Callbacks
(siehe // Zeile 10) als Anonymous Inner Class schauen wir uns weiter unten
genauer an.
Der zweite Parameter des
read()
,
also die
session
, wird nicht von der
completed()
-Methode
selbst genutzt, sondern durchgeschleust und beim Aufruf des
read
-Callbacks
wieder übergeben. Wie wir weiter unten sehen werden, brauchen wir sie
dort (siehe // Zeile 13 und // Zeile 16). Dieses Durchschleusen eines
Objekts vom Einhängen des Callbacks bis zum Ausführen des Callbacks hatten
wir uns oben bei dem
accept-
Callback
schon
mal angesehen (siehe // Zeile 1), dort aber nicht genutzt, sondern stattdessen
nur
null
übergeben; hier beim
read
-Callback
brauchen wir das Durchschleusen aber.
Wie schon oben erwähnt, bleibt der
accept
-Callback
nur so lange aktiv, bis er aufgerufen wurde. Das heißt, er muss immer
wieder neu eingehängt werden, damit weiterhin neue Clients akzeptiert
werden. Dieses Wiedereinhängen machen wir als letzte Aktion in der
completed()
-Methode
(siehe // Zeile 11). Wir nehmen einfach den Callback wieder, der gerade
ausgeführt wird. Das heißt der zweite Parameter im erneuten
accept()
-Aufruf
ist
this
und der erste ist
null
wie schon oben in // Zeile 1, weil wir kein Objekt an den
accept
-Callback
durchschleusen wollen.
Die Client-Session
Sehen wir uns nun die Client-Session genauer
an; wir hatten ein
ClientSession
-Objekt
im
accept
-Callback erzeugt (siehe //
Zeile 6) und verwendet. Die Klasse
ClientSession
sieht wie folgt aus:
class ClientSession { private static final AtomicInteger clientCnt = new AtomicInteger(1);
private final int myId = clientCnt.getAndIncrement(); private volatile int byteCnt;
public boolean handleInput(ByteBuffer buf, int len) { // Zeile 1 2 if (len >= 0) { byteCnt += len; return true; } else { System.out.println("received " + byteCnt + " bytes from client " + myId); return false; } }
}
In unserem einfachen Server-Beispiel simuliert
diese Klasse eigentlich nur die Funktionalität, die typischerweise in
einer Client-Session angesiedelt ist. Zum Beispiel kann man sich vorstellen,
dass in einem echten Server, bei dem die Kommunikation aus XML-Dokumenten
besteht, die empfangenen Daten in der Session gespeichert werden, bis sie
ein vollständiges Dokument ergeben, das geparst und weiterverarbeitet
werden kann.
Die zentrale (und einzige) Methode unserer
Client-Session ist die
handleInput()
-Methode
(beginnend in // Zeile 12). In unserem einfachen Fall wird in der
handleInput()
-Methode
nur die Anzahl der vom Client empfangenen Bytes gezählt, solange bis der
Client die Verbindung beendet. Dann wird die Anzahl zusammen mit der
Client-Session-ID ausgedruckt. Der Returnwert der
handleInput()
-Methode
ist
true
, wenn weiterer Input erwartet
wird, und
false
, wenn die Verbindung
beendet ist. Die Methode
handleInput()
arbeitet sehr eng mit dem
read
-Callback
zusammen, der das Empfangen der Daten abhandelt.
Der eingehängte Read-Callback
Kommen wir deshalb nun zum
read
-
Callback
vom Typ
CompletionHandler<Integer,
ClientSession>
, den wir in // Zeile 10 eingehängt hatten. Wie
vorher auch bei dem
accept
-
Callback
sind die zwei Methoden
completed()
und
failed()
zu implementieren. Wir wollen auch hier der Devise folgen „Besser keine
Fehlerbehandlung zeigen als eine rudimentäre.“ und schauen uns deshalb
nur den Code von
completed()
an:
public void completed(Integer len, ClientSession
session)
{
AsynchronousSocketChannel channel = sessionToChannel.get( session); // Zeile 13
if ( session.handleInput(buf, len)) { // Zeile 14 buf.clear(); // Zeile 15 channel.read(buf, session, this); // Zeile 16 } else { try { channel.close(); } // Zeile 17 catch (IOException e) { /* ignore */ } } }
Die Parameter der Methode sind
len
,
die Anzahl der Bytes, die vom Client empfangen wurden, und die
ClientSession
,
die hierhin vom
read(
)
-Aufruf
(in // Zeile 9) durchgeschleust wurde. Wir holen uns als erstes den zur
session
korrespondierenden
channel
aus unserer
sessionToChannel
-Map
(siehe // Zeile 13).
Dann rufen wir auf der session die handleInput() -Method auf und übergeben dabei den ByteBuffer buf mit den Inputdaten sowie die Anzahl der Bytes, die in den ByteBuffer gelesen wurden (siehe // Zeile 14). Wir haben Zugriff auf den Puffer buf , weil er im äußeren Scope (dem der completed( ) -Methode des accept - Callbacks) final deklariert war (siehe // Zeile 8) und wir in der inneren Klasse (der Implementierung des des read- Callbacks) Zugriff auf die final Variablen des äußeren Scopes haben.
Der Returnwert von
handleInput()
bestimmt, ob wir weiter Daten vom Client lesen oder die Verbindung zum
Client beenden sollen. Ein kurzer Blick auf die Implementierung von
handleInput()
oben (beginnend in // Zeile 12) zeigt, dass die Verbindung zum Client beendet
wird, wenn eine negative Anzahl Bytes vom Client empfangen wurde. Das
macht Sinn, denn wenn man in die Javadoc schaut, wird man finden, dass
len
== -1
das
end-of-file
bedeutet, in unserem Fall also, dass
der Client die Verbindung beendet hat.
Der Code des
if
-
und
else
-Zweigs ist relativ übersichtlich.
Im Fall von „Weiterlesen“ (
if
-Zweig) setzten
wir den ByteBuffer mit
clear()
zurück
(siehe // Zeile 14), um ihn anschließend im
rea
d-
Callback
wieder zu verwenden (siehe // Zeile 16). Wie oben im
ac
cept-
Callback
setzeen wir wieder den Callback, den wir gerade durchlaufen, als Callback
in den
channel
ein. Der dritte Parameter
von
read()
ist deshalb
this
.
Im Fall von „Verbindung beenden“ (
else
-Zweig)
rufen wir die
close()
Methode auf dem
channel
auf (siehe // Zeile 17). Wir müssen dies in einem
try
-Block
tun, da
close()
eine
IOException
werfen kann. Eine Behandlung der Exception implementieren wir nicht.
Was soll man auch tun, wenn ein
close()
nicht fehlerfrei funktioniert? Wiederholen? Wie häufig?
Ohnehin, an der fehlenden Behandlung der IOException hier liegt das Memory Leak nicht. Auf das Memory Leak können wir nämlich jetzt zurückkommen. Wir haben uns nun den gesamten Code des Servers angesehen. Wo haben wir Objekte vergessen, die den Speicherverbrauch kontinuierlich anwachsen lassen? Wo ist das Memory Leak?
Da der gesamte Code des Servers recht übersichtlich
ist, ist das Memory Leak noch relativ einfach zu finden. Das Memory-Leak
entsteht, weil wir für jeden Client einen Eintrag in der
sessionToChannel
-Map
anlegen (siehe // Zeile 7). Wir löschen diesen Eintrag aber nicht,
wenn wir mit dem
close()
die Verbindung
zum Client endgültig beenden (siehe // Zeile 17). Damit bleiben sowohl
das
ClientSession
-Objekt (
Key
des Eintrags) als auch das
AsynchronousSocketChannel
-Objekt
(
Value
des Eintrags) weiter referenziert und keines der Objekte
kann vom Garbage Collector weggeräumt werden. Bei jedem Client, mit
dem wir nicht mehr kommunizieren, wachsen also in der
sessionToChannel
-Map
die vergessenen Objekte an. Das ist ein bisschen so, als hätten wir
in einer Sprache wie C/C++ vergessen, unseren Heap-Speicher mit
free()
bzw.
delete
()
wieder frei zu geben. Deshalb ist die Bezeichnung „Memory Leak“ auch
in einer Sprache wie Java irgendwie gerechtfertigt.
Korrektur: Eliminierung des Memory Leaks
Für die Korrektur gibt es mehrere Möglichkeiten.
Lösung #1:
Wir müssen beim endgültigen Beenden der
Kommunikation mit einem Client den korrespondierenden Eintrag in der Map
löschen. Die korrigierte
completed(
)
-Methode
des
read
-Callbacks müsste also so aussehen:
public void completed(Integer len, ClientSession
s
ession)
{
AsynchronousSocketChannel channel = sessionToChannel.get( s ession);
if (session.handleInput(buf, len)) { buf.clear(); channel.read(buf, session, this); } else { sessionToChannel.remove(session); try { channel.close(); } catch (IOException e) { /* ignore */ } }
}
Diese Korrektur ist zwar naheliegend, aber nicht unbedingt die im Gesamtkontext schlüssigste. Sinnvoll wäre eine Lösung, bei der man den Map-Eintrag und damit die Map nicht braucht, so dass man das Löschen des Map-Eintrags gar nicht erst vergessen kann. Lösung #2:
Die einfachste Lösung sieht so aus, dass
man die Map weglässt und in der
completed()
-Methode
des
accept
-Callbacks den ersten Parameter
vom Typ
AsynchronousSocketChannel
als
final
deklariert (siehe // Zeile 5):
public void completed( final AsynchronousSocketChannel channel, Object attachment) { // Zeile 5 ...
}
Da der read - Callback als Anonymous Inner Class im Scope dieser Methode implementiert ist, kann er so direkt auf den channel zugreifen und braucht ihn sich nicht aus der Map zu holen. Diese Lösung wollen wir an dieser Stelle nicht in allen Details zeigen. Sie ist im Sourcecode unter /SRC/ zu finden. Lösung #3:
Wem diese Lösung mit der final-Deklaration
und der anonymen inneren Klasse zu sehr nach „glücklicher Fügung“
aussieht, dem können wir noch eine dritte Variante anbieten. Auch sie
verzichtet auf die Map. Gegenüber der vorherigen Lösung lässt sie
sich aber deutlich besser verallgemeinern.
Wenn zwischen der
session
und dem
channel
eine so enge Beziehung
besteht, dass wir beide über einen Map-Eintrag verbinden wollen, so können
wir doch alternativ auf die Map verzichten und den
channel
zu einem Attribut der
session
machen.
Der
channel
würde
dann im Konstruktor der
ClientSession
gesetzt. Da die
session
beim
read
-
Callback
durchgeschleust wird, muss man sie nur noch um einen Getter auf das neue
channel
-
Attribut
erweitern. So steht dann zusammen mit der
session
immer auch der
channel
im
read
-
Callback
Verfügung. Auch diese Lösung wollen wir hier nicht im Detail zeigen.
Sie ist auch im Sourcecode unter /SRC/ zu finden.
Fazit
Memory Leaks können bereits in relativ
kurzen Programmen passieren. Sie sind selten offensichtlich und passieren
häufig bei der Benutzung von Frameworks oder „fremden“ Abstraktionen,
wo man sich u.U. nicht ganz klar darüber ist, wer welche Objekte wie lange
benötigt. Im diskutierten Beispiel hätten wir beachten müssen, dass
mit dem Ende einer Client-Verbindung sowohl die Client-Session als auch
der benutzte Channel nicht mehr benötigt werden und dass sie deshalb explizit
unerreichbar gemacht werden müssen, damit sie vom Garbage Collector weggeräumt
werden können. Man darf Referenzen, wie wir sie in der Map gehalten
haben, in solchen Situationen nicht vergessen.
Zusammenfassung und Ausblick
In diesem Artikel haben wir eine Reihe
zum Thema „Memory Leaks in Java“ begonnen. Wir haben gesehen, dass
Memory Leaks durch im Programm „vergessene“ Objekte entstehen, die
der Garbage Collector aber weiterhin als erreichbar identifiziert und deshalb
nicht wegräumt. An der Implementierung unseres Servers haben wir gesehen,
wie so ein Memory Leak konkret aussehen kann. Das Beispiel ist durchaus
typisch. In einem relativ komplexen Kontext vergisst man, Referenzen
auf Objekte aufzulösen, die im weiteren Ablauf des Programms nicht mehr
benötigt werden.
In den folgenden Artikeln wollen wir uns weitere Themen in diesem Umfeld
ansehen, darunter:
Verweise
Die gesamte Serie über Memory Leaks:
|
||||||||||||||||||||||||||||||||||||||||||
© Copyright 1995-2013 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/64.Mem.Leaks/64.Mem.Leaks.html> last update: 21 Oct 2013 |