Eden Space
Więc moje pytanie brzmi: czy którekolwiek z tych stwierdzeń może być prawdziwe, a jeśli tak, to dlaczego alokacja sterty Java jest o wiele szybsza.
Studiowałem trochę o tym, jak działa Java GC, ponieważ jest to dla mnie bardzo interesujące. Zawsze staram się poszerzać swoją kolekcję strategii alokacji pamięci w C i C ++ (zainteresowany próbą zaimplementowania czegoś podobnego w C), i jest to bardzo, bardzo szybki sposób na alokację wielu obiektów w trybie serii praktyczna perspektywa, ale przede wszystkim ze względu na wielowątkowość.
Sposób alokacji Java GC polega na użyciu wyjątkowo taniej strategii alokacji, aby początkowo alokować obiekty do przestrzeni „Eden”. Z tego, co mogę powiedzieć, używa sekwencyjnego przydziału puli.
Jest to o wiele szybsze, jeśli chodzi o algorytm i ograniczenie obowiązkowych błędów strony niż ogólnego przeznaczenia malloc
w C lub domyślnie, rzucając operator new
w C ++.
Ale sekwencyjne alokatory mają rażącą słabość: mogą alokować porcje o zmiennej wielkości, ale nie mogą uwolnić żadnych pojedynczych porcji. Po prostu przydzielają w prosty sekwencyjny sposób z dopełnieniem do wyrównania i mogą wyczyścić tylko całą przydzieloną pamięć naraz. Przydają się zwykle w C i C ++ do konstruowania struktur danych, które wymagają jedynie wstawiania i usuwania elementów, takich jak drzewo wyszukiwania, które musi zostać zbudowane tylko raz, gdy program się uruchamia, a następnie jest wielokrotnie przeszukiwane lub dodawane są tylko nowe klucze ( brak kluczy usuniętych).
Mogą być również używane nawet w strukturach danych, które pozwalają na usuwanie elementów, ale te elementy nie zostaną w rzeczywistości zwolnione z pamięci, ponieważ nie możemy ich indywidualnie zwolnić. Taka struktura wykorzystująca sekwencyjny alokator zużywałaby po prostu coraz więcej pamięci, chyba że miałaby jakieś odroczone przejście, w którym dane zostały skopiowane do nowej, zwartej kopii przy użyciu osobnego sekwencyjnego alokatora (a czasami jest to bardzo skuteczna technika, jeśli wygrany ustalony alokator wygrał z jakiegoś powodu - po prostu od razu po kolei przydziel nową kopię struktury danych i zrzuć całą pamięć starej).
Kolekcja
Podobnie jak w powyższym przykładzie struktury danych / puli sekwencyjnej, ogromnym problemem byłoby, gdyby Java GC alokowała tylko w ten sposób, mimo że jest super szybka dla alokacji serii wielu pojedynczych porcji. Nie byłby w stanie niczego zwolnić, dopóki oprogramowanie nie zostanie zamknięte, w którym to momencie może uwolnić (oczyścić) wszystkie pule pamięci jednocześnie.
Zamiast tego po jednym cyklu GC przechodzi się przez istniejące obiekty w przestrzeni „Eden” (przydzielane sekwencyjnie), a te, do których nadal się odwołuje, są przydzielane za pomocą bardziej ogólnego przeznaczenia, który może uwolnić poszczególne porcje. Te, o których już nie ma mowy, zostaną po prostu zwolnione w procesie oczyszczania. Zasadniczo jest to więc „kopiowanie obiektów z przestrzeni Eden, jeśli nadal się do nich odwołuje, a następnie czyszczenie”.
Zwykle byłoby to dość drogie, więc odbywa się to w osobnym wątku w tle, aby uniknąć znacznego zablokowania wątku, który pierwotnie przydzielił całą pamięć.
Po skopiowaniu pamięci z miejsca Eden i przydzieleniu jej przy użyciu tego droższego schematu, który może uwolnić poszczególne porcje po początkowym cyklu GC, obiekty przenoszą się do bardziej trwałego regionu pamięci. Te pojedyncze fragmenty są następnie uwalniane w kolejnych cyklach GC, jeśli przestaną być przywoływane.
Prędkość
Mówiąc wprost, powodem, dla którego Java GC może znacznie przewyższyć C lub C ++ przy alokacji prostej stosu, jest to, że używa najtańszej, całkowicie zdegenerowanej strategii alokacji w wątku żądającym alokacji pamięci. Następnie oszczędza to droższą pracę, którą normalnie musielibyśmy wykonać, używając bardziej ogólnego alokatora, takiego jak wyprostowanie malloc
dla innego wątku.
Tak więc koncepcyjnie GC musi wykonać ogólnie więcej pracy, ale rozkłada to na wątki, aby pełny koszt nie był opłacany z góry przez jeden wątek. Pozwala to wątkowi przydzielić pamięć, aby zrobić to bardzo tanio, a następnie odłożyć prawdziwy koszt wymagany do prawidłowego wykonania czynności, aby poszczególne obiekty mogły zostać faktycznie uwolnione do innego wątku. W C lub C ++, kiedy my malloc
lub call operator new
, musimy zapłacić pełny koszt z góry w ramach tego samego wątku.
Jest to główna różnica i dlaczego Java może znacznie przewyższać C lub C ++, używając tylko naiwnych wywołań malloc
lub operator new
przydzielić indywidualnie kilka małych fragmentów. Oczywiście zwykle będą pewne operacje atomowe i pewne potencjalne blokowanie, gdy rozpocznie się cykl GC, ale prawdopodobnie jest to całkiem sporo zoptymalizowane.
Zasadniczo proste wyjaśnienie sprowadza się do zapłacenia wyższego kosztu w jednym wątku ( malloc
) w porównaniu do zapłacenia tańszego kosztu w jednym wątku, a następnie zapłacenia wyższego kosztu w innym wątku, który może działać równolegle ( GC
). Wadą robienia tego w ten sposób jest to, że potrzebujesz dwóch pośredników, aby uzyskać odniesienie od obiektu do obiektu, co jest wymagane, aby umożliwić alokatorowi kopiowanie / przenoszenie pamięci bez unieważniania istniejących odniesień do obiektu, a także możesz utracić lokalizację przestrzenną, gdy pamięć obiektu zostanie wyprowadził się z przestrzeni „Eden”.
I na koniec, porównanie jest nieco niesprawiedliwe, ponieważ kod C ++ zwykle nie przydziela dużej liczby obiektów indywidualnie na stercie. Przyzwoity kod C ++ ma tendencję do alokacji pamięci dla wielu elementów w sąsiadujących blokach lub na stosie. Jeśli przydziela ładunek małych obiektów pojedynczo w darmowym sklepie, kod jest gówniany.