Dwa główne argumenty przeciwko zastąpieniu Object.finalize()
są następujące:
Nie możesz zdecydować, kiedy to się nazywa.
Może w ogóle nie zostać wywołany.
Jeśli dobrze to rozumiem, nie sądzę, aby były to wystarczające powody, by Object.finalize()
tak bardzo nienawidzić .
Od implementacji maszyny wirtualnej i GC zależy, kiedy właściwy czas na zwolnienie obiektu, a nie programista. Dlaczego ważne jest, aby zdecydować, kiedy
Object.finalize()
zostaniesz wezwany?Normalnie i popraw mnie, jeśli się mylę, jedynym czasem,
Object.finalize()
gdy nie zostaniemy wywołani, jest zakończenie aplikacji, zanim GC ma szansę na uruchomienie. Jednak obiekt i tak został zwolniony, gdy proces aplikacji został z nim zakończony. WięcObject.finalize()
nie zadzwoniono, ponieważ nie trzeba było dzwonić. Dlaczego deweloper miałby się przejmować?
Za każdym razem, gdy używam obiektów, które muszę ręcznie zamykać (takich jak uchwyty plików i połączenia), jestem bardzo sfrustrowany. Muszę ciągle sprawdzać, czy obiekt ma implementację close()
, i jestem pewien, że w przeszłości w niektórych momentach do niego nie trafiłem. Dlaczego nie jest po prostu łatwiej i bezpieczniej pozostawić VM i GC pozbywanie się tych obiektów poprzez close()
wdrożenie Object.finalize()
?
źródło
finalize()
jest nieco pomieszana. Jeśli kiedykolwiek go zaimplementujesz, upewnij się, że jest bezpieczny dla wszystkich innych metod tego samego obiektu.Odpowiedzi:
Z mojego doświadczenia wynika, że istnieje jeden i jedyny powód zastąpienia
Object.finalize()
, ale jest to bardzo dobry powód :Analiza statyczna może wychwycić pominięcia tylko w trywialnych scenariuszach użycia, a ostrzeżenia kompilatora wspomniane w innej odpowiedzi mają tak uproszczony obraz rzeczy, że faktycznie trzeba je wyłączyć, aby zrobić wszystko, co nie jest trywialne. (Mam znacznie więcej ostrzeżeń niż jakikolwiek inny programista, o którym wiem lub kiedykolwiek słyszałem, ale nie mam włączonych głupich ostrzeżeń).
Finalizacja może wydawać się dobrym mechanizmem zapewniającym, że zasoby nie pozostaną niezdyscyplinowane, ale większość ludzi postrzega to w całkowicie niewłaściwy sposób: uważają to za alternatywny mechanizm awaryjny, zabezpieczenie „drugiej szansy”, które automatycznie uratuje dzień, pozbywając się zapomnianych zasobów. To jest bardzo złe . Musi istnieć tylko jeden sposób na zrobienie czegoś: albo zawsze zamykasz wszystko, albo finalizacja zawsze zamyka wszystko. Ale ponieważ finalizacja jest niewiarygodna, finalizacja nie może nią być.
Istnieje więc schemat, który nazywam obowiązkowym usuwaniem , i stanowi, że programista jest odpowiedzialny za zawsze jawne zamykanie wszystkiego, co implementuje
Closeable
lubAutoCloseable
. (Instrukcja try-with-resources nadal liczy się jako wyraźne zamknięcie.) Oczywiście programista może zapomnieć, więc tutaj zaczyna się finalizacja, ale nie jako magiczna wróżka, która magicznie naprawi wszystko w końcu: jeśli finalizacja odkryje żeclose()
nie została wywołana, to jednak niepróbuj go przywołać, właśnie dlatego, że (z matematyczną pewnością) będą hordy programistów n00b, którzy będą na nim polegać, wykonując zadanie, za które byli zbyt leniwi lub zbyt nieumyślni. Tak więc, przy obowiązkowym usuwaniu, gdy finalizacja odkryje, żeclose()
nie został wywołany, rejestruje jasny czerwony komunikat o błędzie, informując programistę dużymi dużymi, dużymi literami, aby naprawił swoje ... jego rzeczy.Dodatkową korzyścią jest pogłoska, że „JVM zignoruje trywialną metodę finalize () (np. Taką, która wraca bez robienia niczego, jak ta zdefiniowana w klasie Object)”, więc przy obowiązkowym usuwaniu można uniknąć całej finalizacji narzut w całym systemie ( zobacz odpowiedź alip, aby dowiedzieć się, jak straszny jest ten narzut), kodując swoją
finalize()
metodę w następujący sposób:Chodzi o to, że
Global.DEBUG
jest tostatic final
zmienna, której wartość jest znana w czasie kompilacji, więc jeśli tak, tofalse
kompilator nie wyemituje żadnego kodu dla całejif
instrukcji, co sprawi, że będzie to trywialny (pusty) finalizator, który z kolei oznacza, że twoja klasa będzie traktowana tak, jakby nie miała finalizatora. (W języku C # byłoby to zrobione z ładnym#if DEBUG
blokiem, ale co możemy zrobić, to jest Java, w którym płacimy pozorną prostotę kodu z dodatkowym obciążeniem mózgu).Więcej informacji na temat obowiązkowego usuwania, wraz z dodatkową dyskusją na temat usuwania zasobów w dot Net, tutaj: michael.gr: Obowiązkowe usuwanie kontra obrzydliwość „pozbywać się”
źródło
close()
, na wszelki wypadek. Uważam, że jeśli moim testom nie można zaufać, lepiej nie dopuszczać systemu do produkcji.if( Global.DEBUG && ...
konstrukt działał, więc JVM zignoruje tęfinalize()
metodę jako trywialną,Global.DEBUG
musi być ustawiony w czasie kompilacji (w przeciwieństwie do wstrzykiwania itp.), Więc poniższe elementy będą martwym kodem. Wywołaniesuper.finalize()
poza blokiem if również wystarcza, aby JVM mógł potraktować go jako nietrywialny (niezależnie od wartościGlobal.DEBUG
, przynajmniej w HotSpot 1.8), nawet jeśli superklasa#finalize()
jest trywialna!java.io
. Jeśli tego rodzaju bezpieczeństwo wątków nie było na liście życzeń, zwiększa koszty ogólne wywołane przezfinalize()
…Ponieważ uchwyty plików i połączenia (to znaczy deskryptory plików w systemach Linux i POSIX) są dość rzadkim zasobem (możesz być ograniczony do 256 z nich w niektórych systemach lub do 16384 w innych; patrz setrlimit (2) ). Nie ma gwarancji, że GC będzie wywoływana wystarczająco często (lub we właściwym czasie), aby uniknąć wyczerpania tak ograniczonych zasobów. A jeśli GC nie zostanie wystarczająco wywołane (lub finalizacja nie zostanie uruchomiona we właściwym czasie), osiągniesz ten (możliwie niski) limit.
Finalizacja jest sprawą „najlepszego wysiłku” w JVM. Może nie zostać wywołany lub wywołany dość późno ... W szczególności, jeśli masz dużo pamięci RAM lub jeśli twój program nie przydziela dużej liczby obiektów (lub jeśli większość z nich umiera, zanim zostanie przekierowana do wystarczająco starego) generowanie przez kopiowanie generacyjnej GC), GC może być wywoływana dość rzadko, a finalizacje mogą nie być uruchamiane zbyt często (lub nawet wcale).
Więc
close
deskryptory plików jawnie, jeśli to możliwe. Jeśli boisz się ich wyciekać, użyj finalizacji jako dodatkowego środka, a nie głównego.źródło
Spójrz na to w ten sposób: powinieneś pisać tylko kod, który jest (a) poprawny (w przeciwnym razie twój program jest po prostu zły) i (b) konieczny (w przeciwnym razie twój kod jest zbyt duży, co oznacza więcej pamięci RAM, więcej cykli poświęconych na rzeczy bezużyteczne, więcej wysiłku, aby je zrozumieć, więcej czasu poświęconego na utrzymanie go itp.)
Teraz zastanów się, co chcesz zrobić w finalizatorze. Albo to konieczne. W takim przypadku nie można umieścić go w finalizatorze, ponieważ nie wiadomo, czy zostanie on wywołany. To nie jest wystarczająco dobre. Albo to nie jest konieczne - nie powinieneś pisać tego w pierwszej kolejności! Tak czy inaczej, włożenie go do finalizatora jest złym wyborem.
(Pamiętaj, że przykłady, które podajesz, takie jak zamykanie strumieni plików, wyglądają tak, jakby nie były tak naprawdę konieczne, ale są. Chodzi o to, że dopóki nie przekroczysz limitu otwartych uchwytów plików w systemie, nie zauważysz, że Twój kod jest niepoprawny. Ale ten limit jest cechą systemu operacyjnego i dlatego jest jeszcze bardziej nieprzewidywalny niż polityka JVM dotycząca finalizatorów, więc naprawdę bardzo ważne jest , aby nie marnować uchwytów plików.)
źródło
finalize()
go zamknąć, na wypadek gdyby cykl gc naprawdę musiał zwolnić BARAN. W przeciwnym razie trzymałbym obiekt w pamięci RAM zamiast konieczności jego ponownego generowania i zamykania za każdym razem, gdy muszę go użyć. Pewnie, zasoby, które otworzy, nie zostaną uwolnione, dopóki obiekt nie zostanie GCed, w każdym przypadku, ale może nie być konieczne zagwarantowanie, że moje zasoby zostaną uwolnione w określonym czasie.Jednym z największych powodów, dla których nie należy polegać na finalizatorach, jest to, że większość zasobów, które można by pokusić o oczyszczenie w finalizatorze, jest bardzo ograniczona. Moduł odśmiecania działa tylko tak często, ponieważ przeglądanie referencji w celu ustalenia, czy coś można zwolnić, jest drogie. Oznacza to, że może minąć trochę czasu, zanim obiekty zostaną zniszczone. Jeśli masz wiele obiektów otwierających krótkotrwałe połączenia z bazą danych, na przykład pozostawienie finalizatorów do wyczyszczenia te połączenia mogą wyczerpać pulę połączeń, gdy czeka on na śmieciarza, aby w końcu uruchomić i zwolnić ukończone połączenia. Następnie, z powodu oczekiwania, kończy się duża zaległość żądań w kolejce, które szybko wyczerpują pulę połączeń. To'
Dodatkowo użycie try-with-resources sprawia, że zamykanie obiektów „zamykalnych” po zakończeniu jest łatwe. Jeśli nie znasz tego konstruktu, proponuję go sprawdzić: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
źródło
Oprócz tego, dlaczego pozostawienie finalizatorowi zwolnienia zasobów jest generalnie złym pomysłem, obiekty finalizowalne wiążą się z narzutem wydajności.
Z teorii i praktyki Java: Odśmiecanie i wydajność (Brian Goetz), finalizatory nie są twoim przyjacielem :
źródło
finalize()
.Moim (najmniej) ulubionym powodem unikania
Object.finalize
nie jest to, że obiekty mogą zostać sfinalizowane po ich oczekiwaniu, ale mogą zostać sfinalizowane, zanim możesz się tego spodziewać. Problemem nie jest to, że obiekt, który jest nadal w zasięgu, może zostać sfinalizowany przed opuszczeniem zakresu, jeśli Java zdecyduje, że nie jest on już osiągalny.Zobacz to pytanie, aby uzyskać więcej informacji. Jeszcze bardziej zabawne jest to, że ta decyzja może zostać podjęta dopiero po uruchomieniu optymalizacji punktu szybkiego, co sprawia, że debugowanie jest bolesne.
źródło
HasFinalize.stream
sam powinien być przedmiotem, który można sfinalizować oddzielnie. Oznacza to, że finalizacjaHasFinalize
nie powinna się kończyć ani próbować posprzątaćstream
. A jeśli tak, to powinno stać sięstream
niedostępne.W Eclipse pojawia się ostrzeżenie, gdy zapomnę zamknąć coś, co implementuje
Closeable
/AutoCloseable
. Nie jestem pewien, czy jest to kwestia Eclipse, czy też jest to część oficjalnego kompilatora, ale możesz rozważyć użycie podobnych narzędzi do analizy statycznej, które Ci w tym pomogą. Na przykład FindBugs prawdopodobnie pomoże ci sprawdzić, czy zapomniałeś zamknąć zasób.źródło
AutoCloseable
. Ułatwia zarządzanie zasobami za pomocą try-with-resources. To unieważnia kilka argumentów w pytaniu.Na twoje pierwsze pytanie:
Cóż, JVM określi, kiedy należy odzyskać pamięć przydzieloną do obiektu. Nie musi to być czas
finalize()
, w którym powinno nastąpić czyszczenie zasobu, w którym chcesz wykonać . Jest to zilustrowane w pytaniu „finalize () wywołanym silnie osiągalnym obiekcie w Javie 8” na SO. Tamclose()
metoda została wywołana przezfinalize()
metodę, podczas gdy próba odczytu ze strumienia przez ten sam obiekt jest nadal w toku. Oprócz dobrze znanej możliwości, którafinalize()
zostaje wywołana do późna, istnieje możliwość, że zostanie wywołana zbyt wcześnie.Przesłanka twojego drugiego pytania:
jest po prostu zły. JVM nie musi w ogóle obsługiwać finalizacji. Cóż, nie jest to całkowicie błędne, ponieważ nadal można interpretować to jako „wniosek został zakończony przed finalizacją”, przy założeniu, że wniosek kiedykolwiek się zakończy.
Zwróć jednak uwagę na niewielką różnicę między „GC” w oryginalnym oświadczeniu a terminem „finalizacja”. Odśmiecanie jest odrębne od finalizacji. Gdy zarządzanie pamięcią wykryje, że obiekt jest nieosiągalny, może po prostu odzyskać miejsce, jeśli albo nie ma specjalnej
finalize()
metody, albo finalizacja jest po prostu nieobsługiwana lub może kolejkować obiekt do finalizacji. Zatem zakończenie cyklu wyrzucania elementów bezużytecznych nie oznacza, że finalizatory są wykonywane. Może się to zdarzyć w późniejszym czasie, gdy kolejka jest przetwarzana lub wcale.Ten punkt jest również powodem, dla którego nawet w maszynach JVM ze wsparciem finalizacji korzystanie z czyszczenia zasobów jest niebezpieczne. Odśmiecanie jest częścią zarządzania pamięcią i dlatego jest wyzwalane potrzebami pamięci. Możliwe, że wyrzucanie elementów bezużytecznych nigdy się nie uruchomi, ponieważ podczas całego środowiska wykonawczego jest wystarczająca ilość pamięci (cóż, to nadal pasuje do opisu „aplikacja została zakończona zanim GC dostała szansę na uruchomienie”, jakoś). Możliwe jest również, że GC działa, ale potem odzyskano wystarczającą ilość pamięci, więc kolejka finalizatora nie jest przetwarzana.
Innymi słowy, natywne zasoby zarządzane w ten sposób są nadal obce w zarządzaniu pamięcią. Chociaż gwarantowane jest, że to
OutOfMemoryError
jest rzucane tylko po wystarczających próbach zwolnienia pamięci, nie dotyczy to rodzimych zasobów i finalizacji. Możliwe, że otwarcie pliku nie powiedzie się z powodu niewystarczających zasobów, podczas gdy kolejka finalizacji jest pełna obiektów, które mogłyby zwolnić te zasoby, gdyby finalizator kiedykolwiek działał…źródło