Czy przesłanianie Object.finalize () jest naprawdę złe?

34

Dwa główne argumenty przeciwko zastąpieniu Object.finalize()są następujące:

  1. Nie możesz zdecydować, kiedy to się nazywa.

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

  1. 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?

  2. 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ęc Object.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()?

AxiomaticNexus
źródło
1
Uwaga: podobnie jak wiele interfejsów API z ery Java 1.0, semantyka wątków finalize()jest nieco pomieszana. Jeśli kiedykolwiek go zaimplementujesz, upewnij się, że jest bezpieczny dla wszystkich innych metod tego samego obiektu.
billc.cn
2
Kiedy słyszysz, jak ludzie mówią, że finaliści są źli, nie oznacza to, że twój program przestanie działać, jeśli je masz; oznaczają, że cała idea finalizacji jest całkiem bezużyteczna.
user253751,
1
+1 do tego pytania. Większość poniższych odpowiedzi stanowi, że zasoby takie jak deskryptory plików są ograniczone i że należy je ręcznie zbierać. To samo dotyczy / dotyczy pamięci, więc jeśli zaakceptujemy pewne opóźnienie w gromadzeniu pamięci, dlaczego nie zaakceptować go dla deskryptorów plików i / lub innych zasobów?
mbonnin
Odnosząc się do ostatniego akapitu, możesz pozostawić Javie zajmowanie się zamykaniem rzeczy, takich jak uchwyty plików i połączenia, przy niewielkim wysiłku z twojej strony. Użyj bloku try-with-resources - wspomniano go już kilka razy w odpowiedziach i komentarzach, ale myślę, że warto go tutaj umieścić. Samouczek Oracle na ten temat można znaleźć na stronie docs.oracle.com/javase/tutorial/essential/exceptions/…
Jeutnarg

Odpowiedzi:

45

Z mojego doświadczenia wynika, że ​​istnieje jeden i jedyny powód zastąpienia Object.finalize(), ale jest to bardzo dobry powód :

Aby wstawić kod rejestrujący błędy, w finalize()którym powiadamia Cię, jeśli zapomnisz wywołać close().

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 Closeablelub AutoCloseable. (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 że close()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, że close()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:

@Override
protected void finalize() throws Throwable
{
    if( Global.DEBUG && !closed )
    {
        Log.Error( "FORGOT TO CLOSE THIS!" );
    }
    //super.finalize(); see alip's comment on why this should not be invoked.
}

Chodzi o to, że Global.DEBUGjest to static finalzmienna, której wartość jest znana w czasie kompilacji, więc jeśli tak, to falsekompilator nie wyemituje żadnego kodu dla całej ifinstrukcji, 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 DEBUGblokiem, 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ę”

Mike Nakis
źródło
2
@MikeNakis Nie zapominaj, że Closeable jest zdefiniowane jako brak działania, jeśli zostanie wywołany po raz drugi: docs.oracle.com/javase/7/docs/api/java/io/Closeable.html . Przyznaję, że czasami logowałem ostrzeżenie, kiedy moje zajęcia były dwukrotnie zamknięte, ale technicznie nawet nie powinieneś tego robić. Technicznie jednak wywołanie .close () na Closable więcej niż raz jest całkowicie poprawne.
Patrick M,
1
@usr wszystko sprowadza się do tego, czy ufasz testom, czy nie. Jeśli nie ufasz swoje badania, pewnie, idź i cierpią narzutu do finalizacji też close() , na wszelki wypadek. Uważam, że jeśli moim testom nie można zaufać, lepiej nie dopuszczać systemu do produkcji.
Mike Nakis,
3
@Mike, aby if( Global.DEBUG && ...konstrukt działał, więc JVM zignoruje tę finalize()metodę jako trywialną, Global.DEBUGmusi być ustawiony w czasie kompilacji (w przeciwieństwie do wstrzykiwania itp.), Więc poniższe elementy będą martwym kodem. Wywołanie super.finalize()poza blokiem if również wystarcza, aby JVM mógł potraktować go jako nietrywialny (niezależnie od wartości Global.DEBUG, przynajmniej w HotSpot 1.8), nawet jeśli superklasa #finalize()jest trywialna!
alip
1
@Mike, obawiam się, że tak jest. Przetestowałem to za pomocą (nieco zmodyfikowanej wersji) testu w artykule, do którego się odsyłasz , i pełne dane wyjściowe GC (wraz z zaskakująco słabą wydajnością) potwierdzają, że obiekty są kopiowane do przestrzeni ocalałej / starej generacji i potrzebują pełnej stosu GC, aby uzyskać pozbyć się.
alip
1
Często pomijane jest ryzyko wcześniejszego gromadzenia obiektów, co czyni uwalnianie zasobów w finalizatorze bardzo niebezpiecznym działaniem. W wersjach wcześniejszych niż Java 9 jedynym sposobem upewnienia się, że finalizator nie zamknie zasobu, gdy jest on nadal używany, jest zsynchronizowanie obiektu, zarówno finalizatora, jak i metody wykorzystującej zasób. Dlatego to działa 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 przez finalize()
Holger
28

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. [...] Dlaczego nie jest po prostu prostsze i bezpieczniejsze pozostawienie VM i GC pozbywania się tych obiektów poprzez close()wdrożenie Object.finalize()?

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 closedeskryptory plików jawnie, jeśli to możliwe. Jeśli boisz się ich wyciekać, użyj finalizacji jako dodatkowego środka, a nie głównego.

Basile Starynkevitch
źródło
7
Dodałbym, że zamknięcie strumienia do pliku lub gniazda zwykle go opróżnia. Pozostawienie otwartych strumieni niepotrzebnie zwiększa ryzyko utraty danych w przypadku zaniku zasilania,
2
W rzeczywistości, chociaż dozwolona liczba skryptów plików może być niska, nie jest to naprawdę trudna kwestia, ponieważ można by to wykorzystać jako sygnał dla GC. Naprawdę problematyczne jest to: a) całkowicie nieprzejrzyste dla GC, ile zasobów nie zarządzanych przez GC zawiesiło się na nim, oraz b) wiele z tych zasobów jest unikatowych, więc inne mogą zostać zablokowane lub odrzucone.
Deduplicator,
2
A jeśli pozostawisz otwarty plik, może to zakłócać korzystanie z niego przez inną osobę. (Przeglądarka Windows 8 XPS, patrzę na ciebie!)
Loren Pechtel
2
„Jeśli boisz się przeciekać [deskryptory plików], użyj finalizacji jako dodatkowego środka, a nie głównego.” To stwierdzenie brzmi dla mnie podejrzanie. Jeśli dobrze projektujesz swój kod, czy naprawdę powinieneś wprowadzić redundantny kod, który rozprzestrzenia porządki w wielu miejscach?
mucaho,
2
@BasileStarynkevitch Chodzi o to, że w idealnym świecie, tak, nadmiarowość jest zła, ale w praktyce, gdzie nie można przewidzieć wszystkich istotnych aspektów, lepiej być bezpiecznym niż żałować?
mucaho,
13

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

Kilian Foth
źródło
Jeśli mam tylko pisać kod, który jest „niezbędny”, to czy powinienem unikać stylizacji we wszystkich aplikacjach GUI? Z pewnością nic z tego nie jest konieczne; GUI będą dobrze działać bez stylizacji, będą wyglądać okropnie. Jeśli chodzi o finalizator, może być konieczne zrobienie czegoś, ale nadal można go umieścić w finalizatorze, ponieważ finalizator zostanie wywołany podczas obiektu GC, jeśli w ogóle. Jeśli musisz zamknąć zasób, zwłaszcza gdy jest on gotowy do wyrzucania elementów bezużytecznych, finalizator jest optymalny. Albo program zakończy zwalnianie zasobu, albo zostanie wywołany finalizator.
Kröw
Jeśli mam ciężki RAM, kosztowny do utworzenia, zamykany obiekt i postanawiam odwoływać się do niego słabo, aby można go było wyczyścić, gdy nie jest potrzebny, to mógłbym 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.
Kröw
8

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

Psy
źródło
8

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 :

Obiekty z finalizatorami (te, które mają nietrywialną metodę finalizowania ()) mają znaczny narzut w porównaniu z obiektami bez finalizatorów i powinny być używane oszczędnie. Obiekty możliwe do sfinalizowania są wolniejsze w przydzielaniu i wolniejsze w zbieraniu. W czasie alokacji maszyna JVM musi zarejestrować dowolne obiekty możliwe do sfinalizowania w module wyrzucania elementów bezużytecznych i (przynajmniej w implementacji JVM HotSpot) obiekty finalizowane muszą podążać wolniejszą ścieżką alokacji niż większość innych obiektów. Podobnie obiekty, które można sfinalizować, również są wolniejsze. Potrzeba co najmniej dwóch cykli wyrzucania elementów bezużytecznych (w najlepszym przypadku), zanim obiekt, który można sfinalizować, może zostać odzyskany, a moduł wyrzucający elementy bezużyteczne musi wykonać dodatkową pracę, aby wywołać moduł finalizujący. Rezultatem jest więcej czasu poświęcanego na przydzielanie i zbieranie obiektów oraz większa presja na śmietnik, ponieważ pamięć używana przez nieosiągalne obiekty możliwe do sfinalizowania jest przechowywana dłużej. Połączmy to z faktem, że finalizatory nie mają gwarancji działania w przewidywalnych ramach czasowych, a nawet w ogóle, i widać, że jest stosunkowo niewiele sytuacji, w których finalizacja jest właściwym narzędziem do użycia.

alip
źródło
Doskonały punkt Zobacz moją odpowiedź, która pozwala uniknąć nadmiernego obciążenia związanego z wydajnością finalize().
Mike Nakis,
7

Moim (najmniej) ulubionym powodem unikania Object.finalizenie 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.

void test() {
   HasFinalize myObject = ...;
   OutputStream os = myObject.stream;

   // myObject is no-longer reachable at this point, 
   // even though it is in scope. But objects are finalized
   // based on reachability.
   // And since finalization is on another thread, it 
   // could happen before or in the middle of the write .. 
   // closing the stream and causing much fun.
   os.write("Hello World");
}

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.

Michael Anderson
źródło
1
Chodzi o to, że HasFinalize.streamsam powinien być przedmiotem, który można sfinalizować oddzielnie. Oznacza to, że finalizacja HasFinalizenie powinna się kończyć ani próbować posprzątać stream. A jeśli tak, to powinno stać się streamniedostępne.
acelent
4

Muszę ciągle sprawdzać, czy obiekt ma implementację metody close (), i jestem pewien, że w przeszłości w niektórych momentach do niego przeoczyłem.

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.

Nebu Pookins
źródło
1
Dobry pomysł wspomnieć AutoCloseable. Ułatwia zarządzanie zasobami za pomocą try-with-resources. To unieważnia kilka argumentów w pytaniu.
2

Na twoje pierwsze pytanie:

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?

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. Tam close()metoda została wywołana przez finalize()metodę, podczas gdy próba odczytu ze strumienia przez ten sam obiekt jest nadal w toku. Oprócz dobrze znanej możliwości, która finalize()zostaje wywołana do późna, istnieje możliwość, że zostanie wywołana zbyt wcześnie.

Przesłanka twojego drugiego pytania:

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.

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 OutOfMemoryErrorjest 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ł…

Holger
źródło