Sovrascrive Object.finalize () veramente brutto?

32

I due principali argomenti contro l'override di Object.finalize() sono i seguenti:

  1. Non puoi decidere quando è chiamato.

  2. Potrebbe non essere richiamato affatto.

Se capisco correttamente, non penso che siano sufficienti motivi per odiare Object.finalize() così tanto.

  1. Spetta all'implementazione VM e al GC determinare quando è il momento giusto per deallocare un oggetto, non lo sviluppatore. Perché è importante decidere quando viene chiamato Object.finalize() ?

  2. Normalmente, e correggimi se sbaglio, l'unica volta in cui Object.finalize() non viene richiamato è quando l'applicazione viene terminata prima che il GC abbia la possibilità di essere eseguito. Tuttavia, l'oggetto è stato deallocato comunque quando il processo dell'applicazione si è concluso con esso. Quindi Object.finalize() non è stato chiamato perché non era necessario chiamarlo. Perché lo sviluppatore dovrebbe interessarsi?

Ogni volta che utilizzo oggetti che devo chiudere manualmente (come handle di file e connessioni), mi sento molto frustrato. Devo controllare costantemente se un oggetto ha un'implementazione di close() , e sono sicuro di aver mancato alcune chiamate ad esso in alcuni punti del passato. Perché non è semplicemente più semplice e più sicuro lasciare la VM e il GC a smaltire questi oggetti inserendo l'implementazione close() in Object.finalize() ?

    
posta AxiomaticNexus 04.07.2015 - 22:43
fonte

8 risposte

43

Secondo la mia esperienza, esiste un motivo uno e un solo per ignorare Object.finalize() , ma è un buon motivo :

To place error logging code in finalize() which notifies you if you ever forget to invoke close().

Le analisi statiche possono catturare omissioni solo in banali scenari di utilizzo, e gli avvertimenti del compilatore citati in un'altra risposta hanno una visione così semplicistica delle cose che è necessario disabilitarli per ottenere qualcosa di non banale. (Ho molti più avvisi abilitati di qualsiasi altro programmatore di cui conosco o abbia mai sentito parlare, ma non ho abilitato gli avvisi stupidi.)

La finalizzazione potrebbe sembrare un buon meccanismo per assicurarsi che le risorse non siano esposte, ma la maggior parte delle persone la vede in un modo completamente sbagliato: la pensano come un meccanismo alternativo di riserva, una "seconda possibilità" di salvaguardia che salva automaticamente la giornata eliminando le risorse che hanno dimenticato. È assolutamente sbagliato . Deve esserci un solo modo per fare una determinata cosa: o si chiude sempre tutto, o la finalizzazione chiude sempre tutto. Ma poiché la finalizzazione è inaffidabile, la finalizzazione non può essere quella.

Quindi, c'è questo schema che chiamo Smaltimento obbligatorio , e stabilisce che il programmatore è responsabile per chiusura sempre esplicita di tutto che implementa Closeable o AutoCloseable . (La dichiarazione try-with-resource conta ancora come chiusura esplicita.) Naturalmente, il programmatore può dimenticare, quindi è qui che entra in gioco la finalizzazione, ma non come una fata magica che magicamente farà le cose giuste alla fine: se la finalizzazione scopre che close() non è stato invocato, fa non tenta di invocarlo, proprio perché ci saranno (con certezza matematica) orde di programmatori n00b che si affideranno ad esso fare il lavoro che erano troppo pigri o troppo distratti per fare. Quindi, con lo smaltimento obbligatorio, quando la finalizzazione scopre che close() non è stato richiamato, registra un messaggio di errore rosso vivo, dicendo al programmatore con grandi lettere maiuscole e grosse per aggiustare il suo s, la sua roba.

Come ulteriore vantaggio, diciamo che che "la JVM ignorerà un banale metodo finalize () ( ad esempio uno che ritorna semplicemente senza fare nulla, come quello definito nella classe Object) ", quindi con lo smaltimento obbligatorio puoi evitare tutto il sovraccarico di finalizzazione nell'intero sistema ( vedi la risposta di alip per informazioni su quanto sia terribile questo overhead codificando il tuo metodo finalize() in questo modo:

@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.
}

L'idea alla base di questo è che Global.DEBUG è una variabile static final il cui valore è noto al momento della compilazione, quindi se è false il compilatore non emetterà alcun codice per l'intera istruzione if , che renderà questo un banale finalizzatore (vuoto), che a sua volta significa che la classe verrà trattata come se non avesse un finalizzatore. (In C # questo sarebbe fatto con un bel blocco #if DEBUG , ma cosa possiamo fare, questo è java, dove paghiamo apparente semplicità nel codice con sovraccarico aggiuntivo nel cervello.)

Ulteriori informazioni sullo Smaltimento obbligatorio, con ulteriori discussioni sullo smaltimento delle risorse in dot Net, qui: michael.gr: disposizione obbligatoria contro l'abominio" Dispose-disposing "

    
risposta data 05.07.2015 - 00:24
fonte
27

Every time I'm using objects that I have to manually close (like file handles and connections), I get very frustrated. [...] Why isn't it just simpler and safer to just leave it to the VM and GC to dispose of these objects by putting the close() implementation in Object.finalize()?

Perché il file gestisce & le connessioni (ovvero, i descrittori di file su sistemi Linux e POSIX) sono piuttosto scarse (potresti essere limitato a 256 di loro su alcuni sistemi, o su 16384 su alcuni altri, vedi setrlimit (2) ). Non vi è alcuna garanzia che il GC venga chiamato abbastanza spesso (o al momento giusto) per evitare di esaurire tali risorse limitate. E se il GC non viene chiamato abbastanza (o la finalizzazione non viene eseguita al momento giusto) raggiungerai quel limite (possibilmente basso).

La finalizzazione è una cosa "best effort" nella JVM. Potrebbe non essere chiamato, o essere chiamato piuttosto tardi ... In particolare, se hai molta RAM o se il tuo programma non assegna molti oggetti (o se la maggior parte di essi muore prima di essere inoltrati ad alcuni abbastanza vecchi) generazione da una copia generazionale GC), il GC potrebbe essere chiamato abbastanza raramente, e le finalizzazioni potrebbero non essere eseguite molto spesso (o addirittura potrebbero non funzionare affatto).

Quindi close descrittori di file esplicitamente, se possibile. Se hai paura di farli fuoriuscire, usa la finalizzazione come misura aggiuntiva, non come quella principale.

    
risposta data 04.07.2015 - 22:55
fonte
13

Guarda la questione in questo modo: dovresti scrivere solo codice che è (a) corretto (altrimenti il tuo programma è semplicemente sbagliato) e (b) necessario (altrimenti il tuo codice è troppo grande, il che significa più RAM necessaria, più cicli spesi in cose inutili, più sforzo per comprenderlo, più tempo speso per mantenerlo ecc. ecc.)

Ora considera cosa vuoi fare in un finalizzatore. O è necessario. In tal caso non puoi inserirlo in un finalizzatore, perché non sai se verrà chiamato. Non è abbastanza buono. O non è necessario - quindi non dovresti scriverlo in primo luogo! In ogni caso, inserirla nel finalizzatore è la scelta sbagliata.

(Si noti che gli esempi che si chiamano, come la chiusura dei flussi di file, guardano come se non fossero realmente necessari, ma lo sono. È proprio così finché non si raggiunge il limite per gli handle di file aperti su il tuo sistema, non noterai che il tuo codice non è corretto, ma quel limite è una caratteristica del sistema operativo ed è quindi ancora più imprevedibile della politica della JVM per i finalizzatori, quindi è davvero, davvero < strong> è importante che tu non sprechi gli handle di file.)

    
risposta data 04.07.2015 - 23:23
fonte
8

Uno dei motivi principali per non fare affidamento sui finalizzatori è che la maggior parte delle risorse che si potrebbero tentare di ripulire nel finalizzatore sono molto limitate. Il garbage collector viene eseguito ogni tanto, dal momento che attraversare i riferimenti per determinare se qualcosa può essere rilasciato o meno è costoso. Ciò significa che potrebbe essere "un po 'di tempo" prima che i tuoi oggetti vengano effettivamente distrutti. Se molti oggetti aprono connessioni di database di breve durata, ad esempio, lasciando i finalizzatori per ripulire queste connessioni si potrebbe esaurire il pool di connessioni mentre attende che il garbage collector possa finalmente funzionare e liberare le connessioni finite. Quindi, a causa dell'attesa, si finisce con un grande arretrato di richieste in coda, che esauriscono rapidamente il pool di connessioni. È un ciclo che metterà rapidamente la tua applicazione in uno stato di funzionamento invalido.

Inoltre, l'uso di try-with-resources rende la chiusura degli oggetti 'chiudibili' al completamento facile da fare. Se non hai familiarità con questo costrutto, ti suggerisco di verificarlo: link

    
risposta data 04.07.2015 - 23:00
fonte
8

Oltre al motivo per cui lasciare che il finalizzatore rilasci le risorse è in genere una cattiva idea, gli oggetti finalizzabili hanno un sovraccarico prestazionale.

Da Teoria e pratica Java: Garbage collection e performance (Brian Goetz), I finalizzatori non sono i tuoi amici :

Objects with finalizers (those that have a non-trivial finalize() method) have significant overhead compared to objects without finalizers, and should be used sparingly. Finalizeable objects are both slower to allocate and slower to collect. At allocation time, the JVM must register any finalizeable objects with the garbage collector, and (at least in the HotSpot JVM implementation) finalizeable objects must follow a slower allocation path than most other objects. Similarly, finalizeable objects are slower to collect, too. It takes at least two garbage collection cycles (in the best case) before a finalizeable object can be reclaimed, and the garbage collector has to do extra work to invoke the finalizer. The result is more time spent allocating and collecting objects and more pressure on the garbage collector, because the memory used by unreachable finalizeable objects is retained longer. Combine that with the fact that finalizers are not guaranteed to run in any predictable timeframe, or even at all, and you can see that there are relatively few situations for which finalization is the right tool to use.

    
risposta data 05.07.2015 - 11:27
fonte
6

Il mio (minimo) motivo preferito per evitare Object.finalize non è che gli oggetti possano essere finalizzati dopo che te l'aspetti, ma possono essere finalizzati prima che potresti aspettarti. Il problema non è che un oggetto che è ancora in ambito può essere finalizzato prima che l'oscilloscopio venga chiuso se Java decide che non è più raggiungibile.

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");
}

Vedi questa domanda per ulteriori dettagli. Ancora più divertente è che questa decisione potrebbe essere presa solo dopo l'ottimizzazione dell'hot-spot, rendendo così doloroso il debug.

    
risposta data 06.07.2015 - 03:23
fonte
4

I have to be constantly checking if an object has an implementation of close(), and I'm sure I have missed a few calls to it at some points in the past.

In Eclipse, ricevo un avviso ogni volta che dimentico di chiudere qualcosa che implementa Closeable / AutoCloseable . Non sono sicuro che sia una cosa di Eclipse o se faccia parte del compilatore ufficiale, ma potresti cercare di utilizzare strumenti di analisi statica simili per aiutarti. FindBugs, ad esempio, può probabilmente aiutarti a verificare se hai dimenticato di chiudere una risorsa.

    
risposta data 04.07.2015 - 23:04
fonte
1

Alla tua prima domanda:

It is up to the VM implementation and the GC to determine when the right time to deallocate an object is, not the developer. Why is it important to decide when Object.finalize() gets called?

Bene, la JVM determinerà, quando è un buon punto per recuperare lo spazio di archiviazione che è stato allocato per un oggetto. Questo non è necessariamente il momento in cui la pulizia della risorsa, che si desidera eseguire in finalize() , dovrebbe avvenire. Questo è illustrato nel finalize () chiamato su oggetto strongmente raggiungibile in Java 8 " domanda su SO. Lì, un metodo close() è stato chiamato da un metodo finalize() , mentre un tentativo di leggere dallo stream dallo stesso oggetto è ancora in sospeso. Quindi oltre alla ben nota possibilità che finalize() venga richiamato in ritardo, c'è la possibilità che venga chiamato troppo presto.

La premessa della tua seconda domanda:

Normally, and correct me if I'm wrong, the only time Object.finalize() wouldn't get called is when the application got terminated before the GC got a chance to run.

è semplicemente sbagliato. Non è necessario che una JVM supporti la finalizzazione. Beh, non è del tutto sbagliato, dato che potresti ancora interpretarlo come "l'applicazione è stata interrotta prima che la finalizzazione abbia avuto luogo", supponendo che la tua applicazione finirà mai.

Notate la piccola differenza tra "GC" della vostra dichiarazione originale e il termine "finalizzazione". La garbage collection è distinta dalla finalizzazione. Una volta che la gestione della memoria rileva che un oggetto è irraggiungibile, può semplicemente recuperare lo spazio, se non esiste, non ha un metodo speciale finalize() o la finalizzazione è semplicemente non supportata, oppure può accodare l'oggetto per la finalizzazione. Pertanto, il completamento di un ciclo di garbage collection non implica che i finalizzatori vengano eseguiti. Ciò potrebbe accadere in un secondo momento, quando la coda viene elaborata, o mai del tutto.

Questo punto è anche il motivo per cui anche su JVM con supporto per la finalizzazione, fare affidamento su di esso per la pulizia delle risorse è pericoloso. La garbage collection è parte della gestione della memoria e quindi innescata dalle esigenze di memoria. È possibile che la garbage collection non venga mai eseguita perché c'è abbastanza memoria durante l'intero runtime (beh, ciò rientra ancora nel "l'applicazione è stata interrotta prima che GC avesse la possibilità di eseguire" la descrizione, in qualche modo). È anche possibile che il GC venga eseguito, ma in seguito, c'è abbastanza memoria recuperata, quindi la coda del finalizzatore non viene elaborata.

In altre parole, le risorse native gestite in questo modo sono ancora estranee alla gestione della memoria. Mentre è garantito che un OutOfMemoryError viene lanciato solo dopo i tentativi sufficienti di liberare memoria, ciò non si applica alle risorse native e alla finalizzazione. È possibile che l'apertura di un file fallisca a causa di risorse insufficienti mentre la coda di finalizzazione è piena di oggetti che potrebbero liberare queste risorse, se il finalizzatore dovesse mai girare ...

    
risposta data 06.07.2015 - 16:38
fonte

Leggi altre domande sui tag