W Javie, gdy tylko obiekt nie ma już żadnych odniesień, kwalifikuje się do usunięcia, ale JVM decyduje, kiedy obiekt zostanie faktycznie usunięty. Aby użyć terminologii Objective-C, wszystkie odwołania do Java są z natury „silne”. Jednak w Celu C, jeśli obiekt nie ma już żadnych silnych odniesień, obiekt jest natychmiast usuwany. Dlaczego tak nie jest w Javie?
java
garbage-collection
moonman239
źródło
źródło
Odpowiedzi:
Po pierwsze, Java ma słabe referencje i inną kategorię najlepszych starań, zwaną miękkimi referencjami. Słabe kontra mocne referencje to zupełnie odrębny problem od liczenia referencji a odśmiecania.
Po drugie, istnieją wzorce wykorzystania pamięci, które mogą sprawić, że zbieranie śmieci będzie bardziej wydajne w czasie, poświęcając miejsce. Na przykład nowsze obiekty są znacznie częściej usuwane niż starsze obiekty. Więc jeśli zaczekasz trochę między przeglądami, możesz usunąć większość nowej generacji pamięci, jednocześnie przenosząc kilku ocalałych do pamięci długoterminowej. To długoterminowe przechowywanie można skanować znacznie rzadziej. Natychmiastowe usunięcie poprzez ręczne zarządzanie pamięcią lub zliczanie referencji jest znacznie bardziej podatne na fragmentację.
To coś w rodzaju różnicy między pójściem na zakupy spożywcze raz na wypłatę, a pójściem każdego dnia, aby zdobyć tylko tyle jedzenia na jeden dzień. Twoja jedna wielka podróż potrwa o wiele dłużej niż pojedyncza krótka podróż, ale ogólnie możesz zaoszczędzić czas i pieniądze.
źródło
Ponieważ właściwe poznanie czegoś, do czego nie ma już odniesienia, nie jest łatwe. Nawet nie jest to łatwe.
Co jeśli masz dwa obiekty, które się do siebie odnoszą? Czy zostają na zawsze? Po rozszerzeniu tego sposobu myślenia na rozwiązanie dowolnej struktury danych, wkrótce zrozumiesz, dlaczego JVM lub inne śmieciarki są zmuszone stosować znacznie bardziej wyrafinowane metody określania, co jest nadal potrzebne i co może pójść.
źródło
AFAIK, specyfikacja JVM (napisana w języku angielskim) nie wspomina, kiedy dokładnie obiekt (lub wartość) powinna zostać usunięta, i pozostawia to do implementacji (podobnie dla R5RS ). W jakiś sposób wymaga lub sugeruje śmieciarz, ale pozostawia szczegóły do implementacji. Podobnie jest w przypadku specyfikacji Java.
Pamiętaj, że języki programowania to specyfikacje ( składni , semantyki itp.), A nie implementacje oprogramowania. Język taki jak Java (lub jego JVM) ma wiele implementacji. Jego specyfikacja jest opublikowana , dostępna do pobrania (abyś mógł ją przestudiować) i napisana w języku angielskim. §2.5.3 Sterta specyfikacji JVM wspomina o śmieciarzu:
(nacisk jest mój; finalizacja BTW jest wspomniana w § 12.6 specyfikacji Java, a model pamięci znajduje się w §17.4 specyfikacji Java)
Tak więc (w Javie) nie powinieneś się przejmować, kiedy obiekt zostanie usunięty , i możesz kodować tak, jakby tak się nie stało (rozumując w abstrakcji, w której to ignorujesz). Oczywiście musisz dbać o zużycie pamięci i zestaw żywych przedmiotów, co jest innym pytaniem. W kilku prostych przypadkach (pomyśl o programie „witaj świecie”) jesteś w stanie udowodnić - lub przekonać siebie - że przydzielona pamięć jest raczej niewielka (np. Mniejsza niż gigabajt), a wtedy nie przejmujesz się wcale usuwanie poszczególnych obiektów. W wielu przypadkach możesz przekonać się, że żywe przedmioty(lub osiągalne, które są nadzbiorem - łatwiejszym do uzasadnienia - żywe) nigdy nie przekraczają rozsądnego limitu (a wtedy polegasz na GC, ale nie obchodzi cię, jak i kiedy ma miejsce wywóz śmieci). Przeczytaj o złożoności przestrzeni .
Wydaje mi się, że w kilku implementacjach JVM uruchamiających krótkotrwały program Java, taki jak program hello world, moduł wyrzucania elementów nie jest w ogóle uruchamiany i nie jest usuwany. AFAIU, takie zachowanie jest zgodne z licznymi specyfikacjami Java.
Większość implementacji JVM wykorzystuje generacyjne techniki kopiowania (przynajmniej dla większości obiektów Java, tych, które nie używają finalizacji lub słabych referencji ; finalizacja nie jest gwarantowana w krótkim czasie i może zostać odroczona, więc jest to po prostu przydatna funkcja, której Twój kod nie powinien zależą w dużej mierze od), w którym pojęcie usuwania pojedynczego obiektu nie ma żadnego sensu (ponieważ duży blok stref pamięci zawierających wiele obiektów - być może kilka megabajtów naraz zostaje zwolniony).
Gdyby specyfikacja JVM wymagała jak najszybszego usunięcia każdego obiektu (lub po prostu nałożyła więcej ograniczeń na usuwanie obiektów), skuteczne techniki generowania GC byłyby zabronione, a projektanci Java i JVM mądrze tego unikali.
BTW, możliwe, że naiwna maszyna JVM, która nigdy nie usuwa obiektów i nie zwalnia pamięci, może być zgodna ze specyfikacjami (literą, a nie duchem) i na pewno jest w stanie uruchomić cześć świata w praktyce (zauważ, że większość małe i krótkotrwałe programy Java prawdopodobnie nie przydzielają więcej niż kilka gigabajtów pamięci). Oczywiście taka JVM nie jest warta wzmianki i jest tylko zabawką (podobnie jak ta implementacja
malloc
dla C). Zobacz Epsilon NoOp GC, aby uzyskać więcej. Rzeczywiste maszyny JVM są bardzo złożonymi programami i łączą kilka technik usuwania śmieci.Ponadto Java nie jest tym samym, co JVM, i masz implementacje Java działające bez JVM (np. Wyprzedzające kompilatory Java, środowisko wykonawcze Android ). W niektórych przypadkach (głównie akademickich) możesz sobie wyobrazić (tak zwane techniki „usuwania śmieci” w czasie kompilacji), że program Java nie alokuje ani nie usuwa w czasie wykonywania (np. Ponieważ kompilator optymalizujący jest wystarczająco sprytny, aby używać tylko stos wywołań i zmienne automatyczne ).
Ponieważ specyfikacje Java i JVM tego nie wymagają.
Więcej informacji znajdziesz w podręczniku GC (i specyfikacji JVM ). Zauważ, że bycie żywym (lub przydatnym do przyszłych obliczeń) dla obiektu jest właściwością całego programu (niemodularną).
Cel C preferuje podejście do zarządzania pamięcią z liczeniem referencji . Ma to również pułapki (np. Programista Objective-C musi dbać o referencje cykliczne , wyjaśniając słabe referencje, ale JVM dobrze radzi sobie z referencjami cyklicznymi w praktyce, bez konieczności zwracania uwagi przez programistę Java).
W programowaniu i projektowaniu języka programowania nie ma srebrnej kuli (należy pamiętać o problemie zatrzymania ; bycie użytecznym żywym przedmiotem jest ogólnie nierozstrzygalne ).
Możesz także przeczytać SICP , Pragmatics języka programowania , Dragon Book , Lisp In Small Pieces i systemy operacyjne: Three Easy Pieces . Nie dotyczą one Javy, ale otworzą ci umysł i powinny pomóc zrozumieć, co powinna zrobić JVM i jak może ona praktycznie działać (z innymi elementami) na twoim komputerze. Możesz również spędzić wiele miesięcy (lub kilka lat) na badaniu złożonego kodu źródłowego istniejących implementacji JVM typu open source (takich jak OpenJDK , który ma kilka milionów linii kodu źródłowego).
źródło
finalize
żadnym zarządzaniu zasobami (uchwytów plików, połączeń db, zasobów GPU itp.).To nie jest poprawne - Java ma zarówno słabe, jak i miękkie odniesienia, chociaż są one implementowane na poziomie obiektu, a nie jako słowa kluczowe języka.
To również niekoniecznie jest poprawne - niektóre wersje Celu C rzeczywiście używały generatora śmieci. Inne wersje w ogóle nie miały śmieci.
Prawdą jest, że nowsze wersje Celu C używają automatycznego zliczania referencji (ARC) zamiast GC opartego na śledzeniu, co (często) powoduje, że obiekt jest „usuwany”, gdy liczba referencji osiąga zero. Należy jednak pamiętać, że implementacja JVM może być również zgodna i działać dokładnie w ten sposób (do diabła, może być zgodna i w ogóle nie mieć GC).
Dlaczego więc większość implementacji JVM tego nie robi i zamiast tego używa algorytmów GC opartych na śledzeniu?
Krótko mówiąc, ARC nie jest tak utopijna, jak się wydaje:
ARC ma oczywiście zalety - jest łatwa do wdrożenia, a jej gromadzenie jest deterministyczne. Ale powyższe wady, między innymi, powodują, że większość implementacji JVM używa generacyjnej GC opartej na śledzeniu.
źródło
Java nie precyzuje dokładnie, kiedy obiekt zostanie zebrany, ponieważ daje to implementacjom swobodę wyboru sposobu obsługi odśmiecania.
Istnieje wiele różnych mechanizmów wyrzucania elementów bezużytecznych, ale te, które gwarantują, że można natychmiast zebrać obiekt, są prawie całkowicie oparte na liczeniu referencji (nie znam żadnego algorytmu, który łamie ten trend). Liczenie referencji jest potężnym narzędziem, ale wiąże się to z kosztem utrzymania liczby referencji. W kodzie z pojedynczym wątkiem jest to tylko przyrost i spadek, więc przypisanie wskaźnika może kosztować trzykrotnie więcej w kodzie zliczonym w referencji niż w kodzie zliczonym bez referencji (jeśli kompilator może upiec wszystko na maszynie kod).
W kodzie wielowątkowym koszt jest wyższy. Wymaga to przyrostów / spadków atomowych lub blokad, które mogą być kosztowne. W nowoczesnym procesorze operacja atomowa może być 20 razy droższa niż zwykła operacja rejestru (oczywiście różni się w zależności od procesora). Może to zwiększyć koszt.
Dzięki temu możemy rozważyć kompromisy dokonane przez kilka modeli.
Cel C koncentruje się na ARC - automatycznym zliczaniu referencji. Ich podejście polega na wykorzystaniu liczenia referencji do wszystkiego. Nie ma wykrycia cyklu (o którym wiem), więc programiści powinni zapobiegać występowaniu cykli, co kosztuje czas opracowywania. Ich teoria polega na tym, że wskaźniki nie są przypisywane tak często, a ich kompilator może identyfikować sytuacje, w których inkrementacja / dekrementacja referencji nie może spowodować śmierci obiektu, i całkowicie pomija te inkrementacje / dekrementacje. W ten sposób minimalizują koszty zliczania referencji.
CPython wykorzystuje mechanizm hybrydowy. Używają liczników referencyjnych, ale mają także moduł wyrzucający elementy bezużyteczne, który identyfikuje cykle i je zwalnia. Zapewnia to korzyści obu światów kosztem obu podejść. CPython musi zarówno utrzymywać liczbę referencji, jak irobić księgowość, aby wykryć cykle. CPython unika tego na dwa sposoby. Po pierwsze, CPython tak naprawdę nie jest w pełni wielowątkowy. Ma blokadę zwaną GIL, która ogranicza wielowątkowość. Oznacza to, że CPython może używać normalnych przyrostów / spadków zamiast atomowych, co jest znacznie szybsze. CPython jest również interpretowany, co oznacza, że operacje takie jak przypisanie do zmiennej wymagają już garści instrukcji, a nie tylko 1. Dodatkowy koszt wykonywania przyrostów / dekrecji, który jest wykonywany szybko w kodzie C, jest mniejszy, ponieważ „ już zapłaciłem ten koszt.
Java odrzuca podejście polegające na tym, by w ogóle nie gwarantować systemu zliczanego referencji. Rzeczywiście specyfikacja nie mówi nic o tym, jak zarządza się obiektami, poza tym, że będzie istniał automatyczny system zarządzania pamięcią masową. Jednak specyfikacja silnie wskazuje również na założenie, że będzie to śmiecie zbierane w sposób, który obsługuje cykle. Nie określając terminu wygaśnięcia obiektów, Java zyskuje swobodę korzystania z kolektorów, które nie tracą czasu na zwiększanie / zmniejszanie. Rzeczywiście, sprytne algorytmy, takie jak generatory śmieci, mogą nawet obsługiwać wiele prostych przypadków, nawet nie patrząc na dane, które są odzyskiwane (muszą tylko patrzeć na dane, do których wciąż się odwołuje).
Widzimy więc, że każdy z tych trzech musiał dokonać kompromisu. To, który kompromis jest najlepszy, zależy w dużej mierze od charakteru tego, w jaki sposób język ma być używany.
źródło
Chociaż
finalize
został oparty na GC Javy, zbieranie śmieci w jego rdzeniu nie jest zainteresowane martwymi przedmiotami, ale żywymi. W niektórych systemach GC (prawdopodobnie włączając niektóre implementacje Java) jedyną rzeczą odróżniającą wiązkę bitów, która reprezentuje obiekt od grupy pamięci, która nie jest używana do niczego, może być istnienie odniesień do tego pierwszego. Podczas gdy obiekty z finalizatorami są dodawane do specjalnej listy, inne obiekty mogą nie mieć nigdzie we wszechświecie niczego, co mówi, że ich przechowywanie jest powiązane z obiektem, z wyjątkiem odniesień przechowywanych w kodzie użytkownika. Kiedy ostatnie takie odniesienie zostanie zastąpione, wzór bitowy w pamięci natychmiast przestanie być rozpoznawany jako obiekt, niezależnie od tego, czy coś we wszechświecie jest tego świadome.Celem wyrzucania elementów bezużytecznych nie jest niszczenie obiektów, do których nie istnieją żadne odniesienia, ale raczej osiągnięcie trzech rzeczy:
Unieważnij słabe odniesienia, które identyfikują obiekty, z którymi nie są powiązane żadne łatwo dostępne odwołania.
Przeszukaj systemową listę obiektów za pomocą finalizatorów, aby sprawdzić, czy któryś z nich nie ma powiązanych z nimi łatwo dostępnych odniesień.
Zidentyfikuj i skonsoliduj obszary pamięci, które nie są używane przez żadne obiekty.
Zauważ, że głównym celem GC jest # 3, a im dłużej czeka się na to, tym więcej szans na konsolidację będzie miało. Rozsądne jest wykonanie czynności nr 3 w przypadkach, w których można by natychmiast wykorzystać pamięć, ale w przeciwnym razie bardziej sensowne jest odroczenie jej.
źródło
Pozwól, że zasugeruję przeredagowanie i uogólnienie twojego pytania:
Mając to na uwadze, szybko przewiń odpowiedzi tutaj. Do tej pory jest ich siedem (nie licząc tego), z kilkoma wątkami komentarzy.
To twoja odpowiedź.
GC jest trudne. Istnieje wiele rozważań, wiele różnych kompromisów, a ostatecznie wiele bardzo różnych podejść. Niektóre z tych podejść pozwalają na wykonanie GC obiektu, gdy tylko nie jest on potrzebny; inni nie. Utrzymując swobodę umowy, Java daje swoim implementatorom więcej opcji.
Oczywiście nawet w tej decyzji występuje kompromis: utrzymując luźność kontraktu, Java w większości * odbiera programistom możliwość polegania na niszczycielach. Jest to coś, czego szczególnie programiści C ++ często pomijają ([potrzebne źródło];)), więc nie jest to nieznaczny kompromis. Nie widziałem dyskusji na temat tej konkretnej meta-decyzji, ale prawdopodobnie ludzie z Javy zdecydowali, że korzyści wynikające z posiadania większej liczby opcji GC przewyższają korzyści wynikające z możliwości informowania programistów dokładnie, kiedy obiekt zostanie zniszczony.
* Istnieje
finalize
metoda, ale z różnych powodów, które są poza zakresem tej odpowiedzi, trudno jest polegać na niej.źródło
Istnieją dwie różne strategie obsługi pamięci bez wyraźnego kodu napisanego przez programistę: Odśmiecanie i liczenie referencji.
Zaletą śmieciarek jest to, że „działa”, chyba że programista zrobi coś głupiego. Dzięki liczeniu referencji możesz mieć cykle referencyjne, co oznacza, że „działa”, ale programista czasami musi być sprytny. To więc plus za odśmiecanie.
Dzięki liczeniu referencji obiekt natychmiast znika, gdy liczba referencji spada do zera. To zaleta przy liczeniu referencji.
Speedwise, zbieranie śmieci jest szybsze, jeśli uważasz, że fani zbierania śmieci, a liczenie referencji jest szybsze, jeśli uważasz, że fani liczenia referencji.
To tylko dwie różne metody, aby osiągnąć ten sam cel, Java wybrała jedną metodę, Objective-C wybrała inną (i dodała wiele obsługi kompilatora, aby zmienić ją z bolesnego w dupę na coś, co jest mało pracy dla programistów).
Zmiana Javy z odśmiecania pamięci na liczenie referencji byłaby dużym przedsięwzięciem, ponieważ konieczne byłoby wiele zmian w kodzie.
Teoretycznie Java mogłaby zaimplementować kombinację wyrzucania elementów bezużytecznych i liczenia referencji: jeśli liczba referencji wynosi 0, to obiekt jest nieosiągalny, ale niekoniecznie na odwrót. Więc mógł zachować liczby odniesień i usuwanie obiektów, gdy ich liczba odniesienia jest zero (a następnie uruchomić zbieranie śmieci od czasu do czasu, aby złapać obiektów w niedostępnych cykle odniesienia). Myślę, że świat jest podzielony w proporcjach 50/50 u ludzi, którzy uważają, że dodawanie liczenia referencji do odśmiecania jest złym pomysłem, a ludzie, którzy uważają, że dodawanie odśmiecania do liczenia referencji jest złym pomysłem. Tak się nie stanie.
Tak więc Java może natychmiast usuwać obiekty, jeśli ich liczba odniesień wyniesie zero, i usuwać obiekty w nieosiągalnych cyklach później. Ale to decyzja projektowa, a Java zdecydowała się tego nie robić.
źródło
Wszystkie pozostałe argumenty dotyczące wydajności i dyskusje na temat trudności w zrozumieniu, gdy nie ma już odwołań do obiektu, są poprawne, chociaż jednym z pomysłów, o którym myślę, że warto wspomnieć, jest to, że istnieje co najmniej jedna maszyna JVM (azul), która bierze pod uwagę coś takiego w tym, że implementuje równoległy gc, który zasadniczo ma wątek vm stale sprawdzający odniesienia, aby spróbować je usunąć, co nie będzie działało zupełnie inaczej w stosunku do tego, o czym mówisz. Zasadniczo będzie stale rozglądał się po sterty i próbował odzyskać pamięć, do której nie ma odniesienia. Powoduje to bardzo niewielki koszt wydajności, ale prowadzi do zasadniczo zerowego lub bardzo krótkiego czasu GC. (To znaczy, chyba że stale rosnąca wielkość sterty przekracza systemową pamięć RAM, a potem Azul się zdezorientuje i pojawią się smoki)
TLDR Coś takiego istnieje dla JVM, jest to po prostu specjalny JVM i ma wady, jak każdy inny kompromis inżynieryjny.
Oświadczenie: Nie mam żadnych powiązań z Azulem, którego użyliśmy podczas poprzedniej pracy.
źródło
Maksymalizowanie trwałej przepustowości lub minimalizowanie opóźnień gc podlega dynamicznemu napięciu, co jest prawdopodobnie najczęstszym powodem, dla którego GC nie występuje natychmiast. W niektórych systemach, takich jak aplikacje alarmowe 911, niespełnienie określonego progu opóźnienia może rozpocząć wyzwalanie procesów awaryjnych w witrynie. W innych, takich jak strona bankowa i / lub arbitrażowa, znacznie ważniejsze jest maksymalizowanie przepustowości.
źródło
Prędkość
To wszystko dzieje się ostatecznie z powodu szybkości. Jeśli procesory były nieskończenie szybkie lub (aby być praktycznym) blisko niego, np. 1 000 000 000 000 000 000 000 000 000 000 000 000 operacji na sekundę, możesz mieć niesamowicie długie i skomplikowane rzeczy między każdym operatorem, takie jak upewnienie się, że usuwane są odnośniki do obiektów. Ponieważ ta liczba operacji na sekundę nie jest obecnie prawdą, a ponieważ większość innych odpowiedzi wyjaśnia, że jest to naprawdę skomplikowane i zasobochłonne, aby to ustalić, istnieje odśmiecanie, aby programy mogły skupić się na tym, co faktycznie próbują osiągnąć w szybki sposób.
źródło