Naprawdę nie można wydawać ogólnych oświadczeń o odpowiednim sposobie korzystania ze wszystkich implementacji GC. Różnią się bardzo. Więc porozmawiam z .NET, o którym pierwotnie mówiłeś.
Musisz dość dokładnie znać zachowanie GC, aby zrobić to z dowolnej logiki lub powodu.
Jedyną radą dotyczącą zbiórki, jaką mogę udzielić, jest: Nigdy tego nie rób.
Jeśli naprawdę znasz skomplikowane szczegóły GC, nie będziesz potrzebować mojej rady, więc to nie będzie miało znaczenia. Jeśli jeszcze nie wiesz ze 100% pewnością, to pomoże i będziesz musiał szukać w Internecie i znaleźć odpowiedź w następujący sposób: Nie powinieneś dzwonić do GC.Collect , lub alternatywnie: Powinieneś dowiedzieć się więcej o tym, jak działa GC wewnątrz i na zewnątrz, i dopiero wtedy poznasz odpowiedź .
Jest jedno bezpieczne miejsce, w którym warto używać GC .
GC.Collect to dostępny interfejs API, którego można używać do profilowania czasów rzeczy. Możesz profilować jeden algorytm, zbierać i profilować inny algorytm od razu, wiedząc, że GC pierwszego algo nie wystąpił podczas drugiego, wypaczając wyniki.
Tego rodzaju profilowanie to jedyny raz, który sugerowałbym każdemu ręcznie.
W każdym razie wymyślony przykład
Jednym z możliwych przypadków użycia jest to, że jeśli załadujesz naprawdę duże rzeczy, trafią one do Dużych Stert Obiektów, które trafią prosto do Gen 2, chociaż znowu Gen 2 jest dla obiektów o długiej żywotności, ponieważ gromadzi się rzadziej. Jeśli wiesz, że z jakiegoś powodu ładujesz krótkotrwałe obiekty do Gen 2, możesz je szybciej usunąć, aby zmniejszyć Gen 2 i szybciej zbierać kolekcje.
To najlepszy przykład, jaki mogłem wymyślić, i to nie jest dobre - presja LOH, którą tutaj budujesz, spowodowałaby częstsze kolekcje, a kolekcje są tak częste, jak to jest - są szanse, że wyczyści LOH tak samo jak tak szybko, jak zdmuchnąłeś go tymczasowymi przedmiotami. Po prostu nie ufam sobie, że zakładam lepszą częstotliwość zbierania niż sam GC - dostrojony przez ludzi o wiele mądrzejszych niż ja.
Porozmawiajmy więc o niektórych semantykach i mechanizmach w .NET GC ... lub ...
Wszystko, co myślę, że wiem o .NET GC
Każdy, kto znajdzie tutaj błędy - popraw mnie. Znaczna część GC jest znana jako czarna magia i chociaż próbowałem pominąć szczegóły, których nie byłem pewien, prawdopodobnie nadal coś popełniłem źle.
Poniżej celowo brakuje wielu szczegółów, których nie jestem pewien, a także znacznie większej ilości informacji, których po prostu nie jestem świadomy. Wykorzystaj te informacje na własne ryzyko.
Koncepcje GC
.NET GC występuje w niespójnych czasach, dlatego nazywa się go „niedeterministycznym”, co oznacza, że nie można polegać na nim w określonych momentach. Jest to również generator śmieci, co oznacza, że dzieli twoje obiekty na liczbę przejść GC, przez które przeszli.
Obiekty w stercie Gen 0 przeżyły 0 kolekcji, zostały one nowo utworzone, więc ostatnio nie wystąpiła żadna kolekcja od ich wystąpienia. Obiekty na stosie Gen 1 przeszły przez jeden przebieg kolekcji, podobnie obiekty na stosie Gen 2 przeżyły 2 przebiegi kolekcji.
Teraz warto zauważyć, dlaczego odpowiednio kwalifikuje te konkretne generacje i partycje. .NET GC rozpoznaje tylko te trzy generacje, ponieważ przebiegi kolekcji, które przechodzą przez te trzy stosy, są nieco inne. Niektóre obiekty mogą przetrwać zbiór przechodzi tysiące razy. GC pozostawia je po drugiej stronie partycji sterty Gen 2, nie ma sensu ich dalej dzielić, ponieważ tak naprawdę są Gen 44; przekazywanie kolekcji jest takie samo jak wszystko na stosie Gen 2.
Te pokolenia mają cele semantyczne, a także zaimplementowane mechanizmy, które je honorują, a do nich przejdę za chwilę.
Co jest w kolekcji
Podstawowa koncepcja przekazania kolekcji GC polega na tym, że sprawdza on każdy obiekt w przestrzeni sterty, aby sprawdzić, czy nadal istnieją aktywne odwołania (korzenie GC) do tych obiektów. Jeśli dla obiektu zostanie znaleziony katalog główny GC, oznacza to, że aktualnie wykonywany kod może nadal dotrzeć do tego obiektu i go użyć, więc nie można go usunąć. Jeśli jednak nie znaleziono katalogu głównego GC dla obiektu, oznacza to, że uruchomiony proces nie potrzebuje już obiektu, więc może go usunąć, aby zwolnić pamięć dla nowych obiektów.
Teraz, kiedy skończy sprzątać kilka obiektów i pozostawi niektóre z nich w spokoju, nastąpi niefortunny efekt uboczny: wolne przestrzenie między żywymi obiektami, w których martwe zostały usunięte. Ta fragmentacja pamięci, jeśli pozostawiona sama, po prostu zmarnowałaby pamięć, więc kolekcje zwykle wykonują tak zwane „zagęszczenie”, w którym zabiorą wszystkie żywe obiekty pozostawione i ściśną je razem na stosie, dzięki czemu wolna pamięć będzie sąsiadować po jednej stronie stosu dla Gen 0.
Biorąc teraz pod uwagę 3 stosy pamięci podzielone według liczby przejść, przez które przeszły, porozmawiajmy o tym, dlaczego takie partycje istnieją.
Kolekcja Gen 0
Gen 0, będąc absolutnie najnowszymi obiektami, zwykle jest bardzo mały - więc możesz go bezpiecznie zbierać bardzo często . Częstotliwość zapewnia, że sterty pozostają małe, a zbiory są bardzo szybkie, ponieważ gromadzą się na tak małej sterty. Opiera się to mniej więcej na heurystyce, która twierdzi: Znaczna większość tworzonych obiektów tymczasowych jest bardzo tymczasowa, więc tymczasowe nie będą już używane ani odwoływać się do nich prawie natychmiast po użyciu, a zatem mogą być gromadzone.
Kolekcja 1. generacji
Gen 1, będący obiektami, które nie należą do tej bardzo tymczasowej kategorii obiektów, może być raczej krótkotrwały, ponieważ nadal - ogromna część stworzonych obiektów nie jest używana długo. Dlatego Gen 1 zbiera również dość często, ponownie utrzymując niewielki stos, dzięki czemu zbiory są szybkie. Jednak zakłada się, że mniej obiektów jest tymczasowych niż Gen 0, więc zbiera się rzadziej niż Gen 0
Powiem szczerze, że nie znam mechanizmów technicznych, które różnią się między przepustką kolekcjonerską Gen 0 a Gen 1, jeśli są jakieś inne niż częstotliwość, którą zbierają.
Kolekcja Gen 2
Gen 2 teraz musi być matką wszystkich kup, prawda? Cóż, tak, mniej więcej tak. To miejsce, w którym żyją wszystkie twoje stałe obiekty - na przykład obiekt, w którym żyjesz Main()
, i wszystko, do czego się Main()
odwołuje, ponieważ będą one zakorzenione aż do Main()
powrotu po zakończeniu procesu.
Biorąc pod uwagę, że Gen 2 jest zbiorem praktycznie wszystkiego, czego inne pokolenia nie mogły zebrać, jego obiekty są w dużej mierze trwałe lub przynajmniej żyją przynajmniej. Tak więc rozpoznanie bardzo niewielkiej części tego, co znajduje się w Gen 2, będzie w rzeczywistości czymś, co można zebrać, nie trzeba go często zbierać. Pozwala to również na spowolnienie gromadzenia, ponieważ wykonuje się o wiele rzadziej. Właśnie w tym miejscu zajęli się wszystkimi dodatkowymi zachowaniami dla nieparzystych scenariuszy, ponieważ mają czas na ich wykonanie.
Kupa dużych obiektów
Jednym z przykładów dodatkowych zachowań Gen 2 jest to, że wykonuje również kolekcję na stosie dużych obiektów. Do tej pory mówiłem całkowicie o stosie małych obiektów, ale środowisko wykonawcze .NET przydziela rzeczy o określonych rozmiarach do osobnej sterty z powodu tego, co nazwałem powyżej kompaktowaniem. Zagęszczanie wymaga przemieszczania obiektów po zakończeniu zbierania na stosie małych obiektów. Jeśli w Gen 1 znajduje się żywy obiekt o wielkości 10 MB, ukończenie zagęszczania po kolekcji zajmie znacznie więcej czasu, co spowolni kolekcję Gen 1. Tak więc obiekt 10 MB jest przydzielany do sterty dużych obiektów i zbierany podczas Gen 2, który tak rzadko się uruchamia.
Finalizacja
Innym przykładem są obiekty z finalizatorami. Umieszczasz finalizator na obiekcie, który odwołuje się do zasobów poza zakresem .NETs GC (zasoby niezarządzane). Finalizator to jedyny sposób, w jaki GC może zażądać gromadzenia niezarządzanego zasobu - zaimplementujesz go w celu ręcznego gromadzenia / usuwania / uwalniania niezarządzanego zasobu, aby upewnić się, że nie wycieknie on z twojego procesu. Kiedy GC może uruchomić finalizator obiektów, twoja implementacja wyczyści niezarządzany zasób, dzięki czemu GC będzie w stanie usunąć twój obiekt bez ryzyka wycieku zasobów.
Mechanizmem, dzięki któremu finalizatory to robią, jest bezpośrednie odwoływanie się do nich w kolejce finalizacji. Gdy środowisko wykonawcze przydziela obiekt za pomocą finalizatora, dodaje wskaźnik do tego obiektu do kolejki finalizacji i blokuje obiekt na miejscu (zwany pinowaniem), aby zagęszczenie go nie przesunęło, co spowodowałoby uszkodzenie odniesienia do kolejki finalizacji. W miarę upływu kolekcji, w końcu okaże się, że Twój obiekt nie ma już katalogu głównego GC, ale finalizacja musi zostać wykonana, zanim będzie można go zebrać. Kiedy obiekt jest martwy, kolekcja przeniesie swoje odwołanie z kolejki finalizacji i umieści odniesienie do niego w tak zwanej kolejce „FReachable”. Następnie kolekcja jest kontynuowana. W innym „niedeterministycznym” czasie w przyszłości osobny wątek znany jako wątek finalizatora przejdzie przez kolejkę FReachable, wykonując finalizatory dla każdego z wymienionych obiektów. Po zakończeniu kolejka FReachable jest pusta i odwróciła nieco w nagłówku każdego obiektu, co oznacza, że nie wymaga on finalizacji (ten bit można również odwrócić ręcznie za pomocąGC.SuppressFinalize
co jest powszechne w Dispose()
metodach), podejrzewam również, że odpiął obiekty, ale nie cytuj mnie w tym. Następna kolekcja, która pojawi się na stosie, w którym znajduje się ten obiekt, w końcu go zbierze. Kolekcje Gen 0 nawet nie zwracają uwagi na obiekty z tym bitem wymaganym do finalizacji, automatycznie je promuje, nawet nie sprawdzając ich rootowania. Nieukroszony obiekt, który wymaga finalizacji w Gen 1, zostanie rzucony do FReachable
kolejki, ale kolekcja nie robi z nim nic innego, więc żyje w Gen 2. W ten sposób wszystkie obiekty, które mają finalizator, i nie GC.SuppressFinalize
zostaną zebrane w Gen 2.