|
||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | ||||||||||||||||||||||||||||
|
Effective Java - Memory Leaks - Akkumulation
|
|||||||||||||||||||||||||||
Mit dem vorangegangenen Beitrag (/ ML1 /) haben wir die Reihe zum Thema Memory Leaks in Java gestartet. Wir hatten uns die konkrete Implementierung eines Servers angesehen, der ein Memory Leak hatte, das zu einem OutOfMemoryError führen konnte. Dieses Mal wollen wir detailliert die Mechanismen diskutieren, die allgemein ein Memory Leak und einen daraus resultierenden OutOfMemoryError verursachen. Memory Leaks in Java
Als Beispiel für ein Memory Leak haben
wir uns beim letzten Mal die Implementierung eines rudimentären Servers
auf Basis der mit Java 7 eingeführten
AsynchronousSocketChannel
s
angesehen. Das Memory Leak entstand dadurch, dass wir für jeden Client
Verwaltungsinformation (die
ClientSession
)
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. Dadurch vergrößert sich die Map mit jedem neuen
Client. Erzeugt man dann in einem Testprogramm genügend viele Clients
hintereinander, so stürzt der Server mit einem
OutOfMemoryError
ab. Der Sourcecode zu dem Beispiel sowie verschiedene alternative Korrekturen,
die wir im letzten Artikel besprochen haben, finden sich hier (/
SRC
/).
Ungewollte Referenzen
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
unser Memory-Leak-Beispiel vom letzen 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.
Akkumulation von Leaks führt zu OutOfMemoryError
Eine ungewollte Referenz allein führt
noch nicht zu einem problematischen Memory Leak. Noch ein zweiter Aspekt
ist wichtig, damit es zu einem
OutOfMemoryError
aufgrund eines Memory Leaks kommt. Der Programm-Algorithmus muss so fehlerhaft
sein, dass mit dem weiteren Ablauf immer mehr Objekte über ungewollte
Referenzen erreichbar sind. Das ist in unserem Beispiel gegeben: Immer
wenn wir die Kommunikation mit einem Client beenden, "vergessen" wir dessen
Verwaltungsdaten in der Map. Die Anzahl der "geleakten" Objekte kann
also beliebig anwachsen.
Typisch für Memory Leaks, die zu
OutOfMemoryError
führen, ist der Umstand, dass sie nicht durch einzelne Referenzen wie
Stackvariablen oder Felder von Referenztypen entstehen, sondern durch Akkumulation
von Referenzen. So kann eine einzelne Stackvariable von einem Referenztyp,
die wir ganz zu Anfang in der
main
-Methode
angelegt haben, zwar eine ungewollte Referenz sein, aber sie führt nicht
zu einem
OutOfMemoryError
. Der Code für
eine solche Situation kann zum Beispiel so aussehen:
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. Die Referenz
arg
Msg
ist also eine ungewollte Referenz, weil sie ab einem bestimmten Zeitpunkt
im Programmablauf auf ein Objekt zeigt, das nicht mehr genutzt wird.
Der von
arg
Msg
referenzierte
String ist ein Memory Leak, weil die Stackvariable
arg
Msg
ihn bis zur Beendigung des
main
-Threads
(in unserem Fall: bis zum Ende des Programms) erreichbar hält. Was aber
einer Stackvariablen wie
arg
Msg
für einen
OutOfMemoryError
fehlt, ist
die Fähigkeit zur Akkumulation; die Anzahl der "geleakten" Objekte wächst
nicht an. Ein ganz ähnliches Szenario ergibt sich, wenn nicht mehr benötigte
Objekte über ein Feld von einem Referenztyp (statt einer Stackvariablen)
am Leben erhalten werden; solange sie nicht akkumulieren, hat man zwar
ungewollte Referenzen und Memory Leaks, aber keinen
OutOfMemoryError
.
Bei einem Memory Leak, das zu einem
OutOfMemoryError
führt, ist in der Regel irgendeine Art von Ressourcen-Verwaltung im Spiel.
Diese Ressourcen-Verwaltung (oder genauer betrachtet deren interne Datenstruktur)
bildet die ungewollte Referenz. In unserem Server-Beispiel aus dem vorangegangenen
Artikel ist diese Ressourcen-Verwaltung die Map. Deshalb ist es zur Vermeidung
von Memory Leaks wichtig, dass die Verwaltung korrekt benutzt wird: Jeder
Eintrag, der mit
put()
in
der Map abgelegt wird, muss irgendwann mit
remove()
wieder gelöscht werden.
In gewisser Weise gibt es hier eine Analogie
zu Memory Leaks in Programmiersprachen mit explizitem Speichermanagement
wie zum Beispiel C++. In C++ muss man darauf achten, dass jedes Objekt,
das mit
new
angelegt wird, auch mit
delete
wieder freigegeben wird. In Java muss man darauf achten, dass Objekte,
die an eine Verwaltung übergeben werden (
put()
),
auch wieder gelöscht werden (
remove()
).
Das Prinzip ist in beiden Fällen gleich: es geht um das korrekte Verwalten einer Ressource. In C++ ist die fragliche Ressource der rohe Speicher, den man anfordert und freigibt. Mit Speicherverwaltung auf diesem Niveau muss man sich in Java nicht befassen, weil der Garbage Collector die Speicherbereinigung erledigt. Dennoch muss man sich in Java um die korrekte Verwaltung von Ressourcen kümmern. Die fraglichen Ressourcen sind dabei Java-Objekte, die man in eine Verwaltungsstruktur einhängt und später wieder aushängen muss, damit sie unerreichbar und „garbage collectible“ werden. Die Dualität der Ressourcen-Verwaltung
Es geht ganz allgemein um das duale Prinzip
des Ein- und Aushängen (oder „acquire“ / „release“ oder „attach“
/ „detach“ oder Zugänglichmachen und Freigeben) von Ressourcen.
Dieses duale Prinzip gibt es auch an anderen Stellen in Java, zum Beispiel
beim „lock“ / „unlock“ von Locks oder beim „open“ / „close“
von Streams oder Channels. Im Zusammenhang mit dem „new“ / „delete“
von Speicher hat uns der Garbage Collector von der Bürde des „delete“
befreit. Die Dualität an sich bleibt aber erhalten. Diese Dualität
zu beachten ist immer dann ganz besonders wichtig, wenn wiederholt und
regelmäßig Objekte in langlebige Verwaltungsstrukturen eingehängt werden.
Wenn dort das Aushängen vergessen wird, kommt es zur Akkumulation von
ungewollten Referenzen, Memory Leaks und letztlich zum
OutOfMemoryError
.
Nun haben wir es bislang so dargestellt,
als gäbe es nur die Möglichkeit, eine Ressourcen-Verwaltung richtig (man
löscht) oder falsch (man löscht nicht) zu benutzen. Es gibt aber auch
dabei eine Grauzone. Zum Beispiel, wenn man die Verwaltung im Allgemeinen
richtig nutzt und nur in einem speziellen Zweig des Programms das Löschen
vergessen wird. Wenn dieser Fehler während des konkreten Programmablaufs
nicht oder nur sporadisch auftritt, entstehen entweder gar keine Memory
Leaks oder der 'geleakte' Speicher ist von seinem Umfang so gering, dass
es zu keinem
OutOfMemoryError
mit anschließendem
Programmabsturz kommt.
Ein weiters Beispiel: Callbacks
Ein Beispiel für Callbacks im JDK sind die
AWT Event Listener. Schauen wir uns einmal ganz konkret den
java.awt.event.ActionListener
des
java.awt.Button
an. Man muss mit
einer konkreten Klasse das
ActionListener
-Interface
(das heißt, deren Methode
actionPerformed
()
)
implementieren und dann eine Instanz dieser Klasse als Callback mit der
Methode
addActionListener()
beim
Button
-Objekt
registrieren. Intern werden die registrierten Callback-Objekte mit Hilfe
des
java.awt.AWTEventMulticaster
als
verkette Liste gespeichert. Wenn ein Event aufgetreten ist, werden die
für diesen Event registrierten Callbacks mit dem Event als Parameter der
Reihe nach, wie sie in der Liste verkettet sind, aufgerufen.
Soweit die Grundzüge des AWT Event Callback
Mechanismus. Wie kann es dabei jetzt zu einem Memory Leaks kommen?
Ganz einfach: Bei ungeschicktem Programmdesign
kann es vorkommen, dass man immer wieder neue Listener-Callback-Objekte
in die Liste einhängt, ohne die alten mit
removeActionListener()
zu entfernen. Die "vergessen" Callback-Objekte akkumulieren sich, da
die verkettete List der Callback-Objekte immer länger werden kann. Wir
haben also eine ähnliche Situation wie bei der Map im Serverbeispiel.
Wir müssen die Ressourcen-Verwaltung, die hinter dem Callback-Mechanismus
steckt, richtig bedienen Wir müssen die Dualität der Ressourcen-Verwaltung
beachten und die Callback-Objekte nicht nur einhängen, sondern auch wieder
aushängen, wenn wir sie nicht mehr brauchen.
Dass eine Callback-Verwaltung auch anders,
das heißt ohne explizites Aushängen, funktionieren kann, haben wir in
unserem Serverbeispiel des letzten Artikels gesehen. Die Callbacks bei
den
AsynchronousSocketChannel
s bleiben
nur für einen einzigen Aufruf des Callbacks eingehängt; sie werden nicht
dauerhaft in internen Datenstrukturen gespeichert. Das hat natürlich den
Nachteil, dass sich der Callback im Normalfall immer wieder selbst einhängen
muss. Der Vorteil ist jedoch, dass der Callback nicht explizit ausgehängt
werden muss.
Fazit: Man muss die Ressourcen-Verwaltung, die man benutzt kennen, verstehen und korrekt benutzen, um Memory Leaks zu vermeiden. Callbacks und Non-static Inner Classes
Kommen wir noch mal auf die Callback-Objekte
zurück, die sich bei einem Event-Callback-Mechanismus akkumulieren können.
Hier wird bisweilen argumentiert, dass die einzelnen Callback-Objekte nicht
besonders groß seien und damit keine wirkliche Memory Leak Problematik
vorliegt, die zu Speicherengpässen führen kann. Bei einer solchen Argumentation
wird jedoch vergessen, dass Callbacks meist als Non-Static Inner Classes
implementiert werden. Das biete sich an, weil man in dem jeweiligen Kontext
meist eine Klasse braucht,
Genau für solche Situationen sind die
Non-Static Inner Classes in Java eingeführt worden; man wollte damit die
Implementierung von Callbacks einfacher machen, als es bis dahin in Java
der Fall war.
Wichtig für unsere Überlegungen ist nun,
dass eine Instanz einer Non-Static Inner Class (also unser Callback-Objekt)
eine Referenz auf das assoziierte Objekt der äußeren umgebenden Klasse
hält. Häufig wird diese Referenz als verborgen (im Englischen:
hidden
)
bezeichnet wird. Sie kann aber mit <
outerClassName>.this
explizit angesprochen werden. Diese „hidden“ Referenz führt dazu,
dass nicht nur das eingehängte Callback-Objekt selbst, sondern auch das
assoziierte Objekt der äußeren Klasse (eben über diese Referenz) erreichbar
bleibt. Das heißt, das Leak besteht nicht nur aus den "vergessenen"
Callback-Objekten (die unter Umständen wirklich nicht so groß sind),
sondern zusätzlich auch aus den mit ihnen assoziierten Objekten der äußeren
Klasse (die ihrerseits möglicherweise noch eine ganze Menge anderer Objekte
referenzieren).
OutOfMemoryError bedeutet nicht (immer) Memory LeakDamit haben wir die beiden Kernaspekte eines Memory Leaks ausführlich diskutiert, nämlich:Im nächsten Artikel werden wir uns ungewollten Referenzen widmen, die nicht zu Memory Leaks führen, aber dennoch stören.
Hier noch eine abschließende Bemerkung:
Bisweilen wird unterstellt, dass jeder
OutOfMemoryError
auf einem Memory Leak, also auf ungewollten Referenzen, basiert. Das
ist jedoch nicht immer der Fall. Manchmal wird der Speicherbedarf der
Applikation einfach zu groß.
Das kann man an dem Beispiel aus dem letzten
Artikel sehen. Betrachten wir das Programm in einer seiner korrigierten
Versionen, wo es keine ungewollten Referenzen mehr gibt und alle clientspezifischen
Verwaltungsdaten korrekt bei Beendigung der Kommunikation mit dem Client
ausgehängt und „garbage collectible“ werden. Auch ohne ungewollte
Referenzen und Memory Leaks kann es zu einem
OutOfMemoryError
kommen, nämlich dann, wenn sehr viele Clients gleichzeitig mit dem Server
verbunden sind und damit sehr viele Verwaltungsdaten (Objekte vom Typ
ClientSession
)
im Speicher gehalten werden müssen. Dann sieht die Situation beim
OutOfMemoryError
so ähnlich aus wie im Fehlerfalle mit dem Memory Leak: man wird unzählige
Objekte vom Typ
ClientSession
im
Speicher vorfinden. Diese Objekte sind aber nicht ungewollt referenziert,
sondern sie werden alle gebraucht. Die Applikation verschwendet keinen
Speicher, sondern sie ist wirklich so groß und braucht bei weiteren Clients
auch noch mehr Speicher.
Wenn man sich unsicher ist, was der Grund für den OutOfMemoryError war, muss man die Ursachen genauer analysieren. Wie man dabei vorgeht, wollen wir uns in einem zukünftigen Artikel ansehen. Zusammenfassung und Ausblick
Wir haben uns angesehen, wie ungewollte
Referenzen Memory Leaks erzeugen und die Akkumulation von Memory Leaks
zum Abbruch des Programms mit
OutOfMemoryError
führen kann. Die Akkumulation von Leaks entsteht dadurch, dass wir Abstraktionen
nutzen, die eine explizite Freigabe verlangen. Beispiel für solche Abstraktionen
sind im einfachsten Fall Collections, wie die
Map
in unserem Beispiel vom letzten Artikel. Aber auch andere Ressource-Verwaltungen
besonders in Frameworks, wie etwa das Callback-Management der AWT-Komponenten
in dem Beispiel in diesem Artikel, fallen darunter. In solchen Situationen
ist es wichtig, dass unser Programmdesign die Anforderungen der Ressourcen-Verwaltung
berücksichtigt und die explizite Freigabe in allen Fällen korrekt erfolgt.
Beim nächsten Mal wollen wir uns Memory
Leaks ansehen, die sich nicht akkumulieren und damit in der Größe beschränkt
bleiben. Auch wenn sie nicht zu Programmabbrüchen auf Grund von
OutOfMemoryError
führen, sind sie auf ihre eigene Weise interessant.
Literaturverweise
Die gesamte Serie über Memory Leaks:
|
||||||||||||||||||||||||||||
© Copyright 1995-2016 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/65.Mem.Akkumulation/65.Mem.Akkumulation.html> last update: 29 Nov 2016 |