AngelikaLanger.com - Java Performance - Memory Hotspots (original) (raw)

Java Performance - Memory Hotspots

Java Performance: Profiling Strategien - Memory HotSpots Wie findet man Memory Allocation Hot Spots und Memory Leaks? JavaSPEKTRUM, März 2006 Klaus Kreft & Angelika Langer Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im JavaSPEKTRUM erschienen ist. Die übrigen Artikel dieser Serie sind ebenfalls verfügbar (click here).

In unserer Artikelreihe über Java-Performance beschäftigen wir uns mit dem Profiling von Java-Anwendungen. Beim Profiling geht es um Messungen, die u.a. Aussagen über die Performance einer gesamten Anwendung liefern. Solche Messungen werden während der Entwicklung vorgenommen, um Schwachstellen im Programm wie zum Beispiel Performance-HotSpots und -Bottlenecks zu identifizieren und durch ein anschließendes Tuning zu beseitigen. Im letzten Beitrag haben wir uns angesehen, wie man funktionale Performance-HotSpots (= Methoden, die viel Zeit brauchen und oft aufgerufen werden) mit Hilfe von entsprechenden Tools aufspüren und beseitigen kann. Dieses Mal wollen wir uns mit Memory-HotSpots befassen, weil auch das Beschaffen und Freigeben von Speicher Performance kostet.

Die Performance eines Programm hängt nicht nur davon ab, wie schnell die Methoden des Programms ablaufen, sondern die Performance wird auch davon beeinflußt, wie viele Objekte während des Programmablaufs erzeugt und wieder weggeräumt werden. Deshalb ist es im Rahmen eines Performance-Profilings sinnvoll, auch den Speicherverbrauch eines Programms zu untersuchen. Dabei sucht man einerseits nach Object-Creation-Hotspots, Das sind Stellen im Programm, an denen auffallend viele Objekte erzeugt werden. Zum anderen sucht man nach Memory-Leaks. Das sind Objekte, die erzeugt aber nicht mehr weggeräumt werden. In einem anschließenden Tuning wird man versuchen, diese Schwachstellen im Programm zu beseitigen.

Daneben wird man auch den Garbage Collector selbst überprüfen und ihn gegebenenfalls tunen. Wenn die Anwendung viel Zeit mit Garbage Collection verbringt, dann wirkt sich das natürlich auch negativ auf die Performance des gesamten Programms auf. Das GC-Monitoring und –Tuning wollen wir aber einen späteren Artikel verschieben. In diesem Artikel geht es um Object Creation HotSpots und Memory Leaks.

Object-Creation-HotSpots

Unter einem Memory- oder auch Object-Creation-HotSpot versteht man eine Stelle im Programm (typischerweise eine Methode), die auffallend viele Objekte erzeugt.

Warum ist ein Object-Creation-HotSpot relevant für die Performance einer Anwendung? Weil das Erzeugen und Wegräumen (englisch: garbage collection) von Objekten Zeit kostet. Insbesondere die Erzeugung von zahlreichen Objekten mit durchweg kurzer Lebenszeit wirkt sich negativ auf die Performance der Anwendung aus. Schließlich muss für jedes einzelne Objekt (a) Speicher auf dem Heap alloziert werden, (b) das Objekt per Konstrukturaufruf initialisiert werden, und (c) es muss vom Garbage Collector als unerreichbar erkannt und behandelt werden. Die Erzeugung von Objekten kostet daher nicht nur Ressourcen in Form von Heap-Speicher, sondern auch Performance.

Im Folgenden erläutern wir, wie man Object-Creation-HotSpots findet und skizzieren mögliche Tuning-Maßnahmen.

Such- und Optimierungsstrategie

Die Vorgehensweise zum Aufspüren von Object-Creation-HotSpots werden wir am Beispiel des HPROF zusammen mit HPjmeter zeigen Diese beiden Tools haben wir bereits im letzten Beitrag verwendet. HPROF ist ein JVMPI/JVMTI-Agent, der mit der virtuellen Maschine von Sun ausgeliefert wird. HPjmeter ist ein Freeware-Tool von Hewlett-Packard, das den von HPROF erzeugten Output für die Analyse aufbereitet. Es gibt eine Menge andere freie und kommerzielle Werkzeuge, die ähnliches leisten. Eine gute Übersicht über die Tool-Landschaft findet man unter /TUNE/.

Im ersten Schritt unserer Suche nach Object-Creation-HotSpots sehen wir die Statistik an, die alle Methoden auflistet geordnet nach der Zahl der Objekte, die sie erzeugen (siehe Abb. 1). Wenn eine Methode in dieser Liste ganz oben steht, ist sie ein aussichtsreicher Kandidat für ein erfolgreiches Performance-Tuning, bei dem man die Belastung durch die Objekterzeugung reduzieren will.


Abbildung 1: Liste der Methoden geordnet nach Anzahl der Objekte, die in der Methode erzeugt werden (in HPjmeter)

In diesem Beispiel wäre die der Konstruktor Klasse java.util.Vector ein offensichtlicher Kandidat für ein Performance-Tuning, weil sie auffallend viele Objekte erzeugt, nämlich mehr ca. 275.000 Objekte vom Typ Vector und knapp 140.000 Objekte vom Typ AbstractList.

Wenn die Tuning-Kandidaten Methoden aus dem eigenen Projekt sind, dann hat man Zugang zum Source-Code und wird untersuchen, warum so viele Objekte erzeugt werden und wie das ggf. reduziert werden könnte. Oft wird die Statistik aber angeführt von Methoden, die aus fremden Bibliotheken oder Frameworks stammen. Dann sieht man sich an, von welchen eigenen Methoden diese fremden Methoden gerufen werden. Möglicherweise lassen sich die Aufrufe der fremden Object-Creation-HotSpot-Methoden vermeiden oder reduzieren.


Abbildung 2:Dynamische Aufrufbeziehungen der Methoden (in HPjmeter)

Profiler-Tools bieten zum Verfolgen von Aufrufbeziehungen in der Regel eine graphische Darstellung an (siehe Abb. 2). Man sollte dieses Feature des Profilers auch benutzen und nicht etwa auf den Gedanken kommen, daß man die Aufrufbeziehungen zwischen Methoden auch bequem in der vertrauten Entwicklungsumgebung untersuchen könnte. Entwicklungsumgebungen und andere Tools zeigen nämlich die statischen, nicht aber die dynamischen Aufrufbeziehungen. Aus der Tatsache, dass im Source-Code der Methode f() eine andere Methode g() gerufen wird, kann man nicht schließen, daß der Methodenaufruf von g() zum Ablaufzeit auch tatsächlich erfolgt. Möglicherweise ist der Kontrollfluss beim Ablauf des Programms so, dass die Methode g() nie gerufen wird. Das heißt, unter Umständen sucht man sich auf Basis der statischen Aufrufbeziehungen einen Tuning-Kandidaten heraus, der im realen Programmablauf nicht oder fast nicht aufgerufen wird – und dann ist das ganze Tuning nicht sinnvoll. Die Aufrufbeziehungen sollte man daher stets im Profiler-Tool verfolgen.

Welche Maßnahmen für das konkrete Tuning in Frage kommen, hängt ganz von den Umständen und der Semantik der Methode und der Bedeutung der erzeugten Objekte ab. Prinzipiell kann man überlegen, ob Objekte wiederverwendet werden können, statt immer wieder neue Objekte zu erzeugen Object Caching). Man kann auch in Erwägung ziehen, schwergewichtige Objekte, die ihrerseits wieder mehrere Objekte enthalten, durch leichtgewichtigere Objekte zu ersetzen. So könnten z.B. Arrays anstelle von Collections verwenden werden, weil sie deutlich kompakter sind. Wie stark sich eine solche Tuning-Maßnahme auswirkt, hängt auch von der verwendeten Plattform, der verwendeten JVM und der Konfiguration des Garbage Collectors ab.

Deshalb sollte der Effekt des Tuning durch ein erneutes Profiling nach dem Tuning verifiziert werden. Diese Kontrollmessung sollte niemals vergessen werden, denn so manche Optimierung hat sich später als Pessimierung herausgestellt. Ohne Verifikation kann man nie sicher sein, dass ein Tuning auch tatsächlich erfolgreich war.

Neben den Object-Creation-HotSpots, die typischerweise viele kurzlebige Objekte erzeugen, sind Memory Leaks ein Problem. Wenden wir uns also dem Auffinden und Beseitigen vom Memory Leaks zu.

Memory-Leaks

Als Memory-Leaks bezeichnet man Speicher, der angefordert wurde und niemals freigegeben wird, selbst dann nicht, wenn der Speicher gar nicht mehr sinnvoll genutzt wird.

In Java kümmert sich der Garbage Collector um die Freigabe des Speichers. Normalerweise wird ein Objekt angelegt, eine Zeit lang benutzt und dann irgendwann nicht mehr gebraucht. Der Speicher für ein solches Objekt sollte dann eigentlich automatisch vom Garbage Collector freigegeben werden. Den Garbage Collector interessiert aber nicht, ob Objekte noch gebraucht werden oder nicht, sondern der Garbage Collector sieht sich an, welche Objekte über Referenzvariablen erreichbar sind. Alles, was nicht erreichbar ist, ist Müll und wird weggeräumt. Alles Erreichbare bleibt erhalten und beansprucht auch nach einer Garbage Collection weiterhin Heap-Speicher.

Ob ein Objekt erreichbar ist oder ob es noch gebraucht wird, sind zwei verschiedene Dinge. Die Erreichbarkeit ist für die Garbage Collection von Bedeutung. Ob ein Objekt noch gebraucht wird, hängt von der Programmlogik ab. Meistens fällt beides zusammen: erreichbare Objekte werden noch gebraucht, unerreichbare Objekte sind Müll. Es kann jedoch vorkommen, das Objekte erreichbar sind, die von der Programmlogik her nicht mehr gebraucht werden. Das bezeichnet man als unerwünschte Referenz (engl. unwanted reference). Die Erreichbarkeit hindert dann den Garbage Collector daran, das Objekt wegzuräumen, obwohl das Objekt von der Programmlogik her nicht mehr benötigt wird.

Es gibt viele Situationen, in denen Memory Leaks auftreten können. Ein Beispiel für eine solche Situation kann sich ergeben, wenn Objekte irgendwo zentral registriert werden. Nehmen wir an, bei einem zentralen Event-Dispatcher werden Callbacks für das spätere Event-Handling angemeldet. Wenn diese registrierten Objekte nicht explizit deregistriert werden, dann bleibt am Ende in der Registrierung des Event-Dispatchers ein Verweis auf das registrierte Objekt übrig. Dieser Verweis hält das Objekt am Leben, obwohl das registrierte Objekt selbst schon lange nicht mehr gebraucht wird. Dann haben wir ein Memory-Leak, nämlich Speicher, der angefordert und nicht mehr freigegeben wurde, obwohl er eigentlich gar nicht mehr genutzt wird. Kennzeichnend für diese Art von Memory Leak ist, dass am Ende nur eine einzige Referenz (engl: single reference) auf das Objekt zeigt. Solche Single References können mit Hilfe von Tools aufgespürt werden.

Ein anderes Beispiel für ein Memory Leak ist liefert die folgende, simple Implementierung eines Stacks.

public class MyStack {
Object[] array;
int stackPointer = -1;
...
public Object pop() {
if (stackPointer >= 0)
return array[stackPointer--];
else
return null;
}
}

Die MyStack-Abstraktion legt Objekte in einem Array ab und verwaltet eine Zähler stackPointer, der den aktuellen Füllungsgrad des Arrays angibt. Wenn ein Objekt mit pop() aus dem MyStack abgeholt wird, dann wird nur der Zähler runtergezählt, so dass das abgeholte Element nicht mehr mitgezählt wird und logisch nicht mehr zum Stack gehört. Die betreffende Array-Position enthält aber immer noch eine Referenz auf das abgelieferte Objekt. Wenn nun das abgeholte Objekt ansonsten nicht mehr gebraucht wird und die betreffende Array-Position nicht mehr überschrieben wird, dann entsteht ebenfalls ein Memory Leak. Man stelle sich vor, dass 1000 Objekte mit push() auf den Stack gelegt werden und anschließend alle 1000 Objekte mit pop() abgeholt werden. Ansonsten wird das MyStack-Objekt nicht benutzt. Dann bleiben 1000 Memory Leaks übrig, weil alle 1000 Objekte noch immer vom MyStack-Objekt aus erreichbar sind, obwohl sie längst nicht mehr gebraucht werden. Erst wenn das MyStack-Objekt verschwindet, verschwinden auch die Memory Leaks. Auch solche Phänomene lassen sich mit Heap-Profiler-Tools aufspüren.

Was haben nun Memory-Leaks mit Performance zu tun? Direkt eigentlich nichts. Sie verbrauchen sinnlos Speicher, was dazu führen kann, dass der Anwendung der Speicher ausgeht und sie mit einer OutOfMemoryException abstürzt. Memory Leaks sind also in erster Linie Fehler und weniger als Performance Bottlenecks zu betrachten. Aber sie haben auch negative Performance-Effekte.

Memory Leaks belasten den Garbage Collector, ähnlich wie die Unmengen von kurzlebigen Objekte, die von Object-Creation-Hotspots erzeugt werden. Die nicht mehr benötigten, aber immer noch erreichbaren Objekte müssen bei jeder Garbage Collection genau so behandelt werden wie die echten, erreichbaren Objekte. Das heißt, sie werden markiert und kopiert und hin- und hergeschoben, bis sie irgendwann in der Old Generation des Garbage Collectors landen. Dort werden sie dann nur noch im Rahmen von vollständigen Memory-Scans angefasst. Deshalb sind die Memory-Leaks anders belastend als die Unmengen von kurzlebigen Objekte, die von Object-Creation-Hotspots erzeugt werden und mit denen der Garbage Collector bei den Memory-Scans der Young Generation fertig werden muss. Eine Belastung des Garbage Collectors und damit der Performance stellen aber beide Arten von Speicherverbrauch dar.

Der Unterschied zwischen Memory Leaks und Object Creation HotSpots besteht darin, dass Memory-Leaks in der Regel versehentlich entstehen und als Programmierfehler einzustufen sind, während die kurzlebigen Objekte in den Object-Creation-HotSpots absichtlich angelegt werden und erst im Rahmen eines Tunings reduziert werden.

Hinweise auf Memory-Leaks

Wie findet man nun ein Memory-Leak? Es gibt zwei Hinweise auf Memory-Leaks:

Programme haben in der Regel Verarbeitungszyklen. Oft fallen diese Zyklen mit den Use-Cases des Programms zusammen. Beispiele: in einem Server wird der Request eines Clients angenommen und verarbeitet; in einem GUI wird eine Benutzereingabe angenommen und verarbeitet. Es gibt auch andere, weniger offensichtliche Zyklen. In einem Textverarbeitungsprogramm ist das Suchen-und-Ersetzen von Text ein Zyklus, der mit dem Öffnen des Dialogfensters beginnt und mit dem Schließen des Resultatfensters endet. Objekte, die während des Zyklus für den Zweck der Verarbeitung in diesem Zyklus angelegt werden, sollten nach dem Zyklus wieder verschwunden sein. Objekte, die nach dem Zyklus immer noch leben, werden als Residual Objects bezeichnet.

Bei der Suche nach übrig gebliebenen Objekten muss man allerdings genau hinsehen. Nicht alle Objekte, die nach einem Zyklus übrig bleiben, sind automatisch als Memory-Leaks zu betrachten. Es gibt auch Objekte, die über den Zyklus hinaus gebraucht werden. Ein Beispiel wäre der Session-State: wenn ein Server mit einem Client eine Session durchführt, zu der mehrere Requests gehören, dann kann es sein, dass im Rahmen der Verarbeitung eines Requests (= ein Zyklus) Daten im Session-State abgelegt werden, die für die nachfolgenden Requests in der Session zur Verfügung stehen sollen. Die Objekte im Session-State werden im Zyklus erzeugt, leben aber über mehrere Zyklen hinweg. Sie wären also Residual Objects. In diesem Fall ist das Residual Object beabsichtigt und kein Fehler. Man kann es auch anders sehen: Zyklen sind hierarchisch geordnet. Die Session mit dem Client ist ein großer Zyklus, während die einzelnen Request-Verarbeitungen in der Session kleinere Sub-Zyklen sind. In diesem Modell ist der Session-State dem Ober-Zyklus „Session“ zugeordnet und sollte demgemäß nach der Session weg sein. Die kleineren Request-Verarbeitungs-Zyklen überlebt der Session-State jedoch, und das ist auch beabsichtigt.

Es gibt weitere Residual Objects, die nur schwer zu bewerten sind. Dazu gehören Objekte, die in Pools gehalten werden; siehe zum Beispiel Memory Pools in Tomcat. Pool-Objekte haben eine Lebensdauer, die an den Pool geknüpft ist, nicht aber an irgendwelche Verarbeitungszyklen der Anwendung. Pool-Objekte können durchaus in einem Verarbeitungszyklus erzeugt werden und nach dem Zyklus noch leben. Dann sind sie Residual Objects und sehen aus wie Memory Leaks, ohne dass sie Memory Leaks sind.

Wir werden im Folgenden erläutern, wie man Residual Objects aufspüren kann. Das ist aber nur der erste Schritt. Ermittelt werden lediglich Kandidaten für ein Tuning. Bei jedem der Kandidaten muss anschließend überprüft werden, ob es sich bei dem Residual Object tatsächlich um ein Memory-Leak handelt. Dazu muss man den Programmkontext analysieren und verstehen, denn eine solche Beurteilung ist nur in Kenntnis der Semantik des Programms möglich. Das gleiche gilt natürlich auch für die Objekte mit nur einer Referenz. Auch sie sind nicht automatisch Memory-Leaks. Es gibt durchaus Objekte, die tatsächlich nur von genau einer Stelle aus erreichbar sind. Man denke zum Beispiel an Objekte, die als Singleton implementiert sind.

Für das Auffinden von Residual Objects sind die Verarbeitungszyklen einer Anwendung von zentraler Bedeutung. Was macht man mit Programmen, die nicht in Zyklen ablaufen? Ein Beispiel wäre die Implementierung eines dir-Kommandos (Auflisten aller Dateien in einem Directory des Dateisystems). In der Regel ist das kein Problem, weil solche Programme so schnell ablaufen, dass Memory-Leaks keine Rolle spielen.

Wenden wir uns nun den Strategien für die Suche nach Memory-Leaks zu.

Suchstrategie – Residual Objects

Mit welchen Strategien nach Memory-Leaks gesucht wird, hängt von der Anwendung und vom verwendeten Tool ab. Bei einigen Anwendungen ergeben sich die Profiling-Zyklen ganz von selbst, bei anderen Anwendungen muss man erst einen geeigneten Testrahmen herstellen. Einige Tools unterstützen die Suche nach Residual Objects in optimaler Weise, bei anderen Tools muss man viel von Hand machen, d.h. die Zyklen selbst anstoßen, beenden und analysieren. Wir sehen uns im Folgenden die Vorgehensweise mit einem Freeware-Tool, dem Java Memory Profiler JMP, an. Das ist mühseliger als mit einigen kommerziellen Tool, zeigt aber deutlicher die einzelnen Schritte, die für das Memory-Profiling notwendig sind. Die komfortableren kommerziellen Tools gehen prinzipiell genauso vor, nur mit dem Unterschied, dass sie vieles automatisch machen und die Information besser für die anschließende Analyse aufbereiten.

Als Vorbereitung auf ein Memory-Profiling müssen als erstes die relevanten Verarbeitungszyklen der Anwendung bestimmt werden. Dann muss überlegt werden, ob ein solcher Zyklus erreichbare Objekte hinterlassen wird und welche Objekte das sind. Alle anderen während des Zyklus erzeugten Objekte sollten nach dem Zyklus verschwunden sein. Dann muß dafür gesorgt werden, dass die Zyklen gestartet werden können und dass nach jedem Zyklus angehalten wird, damit die Analyse der Profiling-Daten erfolgen kann. Je nach Art der Anwendung ergeben sich die Haltepunkte zwischen den Zyklen ganz von alleine, z.B. weil der Zyklus mit einer Benutzereingabe beginnt und mit einer Anzeige am GUI endet. Andernfalls muß ein geeigneter Testrahmen gebaut werden, um die Zyklen zu starten und um nach jedem Zyklus anzuhalten.

Es ist übrigens ratsam, nicht nach jedem einzelnen Zyklus anzuhalten, um den Heap zu inspizieren, sondern mehrere Zyklen auf einmal durchlaufen zu lassen. Auf diese Weise erhält man aussagekräftigere Daten. Das liegt zum einen daran, dass der Garbage Collector nicht bei jeder Garbage Collection sämtliche unerreichbaren Objekte wegräumt. Er versucht es zwar, aber er schafft es nicht immer. Mancher Müll bleibt einfach für die nächste Garbage Collection liegen. Das heißt, nach einer Garbage Collection findet man u.U. Objekte auf dem Heap, die eigentlich schon Müll sind, und die man für Residual Objects halten könnte. Über mehrere Zyklen hinweg gleicht sich dieser Effekt jedoch aus.

Es gibt andererseits Situationen, bei denen nach einem Zyklus ein Object übrig bleibt, und sogar erreichbar bleibt, und dennoch handelt es sich nicht um ein Memory Leak. Das kann vorkommen, wenn einer langlebigen Referenzvariablen am Anfang eines jedes Zyklus ein Objekt zugewiesen wird. Dann ist das Objekt auch am Ende des Zyklus noch erreichbar und sieht aus wie ein Memory Leak. Aber im nächsten Zyklus wird die Referenzvariable neu belegt und das vermeintliche Memory Leak wird unerreichbar und verschwindet. Das heißt, so eine „falsches“ Memory Leak verschwindet erst im jeweils nächsten Zyklus. Wenn man nur einen Zyklus durchläuft, bleibt ein „falsches“ Memory Leak übrig. Wenn man 100 Zyklen durchläuft, bleibt trotzdem nur ein Objekt übrig, nämlich das aus dem letzten Zyklus. Bei einem richtigen Memory Leak würden bei 100 Zyklen tatsächlich 100 Residual Objects übrig bleiben. Um diese beiden Situationen auseinanderhalten zu können, empfiehlt es sich, immer mehrere Zyklen auf einmal zu durchlaufen, ehe der Heap untersucht wird.

Nach diesen Vorbereitungen lassen wir die Anwendung zusammen mit dem Profiler unserer Wahl laufen. Wir verwenden für die Demonstration den Java Memory Profiler JMP. Das ist ein JVMPI-basiertes Freeware-Tool, das fortlaufend die erhobenen Profiling-Daten anzeigt. Post-Mortem-Profiler wie HPROF können prinzipiell auch verwendet werden, aber die Analyse ist damit noch mühseliger.

Interaktive Memory-Profiler zeigen in der Regel die Aktivitäten des Garbage Collectors an, zum Beispiel wie sich die Heapgröße bei den einzelnen Collections verändert. Es gibt immer auch eine Statistik, die alle Objekte auf dem Heap auflistet, meistens zusammen mit deren Typ und der Anzahl der Objekte von diesem Typ. In dieser Statistik tauchen nicht nur die erreichbaren Objekte auf, sondern alle Objekte auf dem Heap. Dazu gibt es Navigationsmöglichkeiten, die es erlauben, die Methoden und Klassen zu finden, in denen diese Objekte erzeugt wurden. Die Abbildungen 2 und 3 zeigen die entsprechenden Fenster des JMP.


Abbildung 3: Main-Window von JMP


Abbildung 4: Memory Window von JMP

Auch die Referenzbeziehungen zwischen den Objekten auf dem Heap können angezeigt werden, damit man verfolgen kann, von wem ein Objekt referenziert wird und welche Objekte es seinerseits referenziert.

Alle Memory-Profiler bieten die Möglichkeit, den Garbage Collector von Hand zu starten und die Anzeige und Statistiken des Profilers zurückzusetzen. Beide Funktionen werden für die Durchführung der Messzyklen gebraucht.

Prinzipiell geht man so vor, dass die Anwendung gestartet wird und zum Aufwärmen ein paar Zyklen durchlaufen werden. Erst dann geht die eigentliche Messung los. Man ruft vor der Messung den Garbage Collector auf. Dieser räumt alle nicht mehr erreichbaren Objekte weg oder setzt sich zumindest eine Markierung, die bewirkt, dass in den Statistiken nur noch die echt lebendigen Objekte auftauchen. Danach setzt man die Anzeige zurück. Das kann so aussehen, dass der Profiler alle seine Zähler auf Null setzt. Es kann aber auch so aussehen, dass der Profiler ein sichtbare Marke in der grafischen Darstellung setzt. Dann wird die vorgesehene Anzahl von Zyklen angestoßen. Dabei werden neue Objekte erzeugt und die Anzeige und die Statistiken im Profiler-Tool zeigen genau diese neu erzeugten Objekte an. Nach dem Durchlauf der Zyklen wird erneut der Garbage Collector aufgerufen. In den Statistiken tauchen dann nur noch die Objekte auf, die nach dem letzen Zurücksetzen, also während des Messzyklus, erzeugt wurden und den Zyklus überlebt haben. Das sind die Residual Objects.

Abbildung 5 zeigt den typischen Ablauf.


Abbildung 5: Ablauf eines Messzyklus beim Memory-Profiling

Die Tools variieren im Detail. OptimizeIt zum Beispiel unterstützt die Messzyklen, indem es den Garbage Collector vor und nach dem Zyklus automatisch anstößt und die Differenzmenge der Objekte von sich aus ermittelt und anzeigt. Abbildung 6 zeigt die grafische Darstellung in OptimizeIt.


Abbildung 6: Differenzmenge nach einem Meßzyklus (mn Borland OptizeIt Profiler)

Wenn die Residual Objects entstanden sind, benutzt man die Navigationsmöglichkeiten des Profiler-Tools, um herauszufinden, wo die Objekte erzeugt wurden, wer sie verwendet, um entscheiden zu können, ob es sich tatsächlich um Memory Leaks handelt. Die Identifikation der Allocation Sites und Object Owners ist je nach Güte des Tools mehr oder weniger gut unterstützt. Kommerzielle Tools bieten in der Regel graphische Darstellungen, die eine rasche Orientierung erlauben.

Suchstrategie – Single References

Single References zu finden ist deutlich einfacher. Man kann sie auch mit Post-Mortem-Profilern wie HPROF finden. HPROF hat dafür eine Extra-Funktion (die über Guess => Memory Leaks zur Verfügung steht). Dort werden alle Objekte angezeigt, die bei Programmende noch über genau eine, möglicherweise unerwünschte, Referenz erreichbar waren. Auch hier muss dann navigiert werden, um Allocation Site und Object Owner zu finden und zu entscheiden, ob es sich um ein Memory Leak handelt oder nicht.

Tuning

Das Tuning nach einem solchen Memory-Profiling ist relativ einfach. Da es sich bei Memory Leaks um Fehler handelt, wird man den Fehler beseitigen.

Im Falle von Single References kann man Weak References statt der normalen Referenzen verwenden. Eine Weak Reference hält das Objekt, auf das sie verweist, nicht am Leben, so dass der Garbage Collector es wegräumt, wenn die einzige Referenz auf das Objekt eine Weak Reference ist. Das wäre ein angemessene Lösung für unser Event-Dispatcher-Beispiel. Dort wurden die Callbacks beim Event-Dispatcher registriert. Wenn der Event-Dispatcher die registrierten Callbacks nicht über normale Referenzen referenziert, sondern über Weak References, dann würden am Ende nur noch die Weak References der Event-Dispatcher-Registrierung auf die Callbacks zeigen. Das wären zwar immer noch unerwünschte Referenzen, aber sie sind zu schwach, um die registrierten Objekte am Leben zu erhalten.

Bei den Residual Objects wird man dafür sorgen, dass sie am Ende des Zyklus unerreichbar werden. Wie das zu geschehen hat, hängt vom Kontext ab. Wenn das Residual Object deshalb übrig geblieben ist, weil es z.B. in eine Collection eingetragen aber nicht wieder entfernt wurde, dann wird man es entfernen. Betrachten wir zum Beispiel unsere Stack-Implementierung von vorhin. Das Problem war, dass nach einem pop() im Array der Stack-Abstraktion Referenzen stehen blieben, obwohl die referenzierten Objekte logisch schon nicht mehr zum Stack gehörten. Solche Memory Leak kann man vermeiden, indem man die Referenzbeziehung explizit aufgibt, beispielsweise indem die Referenzvariable auf null gesetzt wird. Hier die korrigierte Fassung der pop-Methode:

public Object pop() {
Object tmp = null;

if (stackPointer >= 0) {
tmp = array[stackPointer];
arrray[stackpointer--] = null;
}

return tmp;
}

Zusammenfassung

In diesem Artikel haben wir erläutert, dass der Umgang eines Programms mit Speicher ebenfalls Auswirkungen auf die Performance des Programms hat, weil das Erzeugen, Initialisieren, und Wegräumen Performance kostet. Deshalb wird im Rahmen eines Performance-Profilings auch der Speicherverbrauch eines Programms untersucht. Dabei wird einerseits nach Object-Creation-HotSpots gesucht. Das sind Methoden, die auffallend viel Speicher anfordern. Solche Methoden wird man einem Tuning unterziehen, bei dem entweder der Speicherverbrauch reduziert wird. Daneben wird nach Memory Leaks gesucht. Das sind Objekte, die eigentlich längst weggeräumt sein sollten, weil sie nicht mehr gebraucht werden, aber immer noch existieren, weil sie wegen eines Programmfehlers noch erreichbar sind. Dabei unterscheidet man Objekte, die über eine einzige, unerwünschte Referenz bis zum Programmende erreichbar bleiben, und Residual Objects, die nach einem Verarbeitungszyklus eigentlich verschwunden sein sollten, aber nach dem Zyklus immer noch erreichbar sind. Wir haben die Strategien erläutert, mit denen diese Memory Leaks aufgespürt werden können. Das erfordert insbesondere bei den Residual Objects die Ausführung des Programms in Zyklen, nach denen das Programm angehalten und der Heap untersucht werden kann.

Literaturverweise und weitere Informationsquellen

Die gesamte Serie über Java Performance:

/KRE1/ Java Performance, Teil 1: Was ist ein Micro-Benchmark? Klaus Kreft & Angelika LangerJava Spektrum, Juli 2005URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/21.MicroBenchmarking/21.MicroBenchmarking.html
/KRE2/ Java Performance, Teil 2: Wie wirkt sich die HotSpot-Technologie aufs Micro-Benchmarking aus? Klaus Kreft & Angelika LangerJava Spektrum, September 2005URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/22.JITCompilation/22.JITCompilation.html
/KRE3/ Java Performance, Teil 3: Wie funktionieren Profiler-Tools? Klaus Kreft & Angelika LangerJava Spektrum, November 2005URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/23.ProfilingTools/23.ProfilingTools.html
/KRE4/ Java Performance, Teil 4: Performance Hotspots - Wie findet man funktionale Performance Hotspots? Klaus Kreft & Angelika LangerJava Spektrum, Januar 2006URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/24.FunctionalHotSpots/24.FunctionalHotSpots.html
/KRE5/ Java Performance, Teil 5: Performance Hotspots - Wie findet man Memory Hotspots? Klaus Kreft & Angelika LangerJava Spektrum, März 2006URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/25.MemoryHotSpots/25.MemoryHotSpots.html
/KRE6/ Java Performance, Teil 6: Garbage Collection - Wie funktioniert Garbage Collection? Klaus Kreft & Angelika LangerJava Spektrum, Mai/Juli 2006URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/26.GarbageCollection/26.GarbageCollection.html
/KRE7/ Java Performance, Teil 7: Garbage Collection - Das Tunen des Garbage Collectors Klaus Kreft & Angelika LangerJava Spektrum, September 2006URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/27.GCTuning.html/27.GCTuning.html
If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar High-Performance Java - programming, monitoring, profiling, and tuning techniques 4 day seminar (open enrollment and on-site)