Wyciek pamięci - Memory leak

W informatyce , o przeciek pamięci jest typem wycieku zasobów , które pojawia się, gdy program komputerowy niewłaściwie zarządza alokacji pamięci w taki sposób, że pamięć, która nie jest już potrzebne nie zostaną ujawnione. Przeciek pamięci może również wystąpić, gdy obiekt jest przechowywany w pamięci, ale nie można uzyskać do niego dostępu przez uruchomiony kod. Wyciek pamięci ma objawy podobne do wielu innych problemów i generalnie może być zdiagnozowany tylko przez programistę z dostępem do kodu źródłowego programu.

Wyciek miejsca występuje, gdy program komputerowy zużywa więcej pamięci niż to konieczne. W przeciwieństwie do przecieków pamięci, w których pamięć z przeciekiem nigdy nie jest zwalniana, pamięć zużywana przez przeciek przestrzeni jest zwalniana, ale później niż oczekiwano.

Ponieważ mogą one wyczerpać dostępną pamięć systemową podczas działania aplikacji, przecieki pamięci są często przyczyną lub czynnikiem przyczyniającym się do starzenia się oprogramowania .

Konsekwencje

Przeciek pamięci zmniejsza wydajność komputera, zmniejszając ilość dostępnej pamięci. Ostatecznie, w najgorszym przypadku, zbyt duża ilość dostępnej pamięci może zostać przydzielona i cały lub część systemu lub urządzenia przestanie działać poprawnie, aplikacja ulegnie awarii lub system znacznie spowolni z powodu thrashingu .

Wycieki pamięci mogą nie być poważne lub nawet nie być wykrywalne w normalny sposób. W nowoczesnych systemach operacyjnych normalna pamięć używana przez aplikację jest zwalniana po zakończeniu działania aplikacji. Oznacza to, że wyciek pamięci w programie, który działa tylko przez krótki czas, może nie zostać zauważony i rzadko jest poważny.

Do znacznie poważniejszych przecieków należą:

  • gdzie program działa przez dłuższy czas i z czasem zużywa dodatkową pamięć, np. zadania w tle na serwerach, ale zwłaszcza na urządzeniach osadzonych, które mogą działać przez wiele lat
  • gdzie nowa pamięć jest często przydzielana do jednorazowych zadań, takich jak renderowanie klatek gry komputerowej lub animowanego wideo
  • gdzie program może zażądać pamięci — takiej jak pamięć współdzielona  — która nie jest zwalniana, nawet po zakończeniu programu
  • gdzie pamięć jest bardzo ograniczona, na przykład w systemie wbudowanym lub urządzeniu przenośnym, lub gdy program wymaga bardzo dużej ilości pamięci na początek, pozostawiając niewielki margines na wycieki
  • gdzie wyciek występuje w systemie operacyjnym lub menedżerze pamięci
  • gdy sterownik urządzenia systemowego powoduje wyciek
  • działa w systemie operacyjnym, który nie zwalnia automatycznie pamięci po zakończeniu programu.

Przykład wycieku pamięci

Poniższy przykład, napisany w pseudocode , ma na celu pokazanie, jak może dojść do wycieku pamięci i jego skutków, bez konieczności posiadania wiedzy programistycznej. Program w tym przypadku jest częścią bardzo prostego oprogramowania przeznaczonego do sterowania windą . Ta część programu jest uruchamiana za każdym razem, gdy ktoś w windzie naciśnie przycisk podłogi.

When a button is pressed:
  Get some memory, which will be used to remember the floor number
  Put the floor number into the memory
  Are we already on the target floor?
    If so, we have nothing to do: finished
    Otherwise:
      Wait until the lift is idle
      Go to the required floor
      Release the memory we used to remember the floor number

Wyciek pamięci wystąpiłby, gdyby żądany numer piętra był tym samym, na którym znajduje się winda; warunek zwolnienia pamięci zostałby pominięty. Za każdym razem, gdy wystąpi ten przypadek, wycieka więcej pamięci.

Takie przypadki zwykle nie przyniosłyby żadnych natychmiastowych skutków. Ludzie rzadko naciskają przycisk piętra, na którym już się znajdują, a w każdym razie winda może mieć wystarczająco dużo wolnej pamięci, że może się to zdarzyć setki lub tysiące razy. Jednak w windzie w końcu zabraknie pamięci. Może to zająć miesiące lub lata, więc może nie zostać odkryte pomimo dokładnych testów.

Konsekwencje byłyby nieprzyjemne; przynajmniej winda przestanie odpowiadać na prośby o przeniesienie na inne piętro (na przykład, gdy próbuje się zadzwonić do windy lub gdy ktoś jest w środku i naciska przyciski piętra). Jeśli inne części programu potrzebują pamięci (na przykład część przypisana do otwierania i zamykania drzwi), nikt nie będzie mógł wejść, a jeśli ktoś znajdzie się w środku, zostanie uwięziony (zakładając, że drzwi nie mogą być otwierane ręcznie).

Wyciek pamięci trwa do momentu zresetowania systemu. Na przykład: jeśli zasilanie windy zostanie wyłączone lub nastąpi awaria zasilania, program przestanie działać. Po ponownym włączeniu zasilania program uruchomi się ponownie i cała pamięć będzie ponownie dostępna, ale powolny proces wycieku pamięci zostanie ponownie uruchomiony razem z programem, ostatecznie uniemożliwiając prawidłowe działanie systemu.

Wyciek w powyższym przykładzie można naprawić, przenosząc operację „uwolnienia” poza warunek warunkowy:

When a button is pressed:
  Get some memory, which will be used to remember the floor number
  Put the floor number into the memory
  Are we already on the target floor?
    If not:
      Wait until the lift is idle
      Go to the required floor
  Release the memory we used to remember the floor number

Problemy z programowaniem

Wycieki pamięci są częstym błędem w programowaniu, zwłaszcza w przypadku używania języków , które nie mają wbudowanego automatycznego zbierania śmieci , takich jak C i C++ . Zwykle występuje przeciek pamięci, ponieważ pamięć przydzielona dynamicznie stała się nieosiągalna . Rozpowszechnienie błędów związanych z wyciekiem pamięci doprowadziło do opracowania wielu narzędzi debugowania do wykrywania nieosiągalnej pamięci. BoundsChecker , Deleaker , IBM Rational Purify , Valgrind , Parasoft Insure++ , Dr. Memory i memwatch to tylko niektóre z bardziej popularnych debuggerów pamięci dla programów C i C++. „Konserwatywne” możliwości wyrzucania elementów bezużytecznych można dodać do dowolnego języka programowania, w którym nie ma ich jako wbudowanej funkcji, a biblioteki do tego są dostępne dla programów C i C++. Konserwatywny kolekcjoner odnajduje i odzyskuje większość, ale nie wszystkie, nieosiągalne wspomnienia.

Chociaż menedżer pamięci może odzyskać pamięć nieosiągalną, nie może zwolnić pamięci, która jest nadal osiągalna, a zatem potencjalnie nadal użyteczna. Współczesne menedżery pamięci zapewniają zatem programistom techniki semantycznego oznaczania pamięci z różnymi poziomami użyteczności, które odpowiadają różnym poziomom osiągalności . Menedżer pamięci nie zwalnia obiektu, który jest silnie osiągalny. Obiekt jest silnie osiągalny, jeśli jest osiągalny bezpośrednio przez silne odwołanie lub pośrednio przez łańcuch silnych odniesień. ( Silne referencje to referencja, która w przeciwieństwie do słabej referencji zapobiega zbieraniu śmieci przez obiekt.) Aby temu zapobiec, deweloper jest odpowiedzialny za czyszczenie referencji po użyciu, zazwyczaj poprzez ustawienie referencji na null, gdy już nie jest. potrzebne i, jeśli to konieczne, przez wyrejestrowanie wszystkich detektorów zdarzeń, które utrzymują silne odniesienia do obiektu.

Ogólnie rzecz biorąc, automatyczne zarządzanie pamięcią jest bardziej niezawodne i wygodne dla programistów, ponieważ nie muszą implementować procedur zwalniających ani martwić się o kolejność wykonywania czyszczenia, ani martwić się o to, czy obiekt jest nadal przywoływany. Programiście łatwiej jest wiedzieć, kiedy odwołanie nie jest już potrzebne, niż wiedzieć, kiedy obiekt nie jest już przywoływany. Jednak automatyczne zarządzanie pamięcią może spowodować obciążenie wydajnościowe i nie eliminuje wszystkich błędów programistycznych, które powodują przecieki pamięci.

RAII

RAII , skrót od Resource Acquisition Is Initialization , jest podejściem do problemu powszechnie stosowanego w C++ , D i Ada . Obejmuje kojarzenie obiektów objętych zakresem z nabytymi zasobami i automatyczne zwalnianie zasobów, gdy obiekty są poza zakresem. W przeciwieństwie do zbierania śmieci, RAII ma tę zaletę, że wie, kiedy obiekty istnieją, a kiedy nie. Porównaj następujące przykłady C i C++:

/* C version */
#include <stdlib.h>

void f(int n)
{
  int* array = calloc(n, sizeof(int));
  do_some_work(array);
  free(array);
}
// C++ version
#include <vector>

void f(int n)
{
  std::vector<int> array (n);
  do_some_work(array);
}

Wersja C, jak zaimplementowana w przykładzie, wymaga jawnego cofnięcia alokacji; tablica jest przydzielana dynamicznie (ze sterty w większości implementacji C) i nadal istnieje, dopóki nie zostanie jawnie zwolniona.

Wersja C++ nie wymaga jawnej cofnięcia alokacji; zawsze nastąpi to automatycznie, gdy tylko obiekt arraywyjdzie poza zakres, w tym w przypadku zgłoszenia wyjątku. Pozwala to uniknąć niektórych narzutów związanych ze schematami wyrzucania śmieci . A ponieważ destruktory obiektów mogą zwalniać zasoby inne niż pamięć, RAII pomaga zapobiegać wyciekom zasobów wejściowych i wyjściowych, do których uzyskuje się dostęp za pośrednictwem handle , których wyrzucanie elementów bezużytecznych funkcji mark-and-sweep nie obsługuje zgrabnie. Należą do nich otwarte pliki, otwarte okna, powiadomienia użytkownika, obiekty w bibliotece rysunków graficznych, podstawowe elementy synchronizacji wątków, takie jak sekcje krytyczne, połączenia sieciowe i połączenia z Rejestrem systemu Windows lub inną bazą danych.

Jednak prawidłowe korzystanie z RAII nie zawsze jest łatwe i wiąże się z własnymi pułapkami. Na przykład, jeśli nie jest się ostrożnym, możliwe jest tworzenie nieaktualnych wskaźników (lub referencji) przez zwrócenie danych przez referencję, tylko po to, aby dane te zostały usunięte, gdy ich obiekt zawierający wyjdzie poza zakres.

D używa kombinacji RAII i garbage collection, wykorzystując automatyczne niszczenie, gdy jasne jest, że nie można uzyskać dostępu do obiektu poza jego pierwotnym zakresem, a garbage collection w przeciwnym razie.

Zliczanie referencji i referencje cykliczne

Bardziej nowoczesne schematy zbierania śmieci są często oparte na pojęciu osiągalności – jeśli nie masz użytecznego odniesienia do danej pamięci, możesz ją zebrać. Inne schematy wyrzucania elementów bezużytecznych mogą opierać się na liczeniu referencji , gdzie obiekt jest odpowiedzialny za śledzenie liczby referencji na niego wskazujących. Jeśli liczba spadnie do zera, oczekuje się, że obiekt uwolni się i pozwoli odzyskać pamięć. Wadą tego modelu jest to, że nie radzi sobie z cyklicznymi referencjami i dlatego obecnie większość programistów jest gotowa zaakceptować ciężar droższych systemów typu mark and sweep .

Poniższy kod w języku Visual Basic ilustruje kanoniczny przeciek pamięci zliczania odwołań:

Dim A, B
Set A = CreateObject("Some.Thing")
Set B = CreateObject("Some.Thing")
' At this point, the two objects each have one reference,

Set A.member = B
Set B.member = A
' Now they each have two references.

Set A = Nothing   ' You could still get out of it...

Set B = Nothing   ' And now you've got a memory leak!

End

W praktyce ten trywialny przykład zostałby od razu zauważony i naprawiony. W większości rzeczywistych przykładów cykl odniesień obejmuje więcej niż dwa obiekty i jest trudniejszy do wykrycia.

Dobrze znany przykład tego rodzaju wycieku stał się widoczny wraz z pojawieniem się technik programowania AJAX w przeglądarkach internetowych w problemie lapsed listener . Kod JavaScript, który skojarzył element DOM z obsługą zdarzeń i nie usunie referencji przed wyjściem, spowodowałby wyciek pamięci (strony internetowe AJAX utrzymują dany DOM dłużej niż tradycyjne strony internetowe, więc ten wyciek był znacznie bardziej widoczny) .

Efekty

Jeśli w programie występuje przeciek pamięci, a jej użycie stale rośnie, zwykle nie wystąpi natychmiastowy objaw. Każdy system fizyczny ma skończoną ilość pamięci, a jeśli wyciek pamięci nie zostanie powstrzymany (na przykład przez ponowne uruchomienie programu, który przecieka), ostatecznie spowoduje to problemy.

Większość nowoczesnych systemów operacyjnych dla komputerów stacjonarnych ma zarówno pamięć główną, fizycznie umieszczoną w mikrochipach RAM, jak i dodatkową pamięć masową, taką jak dysk twardy . Alokacja pamięci jest dynamiczna – każdy proces otrzymuje tyle pamięci, ile żąda. Aktywne strony są przenoszone do pamięci głównej w celu szybkiego dostępu; nieaktywne strony są wypychane do pamięci dodatkowej, aby w razie potrzeby zrobić miejsce. Kiedy pojedynczy proces zaczyna zużywać dużą ilość pamięci, zwykle zajmuje coraz więcej pamięci głównej, wypychając inne programy do pamięci dodatkowej – zwykle znacznie spowalniając wydajność systemu. Nawet jeśli przeciekający program zostanie zakończony, może minąć trochę czasu, zanim inne programy zamienią się z powrotem w pamięć główną, a wydajność powróci do normy.

Gdy cała pamięć w systemie zostanie wyczerpana (niezależnie od tego, czy jest pamięć wirtualna, czy tylko pamięć główna, na przykład w systemie wbudowanym), każda próba przydzielenia większej ilości pamięci zakończy się niepowodzeniem. Zwykle powoduje to, że program próbujący przydzielić pamięć sam się kończy lub generuje błąd segmentacji . Niektóre programy są zaprojektowane tak, aby odzyskać z tej sytuacji (prawdopodobnie przez powrót do wstępnie zarezerwowanej pamięci). Pierwszym programem, który doświadcza braku pamięci, może, ale nie musi, być program, który ma przeciek pamięci.

Niektóre wielozadaniowe systemy operacyjne mają specjalne mechanizmy radzenia sobie z brakiem pamięci, takie jak losowe zabijanie procesów (co może wpływać na „niewinne” procesy) lub zabijanie największego procesu w pamięci (co prawdopodobnie jest przyczyną problem). Niektóre systemy operacyjne mają limit pamięci na proces, aby zapobiec zajmowaniu przez jeden program całej pamięci w systemie. Wadą tego rozwiązania jest to, że czasami system operacyjny musi zostać ponownie skonfigurowany, aby umożliwić prawidłowe działanie programów, które zgodnie z prawem wymagają dużej ilości pamięci, na przykład te zajmujące się grafiką, wideo lub obliczeniami naukowymi.

Wzorzec „piłokształtny” wykorzystania pamięci: nagły spadek używanej pamięci jest potencjalnym objawem wycieku pamięci.

Jeśli wyciek pamięci występuje w jądrze , prawdopodobnie sam system operacyjny ulegnie awarii. Komputery bez zaawansowanego zarządzania pamięcią, takie jak systemy wbudowane, mogą również całkowicie zawieść z powodu trwałego wycieku pamięci.

Publicznie dostępne systemy, takie jak serwery WWW lub routery, są podatne na ataki typu „odmowa usługi”, jeśli atakujący odkryje sekwencję operacji, która może spowodować wyciek. Taka sekwencja jest znana jako exploit .

Wzorzec wykorzystania pamięci „piłokształtny” może być wskaźnikiem wycieku pamięci w aplikacji, szczególnie jeśli pionowe spadki zbiegają się z ponownym uruchomieniem lub ponownym uruchomieniem tej aplikacji. Należy jednak zachować ostrożność, ponieważ punkty zbierania śmieci mogą również powodować taki wzór i wskazywać na zdrowe użytkowanie hałdy.

Inni konsumenci pamięci

Pamiętaj, że stale rosnące użycie pamięci niekoniecznie jest dowodem na przeciek pamięci. Niektóre aplikacje będą przechowywać w pamięci coraz większe ilości informacji (np. jako pamięć podręczna ). Jeśli pamięć podręczna może rozrosnąć się do tego stopnia, że ​​powoduje problemy, może to być błąd programistyczny lub projektowy, ale nie jest to wyciek pamięci, ponieważ informacje pozostają w użyciu. W innych przypadkach programy mogą wymagać nieuzasadnionej dużej ilości pamięci, ponieważ programista założył, że pamięć jest zawsze wystarczająca do określonego zadania; na przykład procesor plików graficznych może rozpocząć od odczytania całej zawartości pliku obrazu i zapisania go w pamięci, co jest niewykonalne, gdy bardzo duży obraz przekracza dostępną pamięć.

Innymi słowy, wyciek pamięci wynika z określonego rodzaju błędu programistycznego, a bez dostępu do kodu programu ktoś, kto widzi objawy, może tylko zgadywać, że może wystąpić wyciek pamięci. Lepiej byłoby używać terminów takich jak „stale rosnące wykorzystanie pamięci”, gdy taka wewnętrzna wiedza nie istnieje.

Prosty przykład w C

Następująca funkcja C celowo powoduje wycieki pamięci, tracąc wskaźnik do przydzielonej pamięci. Można powiedzieć, że przeciek występuje, gdy tylko wskaźnik „a” wyjdzie poza zakres, tj. gdy funkcja function_which_allocates() zwraca bez zwalniania „a”.

#include <stdlib.h>

void function_which_allocates(void) {
    /* allocate an array of 45 floats */
    float *a = malloc(sizeof(float) * 45);

    /* additional code making use of 'a' */

    /* return to main, having forgotten to free the memory we malloc'd */
}

int main(void) {
    function_which_allocates();

    /* the pointer 'a' no longer exists, and therefore cannot be freed,
     but the memory is still allocated. a leak has occurred. */
}

Zobacz też

Bibliografia

Zewnętrzne linki