Est-ce que surcharger Object.finalize () est vraiment mauvais?

34

Les deux principaux arguments contre le dépassement Object.finalize()sont les suivants:

  1. Vous ne décidez pas quand ça s'appelle.

  2. Il peut ne pas être appelé du tout.

Si je comprends bien, je ne pense pas que ce soient des raisons suffisantes pour haïr Object.finalize()autant.

  1. Il incombe à l'implémentation de la VM et au GC de déterminer le moment propice pour libérer un objet, et non le développeur. Pourquoi est-il important de décider quand Object.finalize()se faire appeler?

  2. Normalement, et corrigez-moi si je me trompe, le seul moment où Object.finalize()on ne m'appelle pas, c'est quand l'application est terminée avant que le GC ne puisse s'exécuter. Cependant, l'objet a été désalloué de toute façon lorsque le processus de l'application a été interrompu. Donc, Object.finalize()ne pas être appelé parce qu'il n'était pas nécessaire d'appeler. Pourquoi le développeur s'en préoccuperait-il?

Chaque fois que j'utilise des objets que je dois fermer manuellement (comme des descripteurs de fichiers et des connexions), je suis très frustré. Je dois vérifier en permanence si un objet a une implémentation close(), et je suis certain d'avoir manqué quelques appels à ce sujet par le passé. Pourquoi n'est-il pas simplement plus simple et plus sûr de laisser le soin à la VM et au GC de se débarrasser de ces objets en intégrant la close()mise en œuvre Object.finalize()?

AxiomaticNexus
la source
1
Notez également que, comme beaucoup d’API de Java 1.0, la sémantique des threads finalize()est un peu gâchée. Si vous l'implémentez, assurez-vous qu'il est thread-safe par rapport à toutes les autres méthodes sur le même objet.
billc.cn
2
Lorsque vous entendez des personnes dire que les finaliseurs sont mauvais, cela ne signifie pas que votre programme cessera de fonctionner si vous en avez. ils veulent dire que toute l'idée de finalisation est plutôt inutile.
user253751
1
+1 à cette question. La plupart des réponses ci-dessous indiquent que les ressources telles que les descripteurs de fichiers sont limitées et qu'elles doivent être collectées manuellement. La même chose était / est vraie pour la mémoire, donc si nous acceptons un certain retard dans la collecte de mémoire, pourquoi ne pas l’accepter pour les descripteurs de fichier et / ou d’autres ressources?
mbonnin
En ce qui concerne votre dernier paragraphe, vous pouvez laisser à Java le soin de gérer des choses comme les descripteurs de fichiers et les connexions sans trop d'effort de votre part. Utilisez le bloc try-with-resources - cela est mentionné à quelques reprises déjà dans les réponses et les commentaires, mais je pense que cela vaut la peine de le mettre ici. Vous
trouverez

Réponses:

45

D'après mon expérience, il n'y a qu'une et une seule raison de ne pas le faire Object.finalize(), mais c'est une très bonne raison :

Pour placer le code de consignation d’erreur finalize()qui vous avertit si vous oubliez d’invoquer close().

L'analyse statique ne peut détecter que les omissions dans les scénarios d'utilisation triviaux, et les avertissements du compilateur mentionnés dans une autre réponse ont une vue tellement simpliste de la nécessité de les désactiver pour pouvoir effectuer quelque chose de non trivial. (J'ai beaucoup plus d'avertissements activés que n'importe quel autre programmeur que je connaisse ou dont j'ai entendu parler, mais je n'ai pas activé les avertissements stupides.)

La finalisation peut sembler être un bon moyen de s’assurer que les ressources ne restent pas inaperçues, mais la plupart des gens le voient d’une manière totalement fausse: ils le voient comme un mécanisme de secours alternatif, une sauvegarde "de la seconde chance" qui sauvera automatiquement le jour en disposant des ressources qu’ils ont oubliées. C'est complètement faux . Il doit exister une seule façon de faire une chose: soit on ferme toujours tout, soit la finalisation ferme toujours tout. Mais comme la finalisation n'est pas fiable, elle ne peut l'être.

Donc, il y a ce schéma que j'appelle élimination obligatoire , et il stipule que le programmeur est responsable de toujours fermer explicitement tout ce qui implémente Closeableou AutoCloseable. (L'instruction try-with-resources compte toujours en tant que clôture explicite.) Bien sûr, le programmeur peut oublier. C'est là que la finalisation entre en jeu, mais pas en tant que fée magique qui remettra les choses à la magie à la fin: si la finalisation découvre qui close()n'a pas été invoqué, cela ne signifie pasessayez de l'invoquer, précisément parce qu'il y aura (avec une certitude mathématique) des hordes de programmeurs n00b qui compteront dessus pour faire le travail pour lequel ils étaient trop paresseux ou trop distants. Donc, avec l'élimination obligatoire, lorsque la finalisation découvre que cela close()n'a pas été invoqué, un message d'erreur rouge vif est consigné, indiquant au programmeur, avec de grosses lettres majuscules, de réparer ses affaires.

Un avantage supplémentaire, selon la rumeur , "la machine virtuelle Java ignorera une méthode trivial finalize () (par exemple, une méthode qui retourne sans rien faire, comme celle définie dans la classe Object)", ce qui permet d' éviter toute suppression à la finalisation. des frais généraux dans tout votre système ( voir la réponse d' Alip pour des informations sur la gravité de ces frais) en codant votre finalize()méthode comme suit :

@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’idée sous-jacente est qu’il s’agisse d’ Global.DEBUGune static finalvariable dont la valeur est connue au moment de la compilation. Si c’est le cas, falsele compilateur n’émettra aucun code pour l’ ifinstruction complète , ce qui en fera un finaliseur trivial (vide), qui à son tour signifie que votre classe sera traitée comme si elle n'avait pas de finaliseur. (En C #, cela se ferait avec un beau #if DEBUGbloc, mais que pouvons-nous faire, java, où nous payons une simplicité apparente dans le code avec une surcharge supplémentaire dans le cerveau.)

Pour en savoir plus sur la disposition obligatoire, avec des discussions supplémentaires sur la disposition des ressources dans dot net, ici: michael.gr: Élimination obligatoire ou abomination "Éliminer-éliminer"

Mike Nakis
la source
2
@MikeNakis N'oubliez pas que Closeable est défini comme ne faisant rien s'il est appelé une seconde fois: docs.oracle.com/javase/7/docs/api/java/io/Closeable.html . J'avoue que j'ai parfois enregistré un avertissement lorsque mes cours sont fermés à deux reprises, mais techniquement, vous n'êtes même pas censé le faire. Techniquement, appeler .close () plusieurs fois sur Closable est parfaitement valide.
Patrick M
1
@ usr tout se résume à savoir si vous faites confiance à vos tests ou pas. Si vous ne faites pas confiance à vos tests, bien sûr, allez - y et subir les frais généraux de finalisation à également close() , au cas où. Je crois que si l'on ne fait pas confiance à mes tests, je ferais mieux de ne pas mettre le système en production.
Mike Nakis
3
@Mike, pour que la if( Global.DEBUG && ...construction fonctionne de manière à ce que la machine virtuelle Java ignore la finalize()méthode comme étant triviale, Global.DEBUGdoit être défini au moment de la compilation (par opposition à injecté, etc.), de sorte que ce qui suit sera du code mort. L'appel en super.finalize()dehors du bloc if est également suffisant pour que la JVM le considère comme non-trivial (quelle que soit la valeur de Global.DEBUG, du moins sur HotSpot 1.8), même si la super classe #finalize()est également triviale!
alip
1
@ Mike J'ai bien peur que ce soit le cas. Je l'ai testé avec (une version légèrement modifiée) du test dans l' article auquel vous avez lié , et une sortie GC détaillée (avec des performances étonnamment médiocres) confirme que les objets sont copiés dans l'espace de la génération précédente / du survivant et nécessitent un GC complet pour obtenir débarrassé de.
alip
1
Ce qui est souvent négligé, c'est le risque de collection d'objets plus tôt que prévu, ce qui rend la libération de ressources dans le finaliseur une action très dangereuse. Avant Java 9, le seul moyen de s’assurer qu’un finaliseur ne ferme pas la ressource en cours d’utilisation consiste à effectuer une synchronisation sur l’objet, à la fois dans le finaliseur et la ou les méthodes utilisant la ressource. C'est pourquoi cela fonctionne dans java.io. Si ce type de sécurité des threads ne figurait pas sur la liste de souhaits, cela augmenterait les frais généraux induits par finalize()
Holger
28

Chaque fois que j'utilise des objets que je dois fermer manuellement (comme des descripteurs de fichiers et des connexions), je suis très frustré. [...] Pourquoi n'est-il pas simplement plus simple et plus sûr de laisser le soin à la VM et au GC de se débarrasser de ces objets en mettant en close()œuvre l' implémentation Object.finalize()?

Parce que les descripteurs de fichier et les connexions (c'est-à-dire les descripteurs de fichier sur les systèmes Linux et POSIX) sont une ressource assez rare (vous pouvez être limité à 256 sur certains systèmes ou à 16384 sur d'autres; voir setrlimit (2) ). Rien ne garantit que le GC sera appelé assez souvent (ou au bon moment) pour éviter d'épuiser des ressources aussi limitées. Et si le GC n’appelle pas assez (ou si la finalisation n’est pas exécutée au bon moment), vous atteindrez cette limite (éventuellement basse).

La finalisation est une opération "au mieux" dans la machine virtuelle. Cela pourrait ne pas être appelé, ou être appelé assez tard ... En particulier, si vous avez beaucoup de RAM ou si votre programme n'alloue pas beaucoup d'objets génération par copie d'une génération), le GC peut être appelé très rarement et les finalisations risquent de ne pas s'exécuter très souvent (voire même de ne pas s'exécuter du tout).

Donc close, descripteur de fichier explicitement, si possible. Si vous avez peur de les divulguer, utilisez la finalisation comme mesure supplémentaire et non comme principale.

Basile Starynkevitch
la source
7
J'ajouterais que la fermeture d'un flux dans un fichier ou dans une socket le vide normalement. Le fait de laisser les flux ouverts augmente inutilement le risque de perte de données en cas de coupure de courant, de perte de connexion (risque également pour les fichiers accessibles
2
En fait, même si le nombre de descripteurs de fichiers autorisés est peut-être faible, ce n’est pas vraiment un problème, car on pourrait au moins l’ utiliser comme un signal pour le GC. Ce qui est vraiment problématique, c’est a) que les ressources non gérées par le GC ne sont pas entièrement transparentes vis-à-vis du GC, et b) beaucoup de ces ressources sont uniques, de sorte que d’autres pourraient être bloquées ou refusées.
Déduplicateur
2
Et si vous laissez un fichier ouvert, cela pourrait gêner son utilisation par une autre personne. (Windows 8 XPS viewer, je vous regarde!)
Loren Pechtel
2
"Si vous craignez de perdre des [descripteurs de fichier], utilisez la finalisation comme mesure supplémentaire, pas comme mesure principale." Cette déclaration me semble louche. Si vous concevez bien votre code, devriez-vous vraiment introduire du code redondant qui répartit le nettoyage sur plusieurs emplacements?
mucaho
2
@BasileStarynkevitch Votre remarque étant que dans un monde idéal, la redondance est une mauvaise chose, mais dans la pratique, il est préférable de ne pas prévoir tous les aspects pertinents.
mucaho
13

Regardez la question de cette façon: vous ne devriez écrire que du code qui est (a) correct (sinon votre programme est tout simplement faux) et (b) nécessaire (sinon votre code est trop volumineux, ce qui signifie plus de RAM, plus de cycles sont nécessaires) des choses inutiles, plus d’efforts pour le comprendre, plus de temps à le maintenir, etc. etc.)

Considérons maintenant ce que vous voulez faire dans un finaliseur. Soit c'est nécessaire. Dans ce cas, vous ne pouvez pas le mettre dans un finaliseur, car vous ne savez pas s'il sera appelé. Cela ne suffit pas. Ou si ce n'est pas nécessaire - alors vous ne devriez pas l'écrire en premier lieu! Quoi qu'il en soit, le mettre dans le finaliseur est le mauvais choix.

(Notez que les exemples que vous nommez, comme les flux de fichiers fermer, regarder comme si elles ne sont pas vraiment nécessaires, mais ils sont. Il est juste que jusqu'à ce que vous atteignez la limite pour les descripteurs de fichiers ouverts sur votre système, vous ne serez pas un avis que votre le code est incorrect. Mais cette limite est une caractéristique du système d'exploitation et est donc encore plus imprévisible que la stratégie de la JVM pour les finaliseurs, il est donc vraiment important de ne pas gaspiller les descripteurs de fichiers.)

Kilian Foth
la source
Si je suis uniquement censé écrire du code "nécessaire", dois-je éviter tout le style dans toutes mes applications d'interface graphique? Sûrement rien n'est nécessaire; les interfaces graphiques fonctionneront parfaitement sans style, elles auront juste un aspect horrible. À propos du finaliseur, il peut être nécessaire de faire quelque chose, mais vous pouvez quand même le mettre dans un finaliseur, car le finaliseur sera appelé pendant l'objet GC, le cas échéant. Si vous devez fermer une ressource, en particulier lorsqu'elle est prête pour la récupération de place, un finaliseur est optimal. Le programme met fin à la publication de votre ressource ou le finaliseur est appelé.
Kröw
Si j'ai un objet encombrant en RAM, coûteux à créer et coûteux et que je décide de le référencer faiblement afin qu'il puisse être effacé s'il n'est pas utilisé, je pourrais finalize()alors le fermer au cas où un cycle gc aurait vraiment besoin d'être libéré. RAM. Sinon, je garderais l'objet dans la RAM au lieu de devoir le régénérer et le fermer à chaque fois que je devrais l'utiliser. Bien sûr, les ressources qu’elles ouvrent ne seront libérées que lorsque l’objet est GCed, quel que soit le cas, mais je n’ai peut-être pas besoin de garantir que mes ressources soient libérées à un moment donné.
Kröw
8

L'une des principales raisons de ne pas compter sur les finaliseurs est que la plupart des ressources que l'on pourrait être tenté de nettoyer dans le finaliseur sont très limitées. Le ramasse-miettes ne s'exécute que de temps en temps, car le fait de parcourir des références pour déterminer si quelque chose peut être publié coûte cher. Cela signifie qu'il pourrait s'écouler un certain temps avant que vos objets soient réellement détruits. Si de nombreux objets ouvrent des connexions de base de données de courte durée, par exemple, laisser les finaliseurs nettoyer ces connexions pourrait épuiser votre pool de connexions en attendant que le garbage collector soit enfin exécuté et libère les connexions terminées. Ensuite, à cause de l'attente, vous vous retrouvez avec un important arriéré de demandes en file d'attente, qui épuisent rapidement le pool de connexions. Il'

De plus, l'utilisation de try-with-resources facilite la fermeture des objets 'pouvant être fermés'. Si vous n'êtes pas familier avec cette construction, je vous suggère de la vérifier: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

Chiens
la source
8

En plus de la raison pour laquelle laisser au finaliseur le soin de libérer des ressources est généralement une mauvaise idée, les objets pouvant être finalisés sont soumis à une surcharge de performances.

D'après la théorie et la pratique de Java: le ramassage des ordures et les performances (Brian Goetz), les finaliseurs ne sont pas vos amis :

Les objets dotés de finaliseurs (ceux dont la méthode finalize () est non triviale) entraînent une surcharge significative par rapport aux objets sans finaliseur et doivent être utilisés avec parcimonie. Les objets finalisables sont à la fois plus lents à allouer et à collecter. Au moment de l'allocation, la machine virtuelle Java doit enregistrer tous les objets finalisables avec le ramasse-miettes et (au moins dans l'implémentation de la machine virtuelle HotSpot), les objets finalisables doivent suivre un chemin d'allocation plus lent que la plupart des autres objets. De même, les objets finalisables sont plus lents à collecter. Il faut au moins deux cycles de récupération de place (dans le meilleur des cas) avant de pouvoir récupérer un objet définissable, et le récupérateur de place doit effectuer un travail supplémentaire pour appeler le finaliseur. Le résultat est plus de temps passé à allouer et collecter des objets et plus de pression sur le ramasse-miettes, parce que la mémoire utilisée par les objets finalisables inaccessibles est conservée plus longtemps. Combinez cela avec le fait que les finaliseurs ne sont pas garantis pour fonctionner dans un délai prévisible, voire pas du tout, et vous pouvez voir qu'il y a relativement peu de situations pour lesquelles la finalisation est le bon outil à utiliser.

alip
la source
Excellent point. Voir ma réponse qui fournit un moyen d'éviter les frais généraux liés aux performances de finalize().
Mike Nakis
7

Ma raison (la moins) préférée pour éviter Object.finalizen'est pas que les objets puissent être finalisés après que vous les attendiez, mais ils peuvent l'être avant que vous ne les attendiez. Le problème n'est pas qu'un objet toujours dans la portée puisse être finalisé avant que la portée ne soit quittée si Java décide qu'il n'est plus accessible.

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

Voir cette question pour plus de détails. Ce qui est encore plus amusant, c’est que cette décision ne pourrait être prise qu’après l’optimisation des points névralgiques, ce qui rendrait le débogage difficile.

Michael Anderson
la source
1
La chose est que cela HasFinalize.streamdevrait être lui-même un objet finalisable séparément. C'est-à-dire que la finalisation de HasFinalizene devrait pas être finalisée ou essayée de nettoyer stream. Ou s'il le devrait, alors il devrait être streaminaccessible.
acelent
4

Je dois vérifier en permanence si un objet a une implémentation de close (), et je suis certain d'avoir manqué quelques appels auparavant.

Dans Eclipse, je reçois un avertissement chaque fois que j'oublie de fermer quelque chose qui implémente Closeable/ AutoCloseable. Je ne sais pas si c'est un problème Eclipse ou s'il fait partie du compilateur officiel, mais vous pourriez envisager d'utiliser des outils d'analyse statique similaires pour vous aider. FindBugs, par exemple, peut probablement vous aider à vérifier si vous avez oublié de fermer une ressource.

Pookins Nebu
la source
1
Bonne idée à mentionner AutoCloseable. Il est très facile de gérer des ressources à l’essai avec des ressources. Cela annule plusieurs des arguments de la question.
2

A votre première question:

Il incombe à l'implémentation de la VM et au GC de déterminer le moment propice pour libérer un objet, et non le développeur. Pourquoi est-il important de décider quand Object.finalize()se faire appeler?

Eh bien, la JVM déterminera le moment opportun pour récupérer le stockage alloué à un objet. Ce n'est pas nécessairement le moment où le nettoyage de la ressource que vous souhaitez effectuer finalize()doit avoir lieu. Ceci est illustré dans la question «SOI de finalize () appelée sur un objet très joignable dans Java 8» . Là, une close()méthode a été appelée par une finalize()méthode, alors qu'une tentative de lecture du flux par le même objet est toujours en attente. Ainsi, outre la possibilité bien connue d' finalize()être appelée trop tard, il est possible qu'elle soit appelée trop tôt.

La prémisse de votre deuxième question:

Normalement, et corrigez-moi si je me trompe, le seul moment où Object.finalize()on ne m'appelle pas, c'est quand l'application est terminée avant que le GC ne puisse s'exécuter.

est tout simplement faux. Aucune machine virtuelle Java n'est nécessaire pour prendre en charge la finalisation. Eh bien, ce n’est pas complètement faux, car vous pouvez toujours l’interpréter comme «l’application a été terminée avant la finalisation», en supposant que votre application se termine un jour.

Mais notez la petite différence entre «GC» de votre déclaration initiale et le terme «finalisation». La collecte des ordures est distincte de la finalisation. Une fois que la gestion de la mémoire a détecté qu’un objet est inaccessible, il peut simplement récupérer son espace, s’il n’a pas de finalize()méthode spéciale ou si la finalisation n’est tout simplement pas prise en charge, ou peut mettre en file d'attente l’objet pour finalisation. Ainsi, l'achèvement d'un cycle de récupération de place ne signifie pas que les finaliseurs sont exécutés. Cela peut arriver plus tard, lors du traitement de la file d'attente, ou jamais du tout.

C'est également la raison pour laquelle, même sur les machines virtuelles avec prise en charge de la finalisation, il est dangereux de s'y fier pour le nettoyage des ressources. La récupération de place fait partie de la gestion de la mémoire et est donc déclenchée par des besoins en mémoire. Il est possible que la récupération de place ne s'exécute jamais car la mémoire est suffisante pendant toute la durée d'exécution (enfin, cela correspond toujours à la description «l'application a été arrêtée avant que le GC n'ait eu la chance de s'exécuter»). Il est également possible que le CPG s'exécute, mais qu'après, la mémoire est suffisamment récupérée pour que la file d'attente du finaliseur ne soit pas traitée.

En d'autres termes, les ressources natives gérées de cette manière sont toujours étrangères à la gestion de la mémoire. Bien qu'il soit garanti qu'un OutOfMemoryErrorest lancé uniquement après un nombre suffisant de tentatives pour libérer de la mémoire, cela ne s'applique pas aux ressources natives ni à la finalisation. Il est possible que l'ouverture d'un fichier échoue en raison de ressources insuffisantes alors que la file d'attente de finalisation est pleine d'objets susceptibles de libérer ces ressources, si le finaliseur s'exécutait un jour…

Holger
la source