Sovrascrivere Object.finalize () è davvero male?


34

I due argomenti principali contro l'override Object.finalize()è che:

  1. Non puoi decidere quando viene chiamato.

  2. Potrebbe non essere chiamato affatto.

Se lo capisco correttamente, non penso che questi siano abbastanza buoni motivi per odiare Object.finalize()così tanto.

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

  2. Normalmente, e correggimi se sbaglio, l'unica volta Object.finalize()che non verrebbe chiamato è quando l'applicazione è stata chiusa prima che il GC avesse la possibilità di funzionare. Tuttavia, l'oggetto è stato deallocato comunque quando il processo dell'applicazione è stato terminato con esso. Quindi Object.finalize()non è stato chiamato perché non era necessario che fosse chiamato. Perché dovrebbe interessarsi allo sviluppatore?

Ogni volta che utilizzo oggetti che devo chiudere manualmente (come handle di file e connessioni), sono molto frustrato. Devo controllare costantemente se un oggetto ha un'implementazione di close(), e sono sicuro di aver perso alcune chiamate in alcuni punti in passato. Perché non è solo più semplice e sicuro lasciarlo alla VM e GC per smaltire questi oggetti inserendo l' close()implementazione Object.finalize()?


1
Nota anche: come molte API dell'era Java 1.0, la semantica del thread di finalize()è un po 'incasinata. Se mai lo si implementa, assicurarsi che sia thread-safe rispetto a tutti gli altri metodi sullo stesso oggetto.
billc.cn,

2
Quando senti persone dire che i finalizzatori sono cattivi, non significano che il tuo programma smetterà di funzionare se li hai; significano che l'intera idea della finalizzazione è piuttosto inutile.
user253751

1
+1 a questa domanda. La maggior parte delle risposte di seguito afferma che le risorse come i descrittori di file sono limitate e che dovrebbero essere raccolte manualmente. Lo stesso era / è vero per quanto riguarda la memoria, quindi se accettiamo qualche ritardo nella raccolta della memoria, perché non accettarlo per i descrittori di file e / o altre risorse?
mbonnin,

Rivolgendosi al tuo ultimo paragrafo, puoi lasciarlo a Java per gestire la chiusura di oggetti come handle di file e connessioni con il minimo sforzo da parte tua. Usa il blocco try-with-resources: è già menzionato alcune volte nelle risposte e nei commenti, ma penso che valga la pena metterlo qui. Il tutorial Oracle per questo si trova su docs.oracle.com/javase/tutorial/essential/exceptions/…
Jeutnarg,

Risposte:


45

Nella mia esperienza, c'è una e una sola ragione per ignorare Object.finalize(), ma è un'ottima ragione :

Inserire il codice di registrazione degli errori in finalize()cui avvisare se si dimentica di invocare close().

L'analisi statica può solo rilevare omissioni in scenari di utilizzo banali e gli avvisi del compilatore menzionati in un'altra risposta hanno una visione così semplicistica delle cose che devi effettivamente disabilitarle per ottenere qualcosa di non banale. (Ho molti più avvisi abilitati rispetto a qualsiasi altro programmatore di cui conosco o di cui non ho mai sentito parlare, ma non ho abilitati avvisi stupidi.)

La finalizzazione potrebbe sembrare un buon meccanismo per assicurarsi che le risorse non vadano fuori strada, ma la maggior parte delle persone la vede in un modo completamente sbagliato: la considera come un meccanismo alternativo di fallback, una "seconda possibilità" che salverà automaticamente la giorno smaltendo le risorse che hanno dimenticato. Questo è completamente sbagliato . Ci deve essere un solo modo di fare qualsiasi cosa: o chiudi sempre tutto o la finalizzazione chiude sempre tutto. Ma poiché la finalizzazione non è affidabile, la finalizzazione non può esserlo.

Quindi, esiste questo schema che io chiamo smaltimento obbligatorio e stabilisce che il programmatore è responsabile della chiusura esplicita di tutto ciò che implementa Closeableo AutoCloseable. (L'istruzione try-with-resources conta ancora come chiusura esplicita.) Naturalmente, il programmatore potrebbe dimenticare, quindi è qui che entra in gioco la finalizzazione, ma non come una fata magica che magicamente renderà le cose giuste alla fine: se la finalizzazione scopre che close()non è stato invocato, non lo ètenta di invocarlo, proprio perché ci saranno (con certezza matematica) orde di programmatori n00b che faranno affidamento su di esso per fare il lavoro che erano troppo pigri o troppo assenti per farlo. Quindi, con lo smaltimento obbligatorio, quando la finalizzazione scopre che close()non è stata invocata, registra un messaggio di errore rosso brillante, che dice al programmatore con grandi e grosse lettere maiuscole per sistemare la sua roba, la sua roba.

Come ulteriore vantaggio, si dice 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 è possibile evitare qualsiasi finalizzazione overhead in tutto il tuo sistema ( vedi la risposta di alip per informazioni su quanto sia terribile questo overhead) codificando il tuo finalize()metodo 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 ciò è che Global.DEBUGè una static finalvariabile il cui valore è noto al momento della compilazione, quindi se è falsecosì il compilatore non emetterà alcun codice per l'intera ifistruzione, il che renderà questo un finalizzatore banale (vuoto), che a sua volta significa che la tua classe verrà trattata come se non avesse un finalizzatore. (In C # questo sarebbe fatto con un bel #if DEBUGblocco, ma cosa possiamo fare, questo è Java, dove paghiamo un'apparente semplicità nel codice con un sovraccarico aggiuntivo nel cervello.)

Maggiori informazioni sullo smaltimento obbligatorio, con ulteriori discussioni sullo smaltimento delle risorse in dot Net, qui: michael.gr: smaltimento obbligatorio rispetto all'abominio "Smaltimento-smaltimento"


2
@MikeNakis Non dimenticare, Closeable è definito come non fare nulla se chiamato una seconda volta: docs.oracle.com/javase/7/docs/api/java/io/Closeable.html . Ammetto che a volte ho registrato un avviso quando le mie lezioni sono chiuse due volte, ma tecnicamente non dovresti nemmeno farlo. Tecnicamente però, chiamare .close () su Closable più di una volta è perfettamente valido.
Patrick M,

1
@usr tutto si riduce a se ti fidi dei tuoi test o non ti fidi dei tuoi test. Se non vi fidate la vostra prova, certo, andare avanti e subire l'overhead finalizzazione a anche close() , per ogni evenienza. Credo che se i miei test non sono affidabili, è meglio che non rilasci il sistema in produzione.
Mike Nakis,

3
@Mike, affinché il if( Global.DEBUG && ...costrutto funzioni in modo che JVM ignori il finalize()metodo come banale, Global.DEBUGdeve essere impostato al momento della compilazione (al contrario di quello iniettato, ecc.), Quindi ciò che segue sarà un codice morto. La chiamata asuper.finalize() all'esterno del blocco if è anche sufficiente affinché la JVM lo consideri non banale (indipendentemente dal valore di Global.DEBUG, almeno su HotSpot 1.8) anche se anche la superclasse #finalize()è banale!
alip

1
@ Mike Ho paura che sia così. L'ho provato con (una versione leggermente modificata) del test nell'articolo a cui ti sei collegato e l'output GC dettagliato (insieme a prestazioni sorprendentemente scarse) conferma che gli oggetti vengono copiati nello spazio di vecchia generazione e sopravvissuti e per ottenere l'heap GC completo per ottenere liberarsi di.
Alip,

1
Ciò che viene spesso trascurato, è il rischio di raccolta di oggetti prima del previsto che rende la liberazione di risorse nel finalizzatore un'azione molto pericolosa. Prima di Java 9, l'unico modo per essere sicuri che un finalizzatore non chiudesse la risorsa mentre è ancora in uso, è sincronizzare l'oggetto, sia nel finalizzatore che nei metodi che utilizzano la risorsa. Ecco perché funziona java.io. Se quel tipo di sicurezza del thread non era nella lista dei desideri, si aggiunge al sovraccarico indotto da finalize()...
Holger

28

Ogni volta che utilizzo oggetti che devo chiudere manualmente (come handle di file e connessioni), sono molto frustrato. [...] Perché non è solo più semplice e sicuro lasciarlo alla VM e GC per smaltire questi oggetti inserendo l' close()implementazione Object.finalize()?

Poiché gli handle e le connessioni dei file (ovvero i descrittori di file su sistemi Linux e POSIX) sono una risorsa piuttosto scarsa (potresti essere limitato a 256 di essi su alcuni sistemi o a 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 risorse così 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 "da sforzo" nella JVM. Potrebbe non essere chiamato, o essere chiamato abbastanza tardi ... In particolare, se hai molta RAM o se il tuo programma non alloca molti oggetti (o se la maggior parte di loro muore prima di essere inoltrato a un numero abbastanza vecchio generazione da un GC generazionale di copia), il GC potrebbe essere chiamato abbastanza raramente e le finalizzazioni potrebbero non essere eseguite molto spesso (o addirittura potrebbero non funzionare affatto).

Quindi closedescrittori di file esplicitamente, se possibile. Se hai paura di perdere, usa la finalizzazione come misura aggiuntiva, non come principale.


7
Vorrei aggiungere che la chiusura di un flusso in un file o socket normalmente lo scarica. Lasciare i flussi aperti aumenta inutilmente il rischio di perdita di dati se si spegne l'alimentazione, si interrompe una connessione (questo è anche un rischio per i file a cui si accede attraverso una rete), ecc.

2
In realtà, mentre il numero di descrittori di file consentiti potrebbe essere basso, non è questo il punto veramente appiccicoso, perché almeno uno potrebbe usarlo come segnale per il GC. La cosa veramente problematica è a) completamente intransparente al GC quante risorse non gestite da GC sono appese su di esso eb) molte di queste risorse sono uniche, quindi altre potrebbero essere bloccate o negate.
Deduplicatore,

2
E se lasci un file aperto, potrebbe interferire con qualcun altro che lo utilizza. (Visualizzatore XPS per Windows 8, ti sto guardando!)
Loren Pechtel il

2
"Se hai paura di perdere [descrittori di file], usa la finalizzazione come misura aggiuntiva, non come principale." Questa affermazione mi sembra sospetta. Se progetti bene il tuo codice, dovresti davvero introdurre codice ridondante che distribuisce la pulizia su più posizioni?
mucaho,

2
@BasileStarynkevitch Il tuo punto è che in un mondo ideale, sì, la ridondanza è negativa, ma in pratica, dove non puoi prevedere tutti gli aspetti rilevanti, è meglio prevenire che curare?
mucaho,

13

Guarda la questione in questo modo: dovresti solo scrivere un codice che sia (a) corretto (altrimenti il ​​tuo programma è semplicemente sbagliato) e (b) necessario (altrimenti il ​​tuo codice è troppo grande, il che significa che è necessaria più RAM, più cicli spesi per cose inutili, più sforzo per capirlo, 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! Ad ogni modo, metterlo nel finalizzatore è la scelta sbagliata.

(Nota che gli esempi che chiami, come la chiusura di flussi di file, sembrano non essere realmente necessari, ma lo sono. È solo che fino a quando non raggiungi il limite per gli handle di file aperti sul tuo sistema, non noterai che il tuo codice non è corretto. Ma questo limite è una caratteristica del sistema operativo ed è quindi ancora più imprevedibile di quanto la politica della JVM per finalizzatori, in modo che davvero, davvero è importante che non si rifiuti handle di file.)


Se dovessi solo scrivere il codice "necessario", dovrei evitare tutto lo stile in tutte le mie applicazioni GUI? Sicuramente nulla di tutto ciò è necessario; le GUI funzioneranno benissimo senza stile, sembreranno solo orride. Per quanto riguarda il finalizzatore, può essere necessario fare qualcosa, ma comunque ok per inserirlo in un finalizzatore, poiché il finalizzatore verrà chiamato durante l'oggetto GC, se non del tutto. Se è necessario chiudere una risorsa, in particolare quando è pronta per la garbage collection, un finalizzatore è ottimale. Il programma termina il rilascio della risorsa o viene chiamato il finalizzatore.
Kröw,

Se ho un oggetto RAM pesante, costoso da creare e richiudibile e decido di fare riferimento a esso debolmente, in modo che possa essere cancellato quando non è necessario, allora potrei usarlo finalize()per chiuderlo nel caso in cui un ciclo gc debba davvero liberare RAM. Altrimenti, terrei l'oggetto nella RAM invece di doverlo rigenerare e chiuderlo ogni volta che devo usarlo. Certo, le risorse che aprirà non saranno liberate fino a quando l'oggetto non sarà GCed, ogni volta che ciò potrebbe essere, ma potrei non aver bisogno di garantire che le mie risorse vengano liberate in un determinato momento.
Kröw,

8

Uno dei maggiori motivi per non fare affidamento sui finalizzatori è che la maggior parte delle risorse che si potrebbero essere tentati di ripulire nel finalizzatore sono molto limitate. Il garbage collector viene eseguito solo ogni tanto, poiché attraversare i riferimenti per determinare se è possibile rilasciare qualcosa è costoso. Ciò significa che potrebbe "passare un po '" prima che i tuoi oggetti vengano effettivamente distrutti. Se hai molti oggetti che aprono connessioni di database di breve durata, ad esempio, lasciare i finalizzatori per ripulire queste connessioni potrebbe esaurire il tuo pool di connessioni mentre attende che il Garbage Collector venga finalmente eseguito e libera le connessioni finite. Quindi, a causa dell'attesa, si finisce con un ampio arretrato di richieste in coda, che esauriscono rapidamente il pool di connessioni. E'

Inoltre, l'uso di try-with-resources rende facile la chiusura di oggetti "chiudibili" al completamento. Se non hai familiarità con questo costrutto, ti suggerisco di provarlo: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html


8

Oltre al motivo per cui lasciare al finalizzatore il rilascio di risorse è generalmente una cattiva idea, gli oggetti finalizzabili hanno un sovraccarico prestazionale.

Dalla teoria e pratica di Java: Garbage Collection e performance (Brian Goetz), i finalizzatori non sono tuoi amici :

Gli oggetti con finalizzatori (quelli che hanno un metodo finalize () non banale) hanno un notevole sovraccarico rispetto agli oggetti senza finalizzatori e dovrebbero essere usati con parsimonia. Gli oggetti finalizzabili sono sia più lenti da allocare che più lenti da raccogliere. Al momento dell'allocazione, la JVM deve registrare tutti gli oggetti finalizzabili con il Garbage Collector e (almeno nell'implementazione di HotSpot JVM) gli oggetti finalizzabili devono seguire un percorso di allocazione più lento rispetto alla maggior parte degli altri oggetti. Allo stesso modo, anche gli oggetti finalizzabili sono più lenti da raccogliere. Ci vogliono almeno due cicli di garbage collection (nel migliore dei casi) prima che un oggetto finalizzabile possa essere recuperato e il garbage collector deve fare un lavoro extra per invocare il finalizzatore. Il risultato è più tempo dedicato all'allocazione e alla raccolta di oggetti e una maggiore pressione sul garbage collector, perché la memoria utilizzata da oggetti finalizzabili non raggiungibili viene conservata più a lungo. Combinalo con il fatto che i finalizzatori non sono garantiti per essere eseguiti in tempi prevedibili, o addirittura affatto, e puoi vedere che ci sono relativamente poche situazioni per le quali la finalizzazione è lo strumento giusto da usare.


Punto eccellente. Vedi la mia risposta che fornisce un mezzo per evitare il sovraccarico di prestazioni di finalize().
Mike Nakis,

7

Il mio (minimo) motivo preferito per evitare Object.finalizenon è che gli oggetti possano essere finalizzati dopo che te lo aspetti, ma possono essere finalizzati prima che tu te lo aspetti. Il problema non è che un oggetto ancora nell'ambito può essere finalizzato prima che l'ambito 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 maggiori dettagli. Ancora più divertente è che questa decisione potrebbe essere presa solo dopo l'ottimizzazione dell'hot-spot, rendendo questo debug doloroso.


1
Il fatto è che HasFinalize.streamdovrebbe essere esso stesso un oggetto finalizzabile separatamente. Cioè, la finalizzazione di HasFinalizenon dovrebbe finalizzare o cercare di ripulire stream. O se dovrebbe, allora dovrebbe rendere streaminaccessibile.
Acelent,


4

Devo controllare costantemente se un oggetto ha un'implementazione di close () e sono sicuro di aver perso alcune chiamate in alcuni punti in passato.

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


1
Buona idea menzionare AutoCloseable. Rende il cervello estremamente semplice gestire le risorse con le risorse di prova. Questo annulla molti degli argomenti della domanda.

2

Alla tua prima domanda:

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

Bene, la JVM determinerà, quando è un buon punto per recuperare la memoria che è stata allocata per un oggetto. Questo non è necessariamente il momento in cui finalize()dovrebbe avvenire la pulizia della risorsa, in cui si desidera eseguire . Questo è illustrato nella domanda "finalize () chiamato su oggetto fortemente raggiungibile in Java 8" su SO. Lì, un close()metodo è stato chiamato da un finalize()metodo, mentre è ancora in corso un tentativo di leggere dallo stream dallo stesso oggetto. Quindi oltre alla ben nota possibilità che finalize()viene chiamata fino a tardi c'è la possibilità che venga chiamata troppo presto.

La premessa della tua seconda domanda:

Normalmente, e correggimi se sbaglio, l'unica volta Object.finalize()che non verrebbe chiamato è quando l'applicazione è stata chiusa prima che il GC avesse la possibilità di funzionare.

è semplicemente sbagliato. Non è necessario che una JVM supporti la finalizzazione. Bene, non è del tutto sbagliato in quanto potresti ancora interpretarlo come "l'applicazione è stata chiusa prima che la finalizzazione avesse luogo", supponendo che la tua applicazione verrà mai chiusa.

Ma nota la piccola differenza tra "GC" della tua dichiarazione originale e il termine "finalizzazione". La garbage collection è distinta dalla finalizzazione. Una volta che la gestione della memoria rileva che un oggetto non è raggiungibile, può semplicemente recuperare il suo spazio, se uno dei due non ha un finalize()metodo speciale o la finalizzazione è semplicemente non supportata o può accodare l'oggetto per la finalizzazione. Pertanto, il completamento di un ciclo di garbage collection non implica che i finalizzatori vengano eseguiti. Ciò può 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 alla finalizzazione, fare affidamento su di esso per la pulizia delle risorse è pericoloso. La garbage collection fa parte della gestione della memoria e quindi innescata da esigenze di memoria. È possibile che la garbage collection non venga mai eseguita perché c'è abbastanza memoria durante l'intero runtime (beh, si adatta comunque alla descrizione "l'applicazione è stata chiusa prima che il GC avesse la possibilità di eseguire", in qualche modo). È anche possibile che il GC funzioni, ma in seguito è stata recuperata memoria sufficiente, 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 OutOfMemoryErrorviene lanciato solo dopo sufficienti tentativi di liberare memoria, ciò non si applica alle risorse native e alla finalizzazione. È possibile che l'apertura di un file non riesca a causa di risorse insufficienti mentre la coda di finalizzazione è piena di oggetti che potrebbero liberare queste risorse, se il finalizzatore dovesse mai funzionare ...

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.