🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Garbage Collector w C# — generacje i pułapki

Twoja aplikacja tworzy tysiące obiektów na minutę — a mimo to nie wywołujesz nigdzie free() ani delete i pamięć nie wycieka. Robi to za Ciebie Garbage Collector (GC). Dopóki działa, łatwo o nim zapomnieć — do momentu, gdy w logach pojawia się OutOfMemoryException albo aplikacja „przycina się” co kilka sekund. W tym wpisie zobaczysz, jak GC naprawdę działa: skąd wie, co jest śmieciem, po co mu generacje i kiedy sam potrafisz mu zaszkodzić.

Skąd GC wie, co jest śmieciem?

Obiekty tworzone operatorem new trafiają na zarządzaną stertę (managed heap) — o różnicy między stosem a stertą przeczytasz we wpisie o typach wartościowych i referencyjnych. GC okresowo przechodzi po tzw. korzeniach (roots): polach statycznych, zmiennych lokalnych aktywnych metod na stosach wątków i uchwytach GC. Wszystko, do czego da się dojść od korzeni po łańcuchu referencji, jest „żywe”. Reszta to śmieci.

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

Person person = new Person(); // "person" (zmienna lokalna) jest korzeniem —
                              // obiekt na stercie jest osiągalny, więc żywy

Zauważ konsekwencję: obiekt Person trzyma referencję do stringa Name — dopóki żyje osoba, żyje też jej string. GC zwalnia dopiero całe nieosiągalne grafy obiektów, nie pojedyncze pola.

Generacje 0, 1, 2 — dlaczego GC jest szybki

Skanowanie całej sterty przy każdym czyszczeniu byłoby zbyt kosztowne. GC opiera się więc na obserwacji zwanej hipotezą generacyjną: większość obiektów żyje bardzo krótko (zmienna w metodzie, DTO w jednym żądaniu HTTP). Sterta jest podzielona na generacje:

GeneracjaCo tu trafiaJak często zbierana
Gen 0Nowe obiektyBardzo często, bardzo tanio
Gen 1Obiekty, które przeżyły zbiórkę Gen 0Rzadziej
Gen 2Obiekty długowieczne (cache, singletony, statyki)Najrzadziej — pełna zbiórka jest najdroższa
LOHObiekty ≥ 85 000 bajtów (duże tablice, bufory)Razem z Gen 2, domyślnie bez kompaktowania

Obiekt, który przeżyje zbiórkę, awansuje do wyższej generacji. Dzięki temu GC zwykle skanuje tylko młodą, małą część sterty — i to jest cały sekret jego wydajności.

Reguła do zapamiętania: im krócej żyją Twoje obiekty, tym taniej pracuje GC. Drogie są obiekty „w średnim wieku” — te, które przeżywają do Gen 1/2, a potem i tak umierają.

Zobacz GC w akcji — licznik kolekcji

Klasa GC udostępnia m.in. GC.CollectionCount(generacja) — licznik zbiórek danej generacji. Prosty eksperyment: tworzymy milion obiektów, nie zapisując do nich referencji, i co 100 000 iteracji wypisujemy sumę zbiórek.

class Program
{
    static void Main()
    {
        for (int i = 0; i < 1_000_000; i++)
        {
            var p = new Person { Id = i, Name = i.ToString(), Email = i.ToString() };

            if (i % 100_000 == 0)
            {
                Console.WriteLine("Iteracja: " + i + ", zbiórek GC: " + GetTotalCollections());
            }
        }
    }

    static int GetTotalCollections()
    {
        return GC.CollectionCount(0) +
               GC.CollectionCount(1) +
               GC.CollectionCount(2);
    }
}

Każdy obiekt staje się śmieciem natychmiast po utworzeniu (nikt nie trzyma referencji), więc licznik rośnie leniwie — małe obiekty długo mieszczą się w budżecie Gen 0. Wystarczy jednak dodać do klasy pole byte[] Buffer = new byte[100_000]; (czyli każdy obiekt ciągnie za sobą 100 KB — ponad próg LOH!), a liczba zbiórek skoczy o rzędy wielkości. Ten sam program, jedna linijka różnicy — dlatego przy dużych buforach warto znać Span<T> i Memory<T>, które pozwalają pracować na wycinkach pamięci bez nowych alokacji. Z tego samego powodu sklejanie stringów w pętli zastępuje się StringBuilderem.

Kompaktowanie sterty

Po zwolnieniu nieosiągalnych obiektów w stercie zostają „dziury”. GC przesuwa więc żywe obiekty obok siebie (kompaktowanie) i poprawia wszystkie referencje, żeby dalej wskazywały we właściwe miejsca — trochę jak defragmentacja dysku, tylko automatyczna i niewidoczna dla Twojego kodu. Wyjątkiem jest LOH: dużych obiektów domyślnie się nie przesuwa (koszt kopiowania megabajtów byłby zbyt wysoki), przez co LOH może się fragmentować.

Pułapki — tu GC przestaje być magią

Pułapka #1: ręczne GC.Collect()

GC.Collect() wymusza pełną, blokującą zbiórkę wszystkich generacji. Wywoływany „profilaktycznie” psuje dokładnie to, co czyni GC szybkim: sztucznie awansuje krótkotrwałe obiekty do wyższych generacji i zamraża wątki aplikacji. Uzasadnione użycia są rzadkie (np. tuż po zwolnieniu ogromnego, jednorazowego zbioru danych albo w benchmarkach). Jeśli nie umiesz wskazać konkretnego powodu — nie wywołuj go.

Pułapka #2: referencje, o których zapomniałeś

GC nie zwolni niczego, do czego wciąż prowadzi referencja. Wystarczy odkładać obiekty do statycznej listy, żeby „nieśmiertelne” obiekty zapełniły stertę:

static List<Person> people = new List<Person>();

for (int i = 0; i < 1_000_000; i++)
{
    // każdy obiekt ma pole byte[100_000] — po ~600 000 iteracji:
    // Unhandled exception. System.OutOfMemoryException
    people.Add(new Person { Id = i });
}

W realnym kodzie tę rolę częściej gra cache bez limitu albo zdarzenie: publisher trzymający referencję do subskrybenta (obiekt.JakiesZdarzenie += Handler; bez późniejszego -=) potrafi utrzymywać przy życiu całe grafy obiektów. To najczęstsze źródło „wycieków pamięci” w języku, który teoretycznie ich nie ma.

Pułapka #3: GC nie zwalnia zasobów niezarządzanych

Plik, połączenie z bazą, gniazdo sieciowe — to zasoby systemu operacyjnego, o których GC wie tylko tyle, że opakowujący je obiekt .NET-owy przestał być osiągalny. Kiedy je zwolni? Nie wiadomo — może za milisekundę, może za minutę. Dlatego typy trzymające takie zasoby implementują IDisposable, a Ty zwalniasz je deterministycznie blokiem using, zamiast liczyć na GC.

Checklist przed code review

  • Żadnych GC.Collect() bez komentarza wyjaśniającego konkretny powód.
  • Kolekcje statyczne i cache mają politykę usuwania wpisów (limit, TTL).
  • Każde += na zdarzeniu ma ścieżkę do -= (albo subskrybent żyje krócej niż publisher).
  • Obiekty IDisposable zawsze w using.
  • Bufory ≥ 85 000 B (LOH!) — przemyślane: pula buforów, Span<T>, mniejsze porcje.

Podsumowanie

  • GC zwalnia obiekty nieosiągalne od korzeni (statyki, zmienne lokalne, uchwyty) — całe grafy, nie pojedyncze pola.
  • Generacje 0/1/2 sprawiają, że typowa zbiórka skanuje tylko młodą część sterty; obiekty ≥ 85 000 B żyją na LOH zbieranym z Gen 2.
  • Kompaktowanie usuwa fragmentację zwykłej sterty; LOH domyślnie nie jest kompaktowany.
  • GC.Collect() to niemal zawsze pesymalizacja — zostaw decyzję środowisku uruchomieniowemu.
  • „Wycieki” w C# to zapomniane referencje (statyczne kolekcje, zdarzenia) i niezamknięte zasoby — using + higiena subskrypcji załatwiają 90% przypadków.
👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 Co dalej?

Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.

69 comments

  1. I’m truly enjoying the design and layout of your website.
    It’s a very easy on the eyes which makes it much more enjoyable for me to come here and visit more often. Did you hire out a designer to create your theme?

    Exceptional work!

  2. Magnificent goods from you, man. I’ve bear in mind your
    stuff previous to and you are simply extremely fantastic.
    I really like what you’ve acquired here, really like what you’re saying and the best way wherein you are saying
    it. You are making it enjoyable and you still care for to keep it sensible.
    I can’t wait to read much more from you. That is actually a tremendous website.

  3. I was very pleased to discover this great site. I need to to thank you for ones time for this wonderful read!! I definitely appreciated every little bit of it and i also have you saved to fav to look at new things in your site. Eunice Ken Mosra

  4. Having read this I thought it was extremely enlightening. I appreciate you taking the time and energy to put this informative article together. I once again find myself spending a lot of time both reading and posting comments. But so what, it was still worth it. Ketti Jeramie Shriver

  5. Hello there. I found your blog by way of Google at the same time as searching for a related topic, your site came up. It appears to be great. I have bookmarked it in my google bookmarks to visit then. Paola Duky Oringa

  6. Hey there, I think your website might be having browser compatibility issues. When I look at your blog in Chrome, it looks fine but when opening in Internet Explorer, it has some overlapping. I just wanted to give you a quick heads up! Other then that, excellent blog! Clarabelle Morgen Edric

  7. Thank you for each of your hard work on this web page. My mom takes pleasure in participating in research and it is easy to see why. I know all about the lively way you convey valuable items on the web blog and as well as cause contribution from some other people on that issue then our favorite child is really understanding a great deal. Enjoy the remaining portion of the year. Your conducting a tremendous job. Lyndsie Kerk Vladi

  8. I really wanted to write down a small note to express gratitude to you for these unique solutions you are placing on this website. My incredibly long internet look up has at the end been honored with brilliant information to write about with my companions. I would tell you that most of us visitors actually are undeniably blessed to be in a perfect place with many outstanding individuals with interesting pointers. I feel truly lucky to have encountered your entire weblog and look forward to some more thrilling minutes reading here. Thanks a lot again for all the details. Antonina Sax Alli

Dodaj komentarz

czytanie to początek

Zamień wiedzę w umiejętności

Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.

Pobieram roadmapę →