Zrozumienie czyszczenia pamięci w .NET

170

Rozważ poniższy kod:

public class Class1
{
    public static int c;
    ~Class1()
    {
        c++;
    }
}

public class Class2
{
    public static void Main()
    {
        {
            var c1=new Class1();
            //c1=null; // If this line is not commented out, at the Console.WriteLine call, it prints 1.
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Class1.c); // prints 0
        Console.Read();
    }
}

Teraz, nawet jeśli zmienna c1 w metodzie main jest poza zakresem i nie odwołuje się do niej żaden inny obiekt, kiedy GC.Collect()jest wywoływana, dlaczego nie jest tam sfinalizowana?

Victor Mukherjee
źródło
8
GC nie zwalnia natychmiast wystąpień, gdy są poza zakresem. Robi to, gdy uważa, że ​​jest to konieczne. Możesz przeczytać wszystko o GC tutaj: msdn.microsoft.com/en-US/library/vstudio/0xy59wtx.aspx
user1908061
@ user1908061 (Pssst. Twój link jest uszkodzony)
Dragomok

Odpowiedzi:

352

Potknąłeś się tutaj i wyciągasz bardzo błędne wnioski, ponieważ używasz debuggera. Będziesz musiał uruchomić swój kod tak, jak działa na komputerze użytkownika. Przełącz się najpierw do kompilacji wydania za pomocą menedżera Build + Configuration, zmień opcję „Konfiguracja aktywnego rozwiązania” w lewym górnym rogu na „Wersja”. Następnie przejdź do Narzędzia + Opcje, Debugowanie, Ogólne i odznacz opcję „Wstrzymaj optymalizację JIT”.

Teraz ponownie uruchom program i majsterkuj przy kodzie źródłowym. Zwróć uwagę, że dodatkowe szelki nie mają żadnego efektu. Zwróć też uwagę, że ustawienie zmiennej na null nie robi żadnej różnicy. Zawsze wypisze "1". Teraz działa tak, jak masz nadzieję i spodziewasz się, że zadziała.

Co pozostawia z zadaniem wyjaśnienia, dlaczego działa tak inaczej po uruchomieniu kompilacji debugowania. Wymaga to wyjaśnienia, w jaki sposób moduł odśmiecania pamięci wykrywa zmienne lokalne i jaki ma na to wpływ obecność debugera.

Po pierwsze, jitter spełnia dwa ważne zadania podczas kompilowania IL dla metody do kodu maszynowego. Pierwsza z nich jest bardzo widoczna w debugerze, można zobaczyć kod maszynowy za pomocą okna Debug + Windows + Disassembly. Drugi obowiązek jest jednak całkowicie niewidoczny. Generuje również tabelę opisującą, w jaki sposób używane są zmienne lokalne w treści metody. Ta tabela zawiera wpis dla każdego argumentu metody i zmiennej lokalnej z dwoma adresami. Adres, pod którym zmienna będzie najpierw przechowywać odniesienie do obiektu. I adres instrukcji kodu maszynowego, w której ta zmienna nie jest już używana. Również czy ta zmienna jest przechowywana w ramce stosu, czy w rejestrze procesora.

Ta tabela jest niezbędna dla modułu odśmiecania pamięci, musi wiedzieć, gdzie szukać odwołań do obiektów podczas wykonywania kolekcji. Całkiem łatwo to zrobić, gdy odniesienie jest częścią obiektu na stercie GC. Zdecydowanie nie jest to łatwe, gdy odniesienie do obiektu jest przechowywane w rejestrze procesora. Tabela mówi, gdzie szukać.

Adres „nieużywany” w tabeli jest bardzo ważny. To sprawia, że ​​śmieciarz jest bardzo wydajny . Może zbierać odniesienie do obiektu, nawet jeśli jest używane wewnątrz metody, a ta metoda jeszcze się nie zakończyła. Co jest bardzo powszechne, na przykład twoja metoda Main () przestanie działać tylko tuż przed zakończeniem programu. Oczywiście nie chciałbyś, aby odniesienia do obiektów używane wewnątrz tej metody Main () istniały przez czas trwania programu, co oznaczałoby przeciek. Jitter może użyć tabeli, aby odkryć, że taka zmienna lokalna nie jest już użyteczna, w zależności od tego, jak daleko program posunął się wewnątrz tej metody Main (), zanim wykonał wywołanie.

Niemal magiczną metodą związaną z tą tabelą jest GC.KeepAlive (). Jest to bardzo szczególna metoda, w ogóle nie generuje żadnego kodu. Jego jedynym obowiązkiem jest modyfikacja tej tabeli. to rozszerzaokres istnienia zmiennej lokalnej, co zapobiega pobieraniu elementów bezużytecznych przez przechowywane przez nią odwołanie. Jedynym momentem, w którym musisz go użyć, jest powstrzymanie GC przed nadmiernym gromadzeniem odwołania, co może się zdarzyć w scenariuszach międzyoperacyjnych, w których odwołanie jest przekazywane do niezarządzanego kodu. Moduł odśmiecania pamięci nie widzi takich odwołań używanych przez taki kod, ponieważ nie został skompilowany przez jitter, więc nie ma tabeli, która mówi, gdzie szukać odwołania. Przekazanie obiektu delegata do niezarządzanej funkcji, takiej jak EnumWindows (), jest standardowym przykładem, kiedy trzeba użyć GC.KeepAlive ().

Tak więc, jak widać z przykładowego fragmentu kodu po uruchomieniu go w kompilacji wydania, zmienne lokalne mogą zostać zebrane wcześnie, zanim metoda zakończy wykonywanie. Co więcej, obiekt może zostać zebrany, gdy jedna z jego metod jest uruchomiona, jeśli ta metoda już nie odnosi się do tego . Jest z tym problem, debugowanie takiej metody jest bardzo niewygodne. Ponieważ możesz dobrze umieścić zmienną w oknie Watch lub sprawdzić ją. I zniknie podczas debugowania, jeśli wystąpi GC. Byłoby to bardzo nieprzyjemne, więc jitter jest świadomy obecności dołączonego debuggera. Następnie modyfikujetabeli i zmienia „ostatnio używany” adres. I zmienia go z normalnej wartości na adres ostatniej instrukcji w metodzie. Co utrzymuje zmienną przy życiu, dopóki metoda nie zwróciła. Dzięki temu możesz go obserwować do momentu powrotu metody.

To teraz wyjaśnia również, co widziałeś wcześniej i dlaczego zadałeś pytanie. Wyświetla „0”, ponieważ wywołanie GC.Collect nie może zebrać odwołania. Tabela mówi, że zmienna jest używana po wywołaniu GC.Collect (), aż do końca metody. Zmuszony do powiedzenia tego przez dołączenie debugera i uruchomienie kompilacji debugowania.

Ustawienie zmiennej na null ma teraz wpływ, ponieważ GC sprawdzi zmienną i nie będzie już widzieć odwołania. Ale upewnij się, że nie wpadniesz w pułapkę, w którą wpadło wielu programistów C #, pisanie tego kodu było bezcelowe. Nie ma znaczenia, czy ta instrukcja jest obecna, czy nie, podczas uruchamiania kodu w kompilacji wydania. W rzeczywistości optymalizator jittera usunie tę instrukcję, ponieważ nie ma ona żadnego wpływu. Więc pamiętaj, aby nie pisać takiego kodu, nawet jeśli wydawało się, że ma to wpływ.


Ostatnia uwaga na ten temat: to właśnie sprawia, że ​​programiści piszą małe programy, aby zrobić coś z aplikacją pakietu Office. Debugger zwykle umieszcza je na niewłaściwej ścieżce, chcą, aby program pakietu Office zakończył działanie na żądanie. Odpowiednim sposobem jest wywołanie GC.Collect (). Ale odkryją, że to nie działa, gdy debugują swoją aplikację, prowadząc ich do krainy nigdy-nigdy, wywołując Marshal.ReleaseComObject (). Ręczne zarządzanie pamięcią, rzadko działa poprawnie, ponieważ łatwo przeoczą niewidoczne odniesienie do interfejsu. GC.Collect () faktycznie działa, ale nie podczas debugowania aplikacji.

Hans Passant
źródło
1
Zobacz także moje pytanie, na które Hans odpowiedział mi ładnie. stackoverflow.com/questions/15561025/ ...
Dave Nay,
1
@HansPassant Właśnie znalazłem to niesamowite wyjaśnienie, które również odpowiada na część mojego pytania tutaj: stackoverflow.com/questions/30529379/ ... o GC i synchronizacji wątków. Jedno pytanie, które wciąż mam: zastanawiam się, czy GC faktycznie kompaktuje i aktualizuje adresy, które są używane w rejestrze (przechowywane w pamięci podczas zawieszenia), czy po prostu je pomija? Proces aktualizujący rejestry po zawieszeniu wątku (przed wznowieniem) wydaje mi się poważnym wątkiem bezpieczeństwa, który jest blokowany przez system operacyjny.
atlaste
Pośrednio tak. Wątek jest zawieszony, GC aktualizuje magazyn zapasowy dla rejestrów procesora. Po wznowieniu działania wątku używa teraz zaktualizowanych wartości rejestrów.
Hans Passant
1
@HansPassant, byłbym wdzięczny, gdybyś dodał odniesienia do niektórych nieoczywistych szczegółów garbage collectora CLR, które opisałeś tutaj?
denfromufa
Wydaje się, że jeśli chodzi o konfigurację, ważną kwestią jest włączenie opcji „Optymalizuj kod” ( <Optimize>true</Optimize>in .csproj). Jest to ustawienie domyślne w konfiguracji „Wersja”. Ale w przypadku korzystania z niestandardowych konfiguracji należy wiedzieć, że to ustawienie jest ważne.
Zero3
34

[Chciałem tylko dodać więcej informacji na temat wewnętrznych elementów procesu finalizacji]

Tak więc tworzysz obiekt i kiedy obiekt jest zbierany, Finalizenależy wywołać metodę obiektu . Ale finalizacja wymaga czegoś więcej niż tego bardzo prostego założenia.

KRÓTKIE POJĘCIA:

  1. Obiekty NIE implementują Finalizemetod, tam Pamięć jest odzyskiwana natychmiast, chyba że oczywiście nie są już ponownie buforowane przez
    kod aplikacji

  2. Przedmioty wdrażania Finalizemetody, pojęcie / Wdrażanie Application Roots, Finalization Queue, Freacheable Queueprzychodzi, zanim będą mogły zostać odzyskane.

  3. Każdy obiekt jest uważany za śmieci, jeśli NIE jest możliwy do ponownego zbuforowania przez kod aplikacji

Załóżmy, że :: klasy / obiekty A, B, D, G, H NIE implementują Finalizemetody, a C, E, F, I, J implementują Finalizemetodę.

Gdy aplikacja tworzy nowy obiekt, operator new przydziela pamięć ze sterty. Jeśli typ obiektu zawiera Finalizemetodę, to wskaźnik do obiektu jest umieszczany w kolejce finalizacji .

dlatego wskaźniki do obiektów C, E, F, I, J są dodawane do kolejki finalizacji. Kolejka finalizacja jest wewnętrzna struktura danych kontrolowana przez garbage collector. Każdy wpis w kolejce wskazuje na obiekt, dla którego należy wywołać metodę, zanim będzie można odzyskać pamięć obiektu. Poniższy rysunek przedstawia stertę zawierającą kilka obiektów. Niektóre z tych obiektów są dostępne z poziomu katalogu głównego aplikacji

Finalize, a niektórzy nie. Po utworzeniu obiektów C, E, F, I i J platforma .Net wykrywa, że ​​te obiekty mają Finalizemetody, a wskaźniki do tych obiektów są dodawane do kolejki finalizacji .

wprowadź opis obrazu tutaj

Kiedy pojawia się GC (pierwsza kolekcja), obiekty B, E, G, H, I i J są określane jako śmieci. Ponieważ A, C, D, F są nadal osiągalne przez kod aplikacji przedstawiony za pomocą strzałek z żółtego pola powyżej.

Moduł odśmiecania pamięci skanuje kolejkę finalizacji w poszukiwaniu wskaźników do tych obiektów. Po znalezieniu wskaźnika jest on usuwany z kolejki finalizacji i dołączany do kolejki wolnej („F-osiągalny”). Kolejka freachable inny wewnętrzna struktura danych sterowanych przez śmieciarza. Każdy wskaźnik w kolejce freachable identyfikuje obiekt, który jest gotowy do wywołania jego metody.

Finalize

Po zebraniu (pierwsza kolekcja) zarządzana sterta wygląda podobnie do poniższego rysunku. Objaśnienie podane poniżej:
1.) Pamięć zajmowana przez obiekty B, G i H została natychmiast odzyskana, ponieważ obiekty te nie miały metody finalizacji, którą należało wywołać .

2.) Jednak pamięci zajmowanej przez obiekty E, I i J nie można było odzyskać, ponieważ ich Finalizemetoda nie została jeszcze wywołana. Wywołanie metody Finalize jest wykonywane przez kolejkę z możliwością zwolnienia.

3.) A, C, D, F są nadal dostępne do odczytania za pomocą kodu aplikacji przedstawionego za pomocą strzałek z żółtego pola powyżej, więc w żadnym przypadku NIE zostaną zebrane

wprowadź opis obrazu tutaj

Istnieje specjalny wątek środowiska uruchomieniowego przeznaczony do wywoływania metod Finalize. Gdy kolejka z możliwością zwolnienia jest pusta (co zwykle ma miejsce), ten wątek jest w stanie uśpienia. Ale kiedy pojawiają się wpisy, ten wątek budzi się, usuwa każdy wpis z kolejki i wywołuje metodę Finalize każdego obiektu. Moduł odśmiecania pamięci kompaktuje odzyskiwalną pamięć, a specjalny wątek środowiska uruchomieniowego opróżnia kolejkę z możliwością zwolnienia , wykonując Finalizemetodę każdego obiektu . Więc w końcu jest to, kiedy metoda Finalize zostanie wykonana

Następnym razem, gdy zostanie wywołany garbage collector (2. kolekcja), zobaczy, że sfinalizowane obiekty są naprawdę śmieciami, ponieważ korzenie aplikacji nie wskazują na to, a kolejka wolnostojąca już na nią nie wskazuje (jest również PUSTA), dlatego pamięć obiektów (E, I, J) jest po prostu odzyskiwana ze Sterty (patrz rysunek poniżej i porównaj z rysunkiem powyżej)

wprowadź opis obrazu tutaj

Ważną rzeczą do zrozumienia jest to, że do odzyskania pamięci używanej przez obiekty, które wymagają finalizacji, wymagane są dwa GC . W rzeczywistości wymagane są nawet więcej niż dwie kolekcje kabin, ponieważ obiekty te mogą zostać przeniesione do starszego pokolenia

UWAGA :: kolejka freachable jest uważany za pierwiastek podobnie jak zmienne globalne i statyczne są korzenie. Dlatego, jeśli obiekt znajduje się w kolejce freachable, to jest osiągalny i nie jest śmieciem.

Na koniec pamiętaj, że debugowanie aplikacji to jedno, a Garbage Collection to co innego i działa inaczej. Jak dotąd nie możesz CZUĆ zbierania śmieci tylko przez debugowanie aplikacji, a dalej, jeśli chcesz zbadać pamięć, zacznij tutaj.

RC
źródło