Co należy wiedzieć o Garbage Collection w C#?

Wiedza na temat zarządzania pamięcią w C# jest ważna ale nie jest niezbędna. Garbage Collection wykonuje mnóstwo czynności za Nas. Jednak czasem wiedza o pamięci może być pomocna i taka wiedza przydaje się na rozmowy kwalifikacyjne i na egzaminy (70-483). Poniżej zebranie najważniejszych informacji o GC, które należy znać.

Osobiście mam zdanie, że sposób zarządzania pamięcią w C# i Javie spowodował, że w wielu firmach zamiast walczyć o optymalizację aplikacji to dokłada się RAM-u do serwerów i tyle. Programista nie jest zmuszany ograniczać się pamięcią, przez co różne nawyki powodują, że wielu programistów zupełnie pomija aspekt pamięci w aplikacji. Ma to swoje plusy aplikacja jest prostsza i czytelniejsza ale ceną za to jest ignorancja programistów .NET i Javy na sprawy związane z pamięcią.

Wracając do tematu GC.

Pojęcia, które trzeba znać aby przeczytać posta:

  •  Stos (ang. Stack) i sterta (ang. Heap)
  •  CLR

Czego się dowiemy po przeczytaniu posta:

  • Co to jest GC? Jak działa?
  • Co to są pokolenia?
  • Co to są finalizatory?
  • Co to jest WeakReference?
  • Co to to są zasoby niezarządzane?

C# jest językiem gdzie optymalizacją (czyszczeniem) pamięci zajmuje się Garbage Collection.

Podstawowe założenie

GC(Garbage Collection) traktuje wszystkie obiekty jak śmieci, chyba, że zaznaczono inaczej. Zakłada, że wszystkie obiekty na stercie są krótkotrwałe. To znaczy szybciej usunie obiekt, który krócej przebywa na stercie niż ten, który znajduje się na niej dłużej.

Pamieć

Wszystkie obiekty, które trafiają do pamięci są umieszczane w 2 miejscach na stosie i/lub na stercie.

Stos jest czyszczony gdy kończy się metoda. O to aby tak było dba CLR.

GC

Zarządzaniem stertą zajmuje się GC . Gdyby w .NET-cie nie było GC o tym jak długo istnieje obiekt musielibyśmy się troszczyć sami naprzykład poprzez wywoływanie destruktorów(C++).

GC za każdym razem, gdy zostanie uruchomiony sprawdza, które obiekty ze stosu mają powiązanie z polem klasy, zmienną w metodzie lub globalną  zmienną. Po sprawdzeniu całego stosu, wszystkie elementy, które miały jakieś referencje są zbierane razem (bliżej siebie) w pamięci a reszta obiektów jest usuwana z pamięci.

Podczas sprawdzania powiązań między obiektami wszystkie wątki aplikacji są zatrzymywane. Tak aby stan aplikacji się nie zmieniał.

GC uruchamia się przynajmniej w 2 momentach, gdy kończy się pamięć na stercie do umieszczenia kolejnego obiektu lub gdy OS zgłasza mało pamięci.

Dodatkowo GC włączy się wtedy gdy użycie aplikacji będzie małe. Wszystko po to by jak najmniej wpłynąć na szybkość działania aplikacji.

Pokolenia(generacje)

Jednym z elementów algorytmu czyszczenia pamięci w GC są tzw: pokolenia (generacje). W GC zostały zdefiniowane 3 pokolenia o wyjątkowo atrakcyjnych nazwach 0, 1 i 2. Każde z tych pokoleń zawiera obiekty o określonym wieku. Do pokolenia 0 zaliczane są obiekty nowo powstałe a do pokolenia 2 obiekty najstarsze. Przeniesienie obiektu do wyższej generacji następuje w momencie uruchomienia czyszczenia pamięci. Jeśli obiekt przeżył czyszczenie pamięci to znaczy, że są do niego referencje i można go przenieść na wyższy poziom.

Zasoby niezarządzane(unmanaged resources)

Wszystko o czym pisałem wcześniej dotyczy zasobów zarządzanych(string, int itd.). Jednak w językach programowania istnieją jeszcze zasoby nie zarządzane. Krótko mówiąc są to strumienie danych takie jak:

  • otwarte pliki,
  • otwarte połączenia do sieci,
  • połączenia do baz danych.

Do tego dochodzą jeszcze obiekty typu:

  • COM
  • DLL z nie .NET-owego kodu (C++)
  • Wskaźniki

Cechą wspólną tych obiektów jest to, że sami musimy zadbać(w większości przypadków) o to by te obiekty zamknąć. Prosty przykład to błąd jaki otrzymamy, gdy będziemy chcieli skasować otwarty plik.

Finalizatory

W C# do obsługi zasobów niezarządzanych powstały finalizatory. Są to takie destruktory nad którymi nie mamy kontroli.

Poniżej przykład jak taki finalizator zdefiniować:

public class Klasa
{
~Klasa()
{
// Cały kod w tej metodzie zostanie wykonany, gdy GC będzie niszczył obiekt.
}
}

Nie wiemy dokładnie kiedy metoda ~Klasa() zostanie wykonana, choć uruchami się wtedy gdy GC zaczyna proces czyszczenia pamięci – jeśli okaże się, że obiekt można zniszczyć wtedy wykonywana jest metoda w klasie odpowiedzialna za zwalnianie zasobów ~Klasa() – ale my nie możemy przewidzieć miejsca gdzie GC się uruchomi. Poniżej kod, który doskonale tłumaczy w czym rzecz:

public void GCMethod()
{
 StreamWriter stream = File.CreateText("plik.txt");
 stream.Write("Przemek Walkowski");

 GC.Collect();
 GC.WaitForPendingFinalizers();

 File.Delete("plik.txt");
}

Naszym obiektem niezarządzanym jest tutaj strumień plikowy. Otwieramy plik, zapisujemy w nim dane a następnie wymuszamy uruchomienie czyszczenia pamięci poprzez polecenie GC.Collect(), kolejna linia upewnia Nas aby GC poczekał aż wszystkie finalizatory się zakończyły. Po tej operacji skasowanie pliku będzie możliwe mimo, że nie zamknęliśmy strumienia oraz nie używamy słowa kluczowego using., ponieważ GC sprawdził wszystkie referencje do strumienia stream a, że żadnych referencji nie było, więc strumień mógł zostać zamknięty.

Na marginesie GC.Collect i inne polecenie GC nie są zalecane do użycia. Algorytm GC jest dobrze zoptymalizowany i na potrzeby Naszych programów w C# i nie ma potrzeby wykonywać takich operacji. Inna sprawa to gdy działamy na obiektach niezarządzanych. Wtedy są pewnie miejsca gdzie takie operacje mogą się przydać.

Nie odłączonym elementem finalizatora jest interfejs IDisposable, którego metoda Dispose() dba o uwalnianie pamięci. Różnica pomiędzy finalizatorem a interfejsem IDisposable jest taka, że finalizator uruchamia się automatycznie i nie mamy na nim kontroli a metodę Dispose() możemy wywołać w dowolnej części kodu.

Luźe referencje (weak reference)

Ostatnim elementem wartym wspomnienia, jeśli chodzi o GC jest koncepcja WeakReference.  Osobiście używam takiego przykładu, który tłumaczy na czym to polega:

Wyobraźmy sobie, że mamy aplikację, której metoda GetData() pobiera dużą ilość danych. Następnie w aplikacji mamy trzy niezależne metody ShowData(), PrintData() i DoWork() – ShowData() i PrintData() potrzebują danych zwracanych przez GetData(). Jeśli dane zwracane przez  GetData() będą w zmiennej globalnej to, gdy uruchomi się GC zawsze będzie istnieć referencja do zmiennej globalnej, dlatego GC nigdy nie usunie tego obiektu.

Natomiast jeśli dane zwracane przez GetData() będą przechowywane w obiekcie WeakReference a metody ShowData() i PrintData() będą używały właśnie tego obiektu jako dostarczyciela danych to po włączeniu się GC obiekt zostanie usunięty, ponieważ WeakReference nie trzyma tak naprawdę referencji do obiektu.

Co to daje? Jest to prosty mechanizm catch-owania danych. Jeśli pobraliśmy dane a aktualnie aplikacja zajmuje się czymś innym wykonuje metodę DoWork(), która nie używa danych z GetData() oraz posiadamy wystarczająco dużo pamięci to dane będą przechowywane tak długo jak długo starczy pamięci w systemie.

Przykładowe użycie WeakReference:


public static WeakReference data;

private static object GetData()
{
 if (data == null)
 {
  data = new WeakReference(LoadLargeList());
 }

 if (data.Target == null)
 {
  data.Target = LoadLargeList();
 }
 return data.Target;
}

Kontynuując przykład z wyżej metody ShowData()PrintData() będą używały zmiennej data do operacji na danych. Jeśli teraz GC się uruchomi to dane znajdujące w obiekcie date.Target zostaną usunięte i będzie trzeba je pobrać od nowa.

Gdyby zmienna data była typu, który jest zwraca przez metodę GetData() to nigdy nie nastąpiło by zwolnienie pamięci, ponieważ w aplikacji były by referencje do tego obiektu.

Przykładami z życia używania WeakReference jest przechowywanie modeli danych w aplikacjach MVVM w WPF-ie. Na przykład w framework-u WAF.

Podsumowując

  1. Obiekty C# przechowywane są w pamięci podzielonej na stos i stertę.
  2. Sterta jest zarządzana przez GC a stos przez CLR.
  3. GC kasuje obiekty, gdy nie ma do nich żadnych referencji.
  4. Finalizatory wykonywane są gdy GC kasuje obiekt.
  5. WeakReference używane są do zarządzania referencjami do obiektów.

Jeśli jest jeszcze coś co warto znać o GC, proszę o komentarze, chętnie dopisze do postu by uczynić tą listę jeszcze bardziej użyteczną i pełniejszą.

4 przemyślenia nt. „Co należy wiedzieć o Garbage Collection w C#?

  1. Rozbawiłeś mnie tym, że wiedza o GC jest potrzebna, ale na rozmowy kwalifikacyjne i konkretny egzamin.

    LOH to Large Object Heap. Przeznaczony jest dla obiektów powyżej 85000 bajtów i działa o tyle inaczej, że cyklu pracy GC obiekty nie są kompaktowane.

  2. Należy wiedzieć znacznie więcej aby poprawnie zarządzać pamięcią w .NET ale na początek wszystko co napisałeś jest zupełnie wystarczające.

    Nie napisałeś o ważnym aspekcie jakim jest zarządzanie LOH w GC.

    1. Dzieki za uwagi. A mozesz wkleic jakiegos linka co to jest ten LOH? Bo nie kojarze. Poczytam o tym i umieszcze na blogu. Z gory dzieki

Możliwość komentowania jest wyłączona.