Wyciek pamięci

Wyciek pamięci (ang. memory leak) – szczególny rodzaj niezamierzonego użycia pamięci przez program komputerowy, gdy nie zwalnia on zaalokowanej wcześniej pamięci, która nie jest już mu potrzebna, a może nawet rezerwować nową.

Wycieki pamięci są efektem bardzo niepożądanym. Program bowiem zajmuje coraz więcej pamięci, ale nie jest w stanie jej wykorzystać ani zwolnić. Szczególnie w aplikacjach, które działają przez długi czas (w większości serwerowych), efekt wycieku pamięci stopniowo narasta. Sam wyciek prowadzi do spadku wydajności systemu, w skrajnym przypadku zawieszenia się programu lub innych programów, którym system nie może przydzielić wystarczającej ilości pamięci, a nawet zablokowania całego systemu operacyjnego. Doprowadzenie w wadliwym programie do możliwie dużego wycieku pamięci może być jednym ze sposobów wykonania ataku DoS.

Kod programu, który powoduje wycieki pamięci, jest kodem błędnym.

Przykłady wycieków pamięci

C++

int fun() {
  int* wsk = new int; // zarezerwowane pamięci na liczbę typu int
  *wsk = 20; // wpisanie w zarezerwowane miejsce wartości
  return *wsk;
}

W powyższym przykładzie wykonanie funkcji fun spowoduje rezerwację obszaru pamięci na jedną liczbę typu int i zapisanie w nim wartości 20. Wartość ta jest zwracana przez funkcję, jednak obszar pamięci o adresie wskazywanym przez wskaźnik wsk nie zostaje zwolniony (brak wywołania instrukcji delete). Oznacza to wyciek pamięci – każde kolejne wywołanie funkcji fun powiększy rozmiar tego wycieku co najmniej o rozmiar typu danych int.

Java, C#

W takich językach programowania jak Java czy C# problem wycieków pamięci został częściowo zniwelowany poprzez zastosowanie odśmiecania pamięci lub inaczej śmieciarza (ang. Garbage Collector). Garbage Collector nie rozwiązuje jednak całkowicie problemu z wyciekami pamięci, ponieważ zwalnia tylko pamięć, do której nie istnieją odwołania. Powoduje to, że niestarannie napisany kod może skutkować, iż śmieciarz nie zwolni pewnego obszaru pamięci, mimo że nie zamierzamy go już używać.

JavaScript

Pomimo tego, że JavaScript jest językiem wysokopoziomowym i nie udostępnia metod alokacji i zwalniania pamięci, możliwe jest spowodowanie wycieku pamięci wskutek niedoskonałości działań interpretera bądź środowiska uruchomieniowego. Przykładowo, w przeglądarce Internet Explorer, działają równolegle dwa niezależne systemy odśmiecania pamięci – jeden w warstwie DOM, zaś drugi w przestrzeni JScript. W konsekwencji utworzenie referencji cyklicznej pomiędzy takimi elementami (np. poprzez przypisanie metod obsługi zdarzeń) może skutkować wyciekiem pamięci.

Obsługa wyjątków a wycieki pamięci

Częstą sytuacją podczas działania programu jest alokowanie pewnego obszaru pamięci, pracy z nim, a następnie zwolnienie go. W C++ taka funkcja mogłaby mieć postać (pominięto definicje klas i funkcji – nie są potrzebne w przykładzie):

 class KlasaZasobu {
   // definicja klasy
 };

 void pracaZzasobem( KlasaZasobu* dana ) {
 //   implementacja funkcji
 }

 void funkcja() {
   KlasaZasobu* zasob = new KlasaZasobu(); // 1
   pracaZzasobem( zasob );                 // 2
   delete zasob;                           // 3
 }

W (1) następuje alokowanie pamięci poprzez stworzenie obiektu KlasaZasobu. Następnie zostaje wywołana pewna funkcja pracaZzasobem( KlasaZasobu* ) (2) pracująca z obiektem klasy KlasaZasobu. Po tym wszystkim nastąpi zwolnienie zarezerwowanego obszaru pamięci w (3). Na pierwszy rzut oka, funkcja ta nie powoduje wycieków pamięci. Wyobraźmy sobie sytuację, w której po alokowaniu pamięci w (1), funkcja pracaZzasobem( KlasaZasobu* ) wywoła wyjątek (np. z powodu błędnych danych). Zostaną wtedy usunięte wszystkie obiekty lokalne i wykonanie programu zostanie przekazane do miejsca, gdzie została wywołana funkcja funkcja() – zgodnie z działaniem mechanizmu wyjątków. Lokalnym obiektem w tym przypadku jest wskaźnik zasob i zostanie on usunięty, jednak zarezerwowany obszar pamięci, do którego ma dostęp, już nie. Z powodu wywołania wyjątku przez funkcję pracaZzasobem( KlasaZasobu* ), zasób nie zostanie zwolniony w (3). W efekcie powstanie wyciek pamięci.

Aby zaradzić takiej sytuacji i obronić się przed wyciekiem pamięci stosuje się dodatkową klasę, w konstruktorze której przypisuje się alokowany wskaźnik, a w destruktorze zwalnia się alokowany obszar pamięci:

 class KlasaZasobu {
   // definicja klasy
 };

 void pracaZzasobem( KlasaZasobu* dana ) {
 //   implementacja funkcji
 }

 class KlasaPomocnicza {
   public:
     KlasaPomocnicza( KlasaZasobu* wskaznik ) {
       this->dana = wskaznik;
     }
     KlasaZasobu* pobierzWskaznik() {
       return this->dana;
     }
     ~KlasaPomocnicza() {
       delete this->dana;
     }
  private:
    KlasaZasobu* dana;
 };

 void funkcja() {
   KlasaPomocnicza zasob( new KlasaZasobu() ); // 1
   pracaZzasobem( zasob.pobierzWskaznik() );   // 2
 }

W (1) następuje stworzenie lokalnego obiektu klasy KlasaPomocnicza i podanie w konstruktorze wskaźnika do nowo alokowanego obszaru pamięci. Konstruktor ten przypisuje polu dana wskaźnik nowo alokowanego obszaru. Następnie zostaje wywołana funkcja pracaZzasobem( KlasaZasobu* ), której jako parametr podajemy wskaźnik, korzystając z funkcji w klasie KlasaPomocnicza. Gdy funkcja pracaZzasobem( KlasaZasobu* ) wywoła wyjątek, zostaną usunięte wszystkie obiekty lokalne. W tym wypadku obiektem lokalnym jest obiekt zasob, dla którego zostanie wywołany jego destruktor, w którym nastąpi zwolnienie alokowanego zasobu. Funkcja nie będzie mieć również wycieku pamięci w momencie, gdy funkcja pracaZzasobem( KlasaZasobu* ) nie wywoła wyjątku, ponieważ skończy się zasięg ważności obiektu zasob i zostanie on także usunięty (a wraz z nim zwolniony zostanie alokowany obszar pamięci).

Programista nie musi pisać takiej klasy pomocniczej dla klasy, której obiekt alokuje używając operatora new. Z pomocą przychodzą szablony klas. W standardowej bibliotece klas jest to szablon auto ptr, ale dostępne są również inne implementacje. Najpopularniejszą są szablony z biblioteki Boost: scoped_ptr (dla prostego zwalniania obiektów w danym zasięgu ważności) oraz shared_ptr (dla automatycznego zwalniania współdzielonych zasobów poza zasięgiem ważności).

Problem wycieków pamięci w związku z obsługą wyjątków dotyczy głównie C++. W Delphi wprowadzono specjalną konstrukcję try...finally...except, która pozwala na kontrolowanie zwalniania zasobów w momencie wystąpienia wyjątku. W większości innych języków problem ten nie występuje, ze względu na brak mechanizmu wyjątków lub inny sposób zarządzania pamięcią np. Garbage collection.

Zobacz też