Speicherleck

aus Wikipedia, der freien Enzyklopädie
Wechseln zu: Navigation, Suche

Speicherleck (englisch memory leak, gelegentlich auch Speicherloch oder kurz memleak) bezeichnet einen Fehler in der Speicherverwaltung eines Computerprogramms, der dazu führt, dass es einen Teil des Arbeitsspeichers zwar belegt, diesen jedoch weder freigibt noch nutzt.

Problematik[Bearbeiten]

Arbeitsspeicher ist ein nur in endlicher Menge verfügbares Betriebsmittel, das einem Programm auch nicht ohne weiteres wieder entzogen werden darf. Wenn ein Programm durch Speicherlecks zunehmend mehr Arbeitsspeicher belegt, kann es dazu kommen, dass die Leistungsfähigkeit des Computers sinkt, da Teile des Arbeitsspeichers ausgelagert werden müssen. Das kann auch zum sog. Thrashing führen, wobei ständig Seiten in den Hintergrundspeicher aus und wieder eingelagert werden. Wenn die Menge an Speicher, die ein Prozessen belegen kann beschränkt wurde, ist es dem Programm irgendwann nicht mehr möglich, weiter Speicher zu allokieren und es kommt zu einem Programmfehler. Im schlimmsten Fall kann die Überbelegung des Speichers dazu führen, dass auch Teile des Betriebssystems nicht mehr korrekt ausgeführt werden können und das System abstürzt.

Lösungsmöglichkeiten[Bearbeiten]

Der Speicher, der höchstens von einem Prozess belegt werden darf, kann durch das Betriebssystem beschränkt werden. Das behebt den ursprünglichen Fehler nicht, aber begrenzt die Auswirkungen auf andere Prozesse.

Unter Linux existiert eine Kernelfunktion, der OOM-Killer (engl. out of memory killer), die zum Einsatz kommt, wenn alle Versuche Speicher zu allokieren fehlschugen. Sie wählt unter den laufenden Prozessen u. a. denjenigen mit dem höchsten Speicherbedarf, der kürzesten Laufzeit und der niedrigsten Priorität und beendet diesen zwangsweise. Nach Beendigung des Prozesses wird aller von ihm belegter Arbeitsspeicher wieder freigegeben.[1]

Um Speicherlecks aufzuspüren existieren mehrere Möglichkeiten:

  1. Analyse der Referenzen auf Speicherbereiche (z. B. Smart Pointer).
  2. Analyse des Quelltext, ähnlich der formalen Verifikation.
  3. Analyse konkreter Laufzeit-Situationen im Rahmen eines Software-Tests.

Automatische Speicherbereinigung[Bearbeiten]

Falls die verwendete Laufzeitumgebung eine automatische Speicherbereinigung bereitstellt, versucht diese zu ermitteln, auf welche Speicherbereiche ein Prozess nicht mehr zugreift. Stellt die Laufzeitumgebung fest, dass ein belegter Speicherbereich für das Programm nicht mehr erreichbar ist, wird dieser wieder freigegeben. Nicht mehr erreichbar heißt in diesem Zusammenhang, dass keine gültige Referenz auf den belegten Speicherbereich mehr existiert. Speicherlecks können dabei dennoch entstehen, wenn der Speicher für das Programm noch erreichbar ist, d. h. es eine Referenz auf den Speicher hält, ihn aber dennoch nicht mehr verwendet.

Explizite Speicherfreigabe[Bearbeiten]

Im Gegensatz zur automatischen Speicherbereinigung muss der Anwendungsentwickler bei einer manuellen Speicherverwaltung explizit dynamisch Speicherbereiche wieder freigeben. Versäumt er dies bspw. am Ende einer Funktion, wird anschließend die Referenz auf den noch reservierten Speicher gelöscht und der Bereich kann nicht mehr freigegeben werden.

Systematische Tests[Bearbeiten]

Durch systematisches Testen mit Hilfe entsprechender Werkzeuge, die mit einer gewissen Sicherheit feststellen können, welche Speicherbereiche einem Speicherleck zuzuordnen sind, kann man den Problemen der formalen Verifikation entgehen.

Ein bekanntes solches Werkzeug (auf Linux-Systemen) ist memcheck von Valgrind. Es führt eine Liste der allokierten Speicherbereiche mit und an welcher Stelle im Programm die Allokation stattfand. Bei Programmende überprüft es, ob alle allokierte Bereiche auch wieder freigegeben wurden. In sehr einfachen oder sehr gravierenden Fällen kann es ausreichen, nur den Speicherverbrauch eines Prozesses im zeitlichen Verlauf zu beobachten.

RAII[Bearbeiten]

Eine Programmiertechnik die Speicherlecks verhindern kann ist RAII (Ressourcenbelegung ist Initialisierung). Hierbei ist wird der Speicherbereich einer Variablen bei ihrer Initialisierung reserviert und beim Verlassen ihres Gültigkeitsbereichs automatisch wieder freigegeben. Das hat gegenüber der automatischen Speicherverwaltung den Vorteil, dass die „Lebensdauer“ eines Objekts i. d. R. genau bekannt ist.

Formale Verifikation[Bearbeiten]

Durch einen Korrektheitsbeweis können insbesondere auch Speicherlecks entdeckt werden. Dieses Verfahren ist jedoch sehr zeitaufwendig und benötigt Expertenwissen. Die auf Speicherlecks spezialisierte Frage kann auch computergestützt untersucht werden.

Beispiele[Bearbeiten]

Das bekannteste Beispiel für fehlende automatische Speicherverwaltung ist die Sprache C. Neuer Speicher wird in C-Programmen durch die Funktion malloc angefordert. Dabei liefert malloc einen Zeiger auf den Anfang des entsprechenden Speicherbereichs. Dieser Verweis ist notwendig, um den Speicher mittels geeignetem Code wieder freizugeben (mittels der Funktion free). Geht der Zeiger verloren oder wird verändert, kann der Prozess nicht mehr auf diesen Speicher zugreifen und ihn damit auch nicht freigeben.

Beispiel für Personen ohne Programmierkenntnisse[Bearbeiten]

Dieses Beispiel soll Personen ohne Programmierkenntnisse zeigen, wie ein Speicherleck entstehen kann. Es ist ein frei erdachtes Beispiel.

Das Beispiel zeigt einen Ausschnitt eines Programms in Pseudocode zur Steuerung eines Aufzugs. Dieser Teil des Aufzugprogramms wird immer ausgeführt, wenn jemand im Aufzug einen Knopf zum Wählen einer Etage drückt.

Wenn ein Knopf gedrückt wird:
 Reserviere Speicher um die gewünschte Etagennummer zu speichern
 Schreibe die Etagennummer in den reservierten Speicher
 Befindet sich die Kabine bereits auf der gewünschten Etage?
 Wenn ja, gibt es nichts zu tun: Unterprogramm beenden
 Ansonsten:
   Warten bis der Aufzug ohne weitere Aufgaben ist
   Zu der gewünschten Etage fahren
   Den Speicher freigeben, in dem die gewünschte Etagennummer abgelegt wurde

Dieses Programm hat ein Speicherleck. Wenn eine Etage gewählt wurde und der Lift sich schon auf dieser Etage befindet, wird der Speicher, der in der zweiten Programmzeile reserviert wurde, niemals wieder freigegeben.

Dies hat keinen sofortigen Effekt. Liftbenutzer drücken selten den Knopf der Etage, auf der sie sich gerade befinden, und der Lift könnte so viel freien Speicher besitzen, dass dies hunderte oder tausende von Malen ohne Probleme funktionieren würde. Jedoch könnte der Aufzug irgendwann seinen kompletten Speicher aufgebraucht haben. Dies könnte Monate oder Jahre dauern und ist deshalb auch sehr schwer durch Tests herauszufinden.

Die Konsequenzen in diesem Fall wären unangenehm. Der Lift könnte aufhören auf Benutzereingaben zu reagieren und nicht mehr weiterfahren. Falls das Programm auch Speicher benötigt, um die Türen zu öffnen, könnte jemand durch den Fehler im Lift eingesperrt werden, da kein Speicher mehr verfügbar wäre, um die Türen zu öffnen.

Das Speicherleck würde nur so lange existieren, wie das Programm läuft. Würde der Lift abgeschaltet werden, würde das Programm stoppen. Nach Reaktivierung würde das Programm neu starten, der Speicher wäre wieder freigegeben und der langsame Prozess des Speicherleckens würde erneut beginnen.

C[Bearbeiten]

Das folgende Beispiel zeigt die Entstehung eines Speicherlecks anhand eines in C-Programms:

#include <stdlib.h>
 
int main(void)
{
   int *a; /* Zeiger auf einen als Ganzzahl interpretierten Speicherbereich */
 
   /* Speicher für Zeiger reservieren. */
   a = malloc(sizeof(int));
   /* ... */
   a = malloc(sizeof(int));   /* Zeiger auf zuvor allozierten Speicher wird überschrieben. */
 
   free(a);  /* Nur der zweite Speicherblock wird freigegeben --> Speicherleck */
 
   return EXIT_SUCCESS;
}

Ab dem zweiten a = malloc() ist es nicht mehr möglich, auf den Speicherbereich zuzugreifen, auf den a zuvor verwies. Der Bereich kann dann regulär nicht mehr vom Programm freigegeben werden.

C++[Bearbeiten]

Speicherlecks unter C++ entstehen genau so wie unter C. Eine weitere Fehlerquelle, die unter C nicht auftritt, ist die Verwendung des falschen delete-Operators. Wird ein Array von Objekten angefordert, muss dieses Array mit dem delete[]-Operator freigegeben werden. Der normale delete-Operator bewirkt, dass nur der Destruktor des ersten Objektes aufgerufen, aber der Speicher des gesamten Arrays freigegeben wird. Für alle anderen Objekte wird der Destruktor nicht aufgerufen. Wird im Destruktor dynamisch allozierter Speicher freigegeben (wie im Beispiel) treten Speicherlecks auf. Nur der delete[]-Operator ruft für alle Objekte des Arrays den Destruktor auf und gibt danach den Speicher des Arrays frei.

Falsch: Nur der Destruktor des Objektes po[0] wird aufgerufen. Es entsteht ein Speicherleck, falls die Objekte po[1], …, po[4] in ihren Destruktoren noch Speicher freigegeben müssen.

 Object * o = new Object[5];
 delete po;

Richtig: Alle fünf Objekte werden freigegeben und zuvor ihre Destruktoren aufgerufen.

 Object * o = new Object[5];
 delete[] po;

Automatische Speicherbereinigung[Bearbeiten]

Das folgende Beispiel in Java zeigt, dass alleine der Ansatz der automatischen Speicherbereinigung nicht reicht, um Speicherlecks aufzudecken:

public void erzeugeSpeicherleck() {
  List<Integer> nummern = new ArrayList<>();
  for (int i=1; i<10000; i++)
    nummern.add(i);
}

Man erkennt hier, dass der Speicherbedarf ständig anwächst, es handelt sich also um ein Speicherleck, da kein lesender Zugriff mehr auf die Listen-Einträge erfolgt. Dieser Fehler wäre recht leicht durch statische Analyse zu erkennen, wohingegen der Graph aus Referenzen und Speicherbereichen den Fehler nicht erkennen lässt.

Einzelnachweise[Bearbeiten]

  1. http://linux-mm.org/OOM_Killer

Weblinks[Bearbeiten]