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 einem Computerprogramm, der dazu führt, dass ein laufender Prozess einen Speicherbereich zwar belegt, diesen jedoch weder freigeben noch nutzen kann.

Problematik[Bearbeiten]

Da unbeschränktes Wachstum der exklusiven Nutzung einer beschränkten Ressource unmöglich ist, stellt ein Speicherleck ein Problem der Software-Entwicklung dar. Dies kann in Multitasking-Umgebungen auch andere Prozesse beeinträchtigen, die selbst kein Speicherleck aufweisen. In jedem Fall wird aber bei hinreichend langer Laufzeit das Speicherleck zu unerwünschten Erscheinungen führen. Mit dem Ende des Prozesses wird in zeitgemäßen Betriebssystemen auch sein Speicheranspruch aufgegeben.

Die Frage, ob ein Programm ein Speicherleck aufweist, ist wegen des Satzes von Rice nicht entscheidbar.

Lösungsmöglichkeiten[Bearbeiten]

Das Betriebssystem ist nur sehr eingeschränkt in der Lage, Abhilfe zu schaffen, da es üblicherweise in die internen Datenstrukturen der Prozesse keine hinreichend tiefe Einsicht haben kann. Einfach zu implementieren ist die Beschränkung des Speicheranspruchs auf einen Wert, der den Ansprüchen des jeweiligen Prozesses genügen sollte, so dass im Falle eines Speicherlecks wenigstens keine anderen Prozesse beeinträchtigt werden. Es sind auch andere Ansätze denkbar, bei denen durch Heuristiken entschieden wird, welcher der Prozesse derjenige ist, der ein Speicherleck haben könnte, um eben den dann zu beenden.

Bei den möglichen Ansätzen zur Speicherverwaltung kann grundsätzlich zwischen zwei Alternativen unterschieden werden:

  1. Automatische Speicherbereinigung
  2. Explizite Speicherfreigabe

Es gibt folgende Ansätze zur Vermeidung von Speicherlecks:

  1. Analyse des Graphen aus Speicherbereichen (Knoten) und Referenzen (Kanten)
  2. Analyse des Quell-Codes ähnlich der formalen Verifikation
  3. Analyse konkreter Laufzeit-Situationen im Rahmen eines Software-Tests

Im Folgenden sollen diese Alternativen kurz betrachtet werden.

Automatische Speicherbereinigung[Bearbeiten]

Falls die verwendete Laufzeitumgebung eine automatische Speicherbereinigung bereitstellt, kann von einem Prozess Speicher belegt und verwendet werden. Sobald die Laufzeitumgebung feststellt, dass ein belegter Speicherbereich nicht mehr erreichbar ist, wird dieser wieder freigegeben. Nicht mehr erreichbar heißt in diesem Zusammenhang, dass keine gültige Variablenreferenz auf den belegten Speicherbereich mehr existiert. Im allgemeinen Fall ist dieser Ansatz jedoch ungenügend, da fälschlich gespeicherte Referenzen nicht erkannt werden können.

Explizite Speicherfreigabe[Bearbeiten]

Im Gegensatz zur automatischen Speicherbereinigung kann der Anwendungsentwickler in anderen Programmiersprachen (C, C++ oder Pascal) die Speicherverwaltung selbst implementieren. Das heißt, es können dynamisch Speicherbereiche angefordert, verwendet und anschließend wieder freigegeben werden. Die Speicherbelegung erfolgt explizit an vom Programmierer vorgesehenen Stellen und ist nicht an die Existenz einer Variablenreferenz gekoppelt, was zusätzlich zu den Schwierigkeiten der automatischen Speicherbereinigung Fehlerquellen schafft.

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 ist Valgrind. Es untersucht (in Linux-Systemen) die Speicherbenutzung eines Prozesses zur Laufzeit. In wenigen offensichtlichen Fällen kann es ausreichen, nur den Speicherverbrauch eines Prozesses im zeitlichen Verlauf zu beobachten.

Formale Verifikation[Bearbeiten]

Durch einen Korrektheitsbeweis können insbesondere auch Speicherlecks entdeckt werden. Dieses Verfahren ist jedoch sehr zeitaufwendig und benötigt hochqualifiziertes Personal. 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 implementierten Programms:

 #include <stdlib.h>
 
 int main(void)
 {
     int *a, *b; /* Zeiger auf einen als Ganzzahl interpretierten Speicherbereich */
 
     /* Speicher für Zeiger reservieren. */
     a = malloc(sizeof(int));
     b = malloc(sizeof(int));
 
     *a = 5;     /* Schreibt den Wert „5“ in den von a referenzierten Speicherbereich */
     *b = 3;     /* Schreibt den Wert „3“ in den von b referenzierten Speicherbereich */
      a = b;     /* Weist dem Zeiger a das Ziel des Zeigers b zu. */
 
     /* a und b verweisen nun auf den gleichen Speicherbereich. Der zuvor von a referenzierte
      * Speicher ist damit verwaist und kann nicht mehr freigegeben werden. An dieser Stelle
      * entsteht ein Speicherleck.
      */
 
     free(b);    /* Gibt den von b referenzierten Speicher frei */
     free(a);    /* a und b zeigen auf den gleichen Speicherbereich, der jedoch bereits
                  * in der vorangegangenen Zeile freigegeben wurde.
                  * Das Verhalten ist dann nicht mehr definiert.
                  */
 
     return EXIT_SUCCESS;
 }

Ab dem Schritt a = b 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.

 class Object
 {
   int* m_value;
 
 public:
   Object()
   {
      m_value = new int;
      *m_value = 42;
   }
 
   virtual ~Object()
   {
      *m_value = 0;
      delete m_value;
      m_value = NULL;
   }
 };

Richtig: Speicher für 5 Objekte vom Typ int angefordert. Der Speicher wird komplett freigegeben

 int * pi = new int[5];
 delete [] pi;

Falsch: Nur der Destruktor des Objektes po[0] wird aufgerufen. Speicherleck, da Destruktor für po[1] bis po[4] nicht aufgerufen wird.

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

Richtig: Die Destruktoren aller fünf Objekte werden aufgerufen. Der gesamte allozierte Speicher wird freigegeben.

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

Automatische Speicherbereinigung[Bearbeiten]

Das folgende Beispiel 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 sieht hier deutlich, dass der Speicherbedarf ständig anwächst. Unter der Annahme, dass der zunehmende Speicherbedarf selbst nicht Zweck des Programms ist, handelt es sich also um ein Speicherleck, da gar kein lesender Zugriff auf die Listen-Einträge erfolgt. Dieser Fehler wäre recht leicht durch statische Analyse zu erkennen, während aber der Graph aus Referenzen und Speicherbereichen den Fehler nicht erkennen lässt.

Einzelnachweise[Bearbeiten]


Weblinks[Bearbeiten]