Dlaczego obiekty Java nie są usuwane natychmiast po ich odwołaniu?

77

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?

moonman239
źródło
46
Nie powinieneś się przejmować, kiedy obiekty Java zostaną faktycznie usunięte. Jest to szczegół implementacji.
Basile Starynkevitch,
154
@BasileStarynkevitch Powinieneś absolutnie dbać i kwestionować działanie twojego systemu / platformy. Zadawanie pytań „jak” i „dlaczego” jest jednym z najlepszych sposobów, aby stać się lepszym programistą (i, mówiąc bardziej ogólnie, mądrzejszą osobą).
Artur Biesiadowski
6
Co robi Cel C, gdy istnieją odwołania cykliczne? Zakładam, że to po prostu przecieka?
Mehrdad
45
@ArturBiesiadowksi: Nie, specyfikacja Java nie informuje, kiedy obiekt zostanie usunięty (i podobnie w przypadku R5RS ). Możesz i prawdopodobnie powinieneś opracować swój program Java, tak jakby to nigdy się nie zdarzyło (a w przypadku krótkotrwałych procesów, takich jak świat hello Java, tak naprawdę nie ma miejsca). Możesz przejmować się zestawem żywych obiektów (lub zużyciem pamięci), co jest inną historią.
Basile Starynkevitch,
28
Pewnego dnia nowicjusz powiedział do mistrza: „Mam rozwiązanie naszego problemu z alokacją. Dajemy każdemu alokacji liczbę referencyjną, a gdy osiągnie zero, możemy usunąć obiekt”. Mistrz odpowiedział „Pewnego dnia nowicjusz powiedział do mistrza:„ Mam rozwiązanie ...
Eric Lippert,

Odpowiedzi:

79

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.

Karl Bielefeldt
źródło
58
Żona programisty wysyła go do supermarketu. Mówi mu: „Kup bochenek chleba, a jeśli zobaczysz jajka, weź tuzin”. Później programista powraca z tuzinem bochenków chleba pod pachą.
Neil
7
Sugeruję wspomnieć, że czas nowej generacji gc jest zasadniczo proporcjonalny do ilości obiektów aktywnych , więc posiadanie większej liczby usuniętych obiektów oznacza, że ​​w wielu przypadkach ich koszt nie zostanie w ogóle zapłacony. Usunięcie jest tak proste, jak odwrócenie wskaźnika przestrzeni ocalałej i opcjonalne wyzerowanie całej przestrzeni pamięci w jednym dużym zestawie (nie jestem pewien, czy jest to wykonywane na końcu gc, czy amortyzowane podczas alokacji płyt lub obiektów w bieżących plikach Jvms)
Artur Biesiadowski
64
@Neil nie powinno to być 13 chlebów?
JAD
67
„Wyłączone jednym błędem w przejściu 7”
joeytwiddle
13
@JAD Powiedziałbym, że 13, ale większość nie ma tego. ;)
Neil
86

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ść.

Jaka jest nazwa?
źródło
7
Lub możesz zastosować podejście w języku Python, w którym używasz liczenia w jak największym stopniu, uciekając się do GC, gdy spodziewasz się, że zależności cykliczne przeciekają pamięć. Nie rozumiem, dlaczego nie mogli mieć liczenia oprócz GC?
Mehrdad
27
@ Mehrdad Oni mogli. Ale prawdopodobnie będzie wolniej. Nic nie stoi na przeszkodzie, aby to wdrożyć, ale nie spodziewaj się, że pokonasz GC w Hotspot lub OpenJ9.
Josef
21
@ jpmc26, ponieważ jeśli usuniesz obiekty, gdy tylko nie będą już używane, istnieje duże prawdopodobieństwo, że usuniesz je w sytuacji dużego obciążenia, co jeszcze bardziej zwiększa obciążenie. GC może działać, gdy obciążenie jest mniejsze. Samo liczenie referencji jest niewielkim narzutem dla każdego odniesienia. Również za pomocą GC często możesz odrzucić dużą część pamięci bez odniesień bez obsługi pojedynczych obiektów.
Josef
33
@Josef: prawidłowe liczenie referencji też nie jest darmowe; aktualizacja liczby referencji wymaga inkrementacji / dekompresji atomowych, które są zaskakująco kosztowne , szczególnie w przypadku nowoczesnych architektur wielordzeniowych. W CPython nie stanowi większego problemu (CPython sam w sobie jest bardzo wolny, a GIL ogranicza swoją wydajność wielowątkową do poziomów jednordzeniowych), ale w szybszym języku obsługującym równoległość może być problemem. To nie jest szansa, że ​​PyPy całkowicie pozbywa się liczenia referencji i po prostu używa GC.
Matteo Italia,
10
@ Mehrdad po zaimplementowaniu licznika referencyjnego GC dla Javy chętnie go przetestuję, aby znaleźć przypadek, w którym działa gorzej niż jakakolwiek inna implementacja GC.
Josef
45

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:

Magazyn stosów dla obiektów jest odzyskiwany przez automatyczny system zarządzania pamięcią (znany jako kolektor śmieci); obiekty nigdy nie są jawnie zwolnione. Wirtualna maszyna Java nie zakłada żadnego konkretnego rodzaju automatycznego systemu zarządzania pamięcią masową

(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 mallocdla 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 ).

Dlaczego obiekty Java nie są usuwane natychmiast po ich odwołaniu?

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).

Basile Starynkevitch
źródło
20
„możliwe, że naiwna JVM, która nigdy nie usuwa obiektów i nie zwalnia pamięci, może być zgodna ze specyfikacjami” Z pewnością jest zgodna ze specyfikacją! Java 11 w rzeczywistości dodaje bezużyteczny moduł wyrzucania elementów bezużytecznych dla między innymi bardzo krótkotrwałych programów.
Michael
6
„nie należy się przejmować usunięciem obiektu” Nie zgadzam się. Po pierwsze, powinieneś wiedzieć, że RAII nie jest już wykonalnym wzorcem i że nie możesz polegać na finalizeżadnym zarządzaniu zasobami (uchwytów plików, połączeń db, zasobów GPU itp.).
Alexander
4
@Michael To ma sens w przypadku przetwarzania wsadowego przy użyciu sufitu pamięci. System operacyjny może po prostu powiedzieć „cała pamięć używana przez ten program już zniknęła!” w końcu jest to dość szybkie. Rzeczywiście, wiele programów w C zostało napisanych w ten sposób, szczególnie we wczesnym świecie uniksowym. Pascal miał pięknie okropne „zresetowanie wskaźnika stosu / sterty do wcześniej zapisanego punktu kontrolnego”, które pozwoliło ci zrobić dokładnie to samo, chociaż było to dość niebezpieczne - zaznacz, uruchom pod zadanie, zresetuj.
Luaan,
6
@Alexander ogólnie poza C ++ (i kilkoma językami, które celowo z niego wywodzą), zakładając, że RAII będzie działać w oparciu o same finalizatory, jest anty-wzorcem, przed którym należy ostrzec i zastąpić go jawnym blokiem kontroli zasobów. Cały sens GC polega na tym, że w końcu życie i zasoby są oddzielone.
Leushenko
3
@Leushenko zdecydowanie nie zgadzam się, że „życie i zasoby są oddzielone” to „cały punkt” GC. Jest to cena ujemna, którą płacisz za główny punkt GC: łatwe, bezpieczne zarządzanie pamięcią. „zakładanie, że RAII będzie działać w oparciu o same finalizatory, jest anty-wzorcem” W Javie? Być może. Ale nie w CPython, Rust, Swift ani Celu C ”ostrzegał i zastępował go wyraźnym blokiem kontroli zasobów„ Nie, są one ściśle ograniczone. Obiekt, który zarządza zasobem za pośrednictwem RAII, daje ci uchwyt do przekazywania życia o określonym zasięgu. Blok try-with-resource jest ograniczony do jednego zakresu.
Alexander
23

Aby użyć terminologii Objective-C, wszystkie odwołania do Java są z natury „silne”.

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.

W Objective-C, jeśli obiekt nie ma już żadnych silnych odniesień, obiekt jest natychmiast usuwany.

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:

  • Musisz zwiększać lub zmniejszać licznik za każdym razem, gdy odwołanie jest kopiowane, modyfikowane lub wykracza poza zakres, co wiąże się z oczywistym obciążeniem wydajności.
  • ARC nie może łatwo wyczyścić cyklicznych odniesień, ponieważ wszystkie mają odniesienia do siebie, dlatego ich liczba odniesienia nigdy nie osiąga zera.

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.

berry120
źródło
1
Zabawne jest to, że Apple przeszedł na ARC właśnie dlatego, że zobaczył, że w praktyce znacznie przewyższa inne GC (zwłaszcza generacyjne). Mówiąc szczerze, dotyczy to głównie platform o ograniczonej pamięci (iPhone). Ale przeciwstawiłbym się twojemu stwierdzeniu, że „ARC nie jest tak utopijna, jak się wydaje”, mówiąc, że pokoleniowe (i inne niedeterministyczne) GC nie są tak utopijne, jak się wydaje: zniszczenie deterministyczne jest prawdopodobnie lepszą opcją w zdecydowana większość scenariuszy.
Konrad Rudolph,
3
@KonradRudolph, chociaż jestem także fanem deterministycznego zniszczenia, nie sądzę, aby „lepsza opcja w zdecydowanej większości scenariuszy” się utrzymywała. Jest to z pewnością lepsza opcja, gdy opóźnienie lub pamięć są ważniejsze niż średnia przepustowość, a zwłaszcza gdy logika jest dość prosta. Ale to nie tak, że nie ma wielu złożonych aplikacji, które wymagają wielu cyklicznych odniesień itp. I wymagają szybkiej średniej operacji, ale tak naprawdę nie dbają o opóźnienia i mają dużo dostępnej pamięci. W przypadku tych wątpliwości wątpliwe jest, czy ARC jest dobrym pomysłem.
lewo około
1
@leftaroundabout W „większości scenariuszy” ani przepustowość, ani pamięć nie stanowią wąskiego gardła, więc nie ma to znaczenia. Twój przykład to jeden konkretny scenariusz. To prawda, że ​​nie jest to niezwykle rzadkie, ale nie posunąłbym się nawet do twierdzenia, że ​​jest to bardziej powszechne niż w innych scenariuszach, w których ARC jest bardziej odpowiedni. Co więcej, ARC dobrze sobie radzi z cyklami. Wymaga jedynie prostej, ręcznej interwencji programisty. To sprawia, że ​​jest mniej idealny, ale nie stanowi przełomu. Uważam, że deterministyczna finalizacja jest o wiele ważniejszą cechą niż się wydaje.
Konrad Rudolph,
3
@KonradRudolph Jeśli ARC wymaga prostej ręcznej interwencji programisty, to nie radzi sobie z cyklami. Jeśli zaczniesz intensywnie używać podwójnie połączonych list, ARC przechodzi w ręczne przydzielanie pamięci. Jeśli masz duże dowolne wykresy, ARC zmusza cię do napisania śmieciarza. Argument GC byłby taki, że zasoby, które wymagają zniszczenia, nie są zadaniem podsystemu pamięci, a aby śledzić stosunkowo niewielką ich liczbę, powinny one zostać deterministycznie sfinalizowane poprzez jakąś prostą ręczną interwencję programisty.
prosfilaes
2
@KonradRudolph ARC i cykle zasadniczo prowadzą do wycieków pamięci, jeśli nie są obsługiwane ręcznie. W wystarczająco skomplikowanych systemach mogą wystąpić poważne wycieki, jeśli np. Jakiś obiekt przechowywany na mapie przechowuje odniesienie do tej mapy, co może zostać wprowadzone przez programistę nie odpowiedzialnego za sekcje kodu tworzące i niszczącą tę mapę. Duże dowolne wykresy nie oznaczają, że wewnętrzne wskaźniki nie są mocne, że elementy powiązane z nimi mogą zniknąć. Czy poradzenie sobie z niektórymi wyciekami pamięci jest mniejszym problemem niż ręczne zamykanie plików, nie powiem, ale to prawda.
prosfilaes
5

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.

Cort Ammon
źródło
4

Chociaż finalizezostał 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:

  1. Unieważnij słabe odniesienia, które identyfikują obiekty, z którymi nie są powiązane żadne łatwo dostępne odwołania.

  2. 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ń.

  3. 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.

supercat
źródło
5
W rzeczywistości gc ma tylko jeden cel: symulowanie nieskończonej pamięci. Wszystko, co określiłeś jako cel, jest albo niedoskonałością abstrakcji, albo szczegółem implementacji.
Deduplicator
@Deduplicator: Słabe referencje oferują przydatną semantykę, której nie można osiągnąć bez pomocy GC.
supercat,
Jasne, słabe referencje mają przydatną semantykę. Ale czy ta semantyka byłaby potrzebna, gdyby symulacja była lepsza?
Deduplicator
@Deduplicator: Tak. Rozważ kolekcję, która określa, w jaki sposób aktualizacje będą oddziaływać z wyliczaniem. W takiej kolekcji może być konieczne przechowywanie słabych odniesień do dowolnych wyliczających na żywo. W systemie z nieograniczoną pamięcią wielokrotnie powtarzana kolekcja sprawiłaby, że lista zainteresowanych podmiotów wyliczających powiększyła się bez ograniczeń. Pamięć wymagana dla tej listy nie stanowiłaby problemu, ale czas potrzebny na jej iterację obniżyłby wydajność systemu. Dodanie GC może oznaczać różnicę między algorytmem O (N) i O (N ^ 2).
supercat,
2
Dlaczego chcesz powiadomić podmioty wyliczające, zamiast dołączać do listy i pozwalać im szukać siebie, gdy są używane? I każdy program w zależności od śmieci przetwarzanych w odpowiednim czasie zamiast w zależności od presji pamięci i tak żyje w stanie grzechu, jeśli w ogóle się porusza.
Deduplicator
4

Pozwól, że zasugeruję przeredagowanie i uogólnienie twojego pytania:

Dlaczego Java nie daje silnych gwarancji dotyczących procesu GC?

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 finalizemetoda, ale z różnych powodów, które są poza zakresem tej odpowiedzi, trudno jest polegać na niej.

yshavit
źródło
3

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ć.

gnasher729
źródło
W przypadku liczenia referencji finalizacja jest banalna, ponieważ programista zajął się cyklami. W przypadku gc cykle są banalne, ale programista musi zachować ostrożność przy finalizacji.
Deduplicator
@Deduplicator W Javie, jest to również możliwe, aby stworzyć silne odniesienia do obiektów finalizowane ... w Objective-C i Swift, gdy liczba referencyjna wynosi zero, obiekt będzie znikać (o ile można umieścić nieskończoną pętlę w dealloc / deist).
gnasher729
Właśnie zauważyłem głupią
funkcję
1
Jest powód, dla którego większość programistów nienawidzi automatycznej korekty pisowni ... ;-)
Deduplicator
lol ... Myślę, że świat jest podzielony 0,1 / 0,1 / 99,8 pomiędzy ludzi, którzy uważają, że dodawanie liczenia odwołań do odśmiecania jest złym pomysłem, a ludzie, którzy myślą, że dodawanie odśmiecania do liczenia odwołań jest złym pomysłem, a ludzie, którzy odliczaj dni, aż nadejdzie wywóz śmieci, ponieważ ta tona znów śmierdzi ...
leftaroundabout
1

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.

ford prefekt
źródło
1

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.

ostry
źródło
0

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.

Michael Durrant
źródło
Cóż, jestem pewien, że znaleźlibyśmy bardziej interesujące sposoby wykorzystania dodatkowych cykli niż to.
Deduplicator