Les deux principaux arguments contre la substitution de Object.finalize()
sont les suivants:
Vous ne pouvez pas décider quand il est appelé.
Il n'est peut-être pas du tout appelé.
Si je comprends bien, je ne pense pas que ce soient de bonnes raisons de détester autant Object.finalize()
.
C'est à l'implémentation VM et au GC de déterminer le moment opportun pour désallouer un objet, pas au développeur. Pourquoi est-il important de décider quand Object.finalize()
obtient appelé?
Normalement, et corrigez-moi si je me trompe, le seul moment où Object.finalize()
ne serait pas appelé est lorsque l'application a été arrêtée avant que le GC ait la chance de s'exécuter. Cependant, l'objet a été désalloué de toute façon lorsque le processus de l'application s'est terminé avec lui. Ainsi, Object.finalize()
n'a pas été appelée car il n'était pas nécessaire de l'appeler. Pourquoi le développeur s'en soucierait-il?
Chaque fois que j'utilise des objets que je dois fermer manuellement (comme les descripteurs de fichiers et les connexions), je suis très frustré. Je dois constamment vérifier si un objet a une implémentation de close()
, et je suis sûr que j'ai manqué quelques appels à certains moments dans le passé. Pourquoi n'est-il pas simplement plus simple et plus sûr de laisser le soin à VM et GC de disposer de ces objets en mettant l'implémentation close()
dans Object.finalize()
?
D'après mon expérience, il y a un et un seul raison de remplacer Object.finalize()
, mais il est une très bonne raison:
Pour placer le code d'enregistrement des erreurs dans
finalize()
qui vous avertit si vous oubliez d'appelerclose()
.
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 des choses que vous devez réellement les désactiver afin d'obtenir quelque chose de non trivial. (J'ai beaucoup plus d'avertissements activés que tout autre programmeur que je connaisse ou dont j'ai jamais entendu parler, mais je n'ai pas activé d'avertissements stupides.)
La finalisation peut sembler être un bon mécanisme pour s'assurer que les ressources ne sont pas indisponibles, mais la plupart des gens le voient de manière complètement erronée: ils le voient comme un mécanisme de secours alternatif, une sauvegarde de "seconde chance" qui sauvera automatiquement le jour en disposant des ressources qu'ils ont oubliées. C'est complètement faux. Il ne doit y avoir qu'une seule façon de faire quelque chose: soit vous fermez toujours tout, soit la finalisation ferme toujours tout. Mais comme la finalisation n'est pas fiable, la finalisation ne peut pas l'être.
Donc, il y a ce schéma que j'appelle Disposition obligatoire, et il stipule que le programmeur est responsable de toujours fermer explicitement tout qui implémente Closeable
ou AutoCloseable
. (L'instruction try-with-resources compte toujours comme une fermeture explicite.) Bien sûr, le programmeur peut oublier, c'est donc là que la finalisation entre en jeu, mais pas comme une fée magique qui fera magiquement les choses à la fin: si la finalisation découvre que close()
n'a pas été invoquée, elle essaie pas d'essayer de l'invoquer, précisément parce qu'il y aura (avec une certitude mathématique) Les programmeurs n00b qui comptent sur lui pour faire le travail qu'ils étaient trop paresseux ou trop absents pour faire. Ainsi, avec l'élimination obligatoire, lorsque la finalisation découvre que close()
n'a pas été invoquée, elle enregistre un message d'erreur rouge vif, indiquant au programmeur avec de grosses lettres majuscules pour réparer son s-- er, ses trucs.
Comme avantage supplémentaire, selon la rumeur que "la JVM ignorera une méthode triviale finalize () (par exemple, une qui retourne sans rien faire, comme celle définie dans la classe Object)", donc avec élimination obligatoire, vous pouvez éviter tous les frais généraux de finalisation dans l'ensemble de votre système ( voir la réponse d'Alip pour plus d'informations sur la gravité de ces frais généraux) en codant votre méthode finalize()
comme ceci:
@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 derrière cela est que Global.DEBUG
est un static final
variable dont la valeur est connue au moment de la compilation, donc si elle est false
alors le compilateur n'émettra aucun code pour la totalité de l'instruction if
, ce qui en fera un trivial (vide) finaliseur, ce qui signifie que votre classe sera traitée comme si elle n'avait pas de finaliseur. (En C # cela se ferait avec un Nice #if DEBUG
bloquer, mais que pouvons-nous faire, c'est Java, où nous payons une simplicité apparente dans le code avec des frais supplémentaires dans le cerveau.)
En savoir plus sur l'élimination obligatoire, avec une discussion supplémentaire sur l'élimination des ressources dans dot Net, ici: michael.gr: élimination obligatoire vs abomination "Éliminer-éliminer"
Chaque fois que j'utilise des objets que je dois fermer manuellement (comme les descripteurs de fichiers et les connexions), je suis très frustré. [...] Pourquoi n'est-il pas plus simple et plus sûr de laisser le soin à VM et GC de disposer de ces objets en mettant l'implémentation
close()
dansObject.finalize()
?
Parce que les descripteurs et connexions de fichiers (c'est-à-dire descripteurs de fichiers sur les systèmes Linux et POSIX) sont une ressource assez rare (vous pourriez être limité à 256 d'entre eux sur certains systèmes ou à 16384 sur d'autres; voir setrlimit (2) ). Rien ne garantit que le GC sera appelé suffisamment souvent (ou au bon moment) pour éviter d'épuiser des ressources aussi limitées. Et si le GC n'est pas suffisamment appelé (ou que la finalisation n'est pas exécutée au bon moment), vous atteindrez cette limite (peut-être basse).
La finalisation est un "meilleur effort" dans la JVM. Il 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 (ou si la plupart des ils meurent avant d'être transmis à une génération assez ancienne par un GC générationnel copiant), le GC pourrait être appelé assez rarement, et les finalisations pourraient ne pas fonctionner très souvent (ou même ne pas fonctionner du tout).
Donc, les descripteurs de fichier close
explicitement, si possible. Si vous avez peur de les divulguer, utilisez la finalisation comme mesure supplémentaire, et non comme principale.
Regardez la question de cette façon: vous ne devez écrire que du code qui est (a) correct (sinon votre programme est tout simplement faux) et (b) nécessaire (sinon votre code est trop gros, ce qui signifie plus RAM nécessaire, plus de cycles consacrés à des choses inutiles, plus d'efforts pour le comprendre, plus de temps passé à le maintenir, etc. etc.)
Considérez 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é. Ce n'est pas assez bon. Ou 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 n'est pas le bon choix.
(Notez que les exemples que vous nommez, comme la fermeture de flux de fichiers, semblent comme s'ils n'étaient pas vraiment nécessaires, mais ils le sont. C'est juste que jusqu'à ce que vous atteignez la limite pour les descripteurs de fichiers ouverts sur votre système, vous ne remarquerez pas que votre code est incorrect. Mais cette limite est une caractéristique du système d'exploitation et est donc encore plus imprévisible que la politique de la JVM pour les finaliseurs, donc il est vraiment, vraiment est important de ne pas gaspiller les descripteurs de fichiers.)
L'une des principales raisons de ne pas se fier aux finaliseurs est que la plupart des ressources que l'on pourrait être tenté de nettoyer dans le finaliseur sont très limitées. Le garbage collector ne s'exécute que de temps en temps, car parcourir les références pour déterminer si quelque chose peut être libéré est coûteux. Cela signifie qu'il pourrait s'écouler un certain temps avant que vos objets ne soient réellement détruits. Si vous avez beaucoup d'objets ouvrant des connexions de base de données de courte durée, par exemple, laisser les finaliseurs pour nettoyer ces connexions pourrait épuiser votre pool de connexions pendant qu'il attend que le garbage collector s'exécute enfin et libère les connexions terminées. Ensuite, en raison de l'attente, vous vous retrouvez avec un important arriéré de demandes en file d'attente, qui épuisent rapidement le pool de connexions. C'est un cycle qui mettra rapidement votre application dans un état de fonctionnement paralysé.
De plus, l'utilisation de try-with-resources facilite la fermeture des objets "fermables" à la fin. 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
En plus de la raison pour laquelle le fait de laisser au finaliseur le soin de libérer des ressources est généralement une mauvaise idée, les objets finalisables s'accompagnent d'un surcoût de performance.
Extrait de Théorie et pratique Java: Collecte des ordures et performances (Brian Goetz), Les finaliseurs ne sont pas vos amis :
Les objets avec finaliseurs (ceux qui ont une méthode non triviale finalize ()) ont un surdébit significatif par rapport aux objets sans finaliseurs et doivent être utilisés avec parcimonie. Les objets finalisables sont à la fois plus lents à allouer et plus lents à collecter. Au moment de l'allocation, la JVM doit enregistrer tous les objets finalisables auprès du garbage collector et (au moins dans l'implémentation HotSpot JVM) les objets finalisables doivent suivre un chemin d'allocation plus lent que la plupart des autres objets. De même, les objets finalisables sont également plus lents à collecter. Il faut au moins deux cycles de récupération de place (dans le meilleur des cas) avant qu'un objet finalisable puisse être récupéré, et le ramasse-miettes doit faire 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, car 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, ou même pas du tout, et vous pouvez voir qu'il y a relativement peu de situations pour lesquelles la finalisation est le bon outil à utiliser.
Ma (la moins) raison préférée pour éviter Object.finalize
n'est pas que les objets peuvent être finalisés après que vous vous y attendiez, mais ils peuvent être finalisés avant que vous ne vous y attendiez. Le problème n'est pas qu'un objet qui est encore dans la portée peut ê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 peut être prise qu'après une optimisation des points chauds, ce qui rend le débogage difficile.
Je dois constamment vérifier si un objet a une implémentation de close (), et je suis sûr que j'ai raté quelques appels à certains moments dans le passé.
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 une chose Eclipse ou si elle fait partie du compilateur officiel, mais vous pourriez envisager d'utiliser des outils d'analyse statique similaires pour vous y aider. FindBugs, par exemple, peut probablement vous aider à vérifier si vous avez oublié de fermer une ressource.
À votre première question:
C'est à l'implémentation VM et au GC de déterminer le moment opportun pour désallouer un objet, pas au développeur. Pourquoi est-il important de décider quand
Object.finalize()
obtient appelé?
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 dans finalize()
, doit se produire. Ceci est illustré dans la question "finalize () appelée sur un objet fortement accessible dans Java 8" sur SO. Là, une méthode close()
a été appelé par une méthode finalize()
, alors qu'une tentative de lecture du flux par le même objet est toujours en attente. Ainsi, outre la possibilité bien connue que finalize()
soit appelé trop tard est la possibilité qu'il soit appelé 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()
ne serait pas appelé est lorsque l'application a été arrêtée avant que le GC ait la chance de s'exécuter.
est tout simplement faux. Il n'est pas nécessaire qu'une JVM prenne en charge la finalisation. Eh bien, ce n'est pas tout à fait faux car vous pouvez toujours l'interpréter comme "l'application s'est terminée avant la finalisation", en supposant que votre application se terminera jamais.
Mais notez la petite différence entre "GC" de votre déclaration d'origine et le terme "finalisation". La collecte des ordures est distincte de la finalisation. Une fois que la gestion de la mémoire détecte qu'un objet est inaccessible, elle peut simplement récupérer son espace, si non, elle n'a pas de méthode spéciale finalize()
ou la finalisation n'est simplement pas prise en charge, ou elle peut mettre l'objet en file d'attente pour finalisation . Ainsi, l'achèvement d'un cycle de récupération de place n'implique pas que les finaliseurs sont exécutés. Cela peut se produire ultérieurement, lorsque la file d'attente est traitée, ou jamais du tout.
Ce point est également la raison pour laquelle même sur les machines virtuelles Java avec prise en charge de la finalisation, le recours à celui-ci pour le nettoyage des ressources est dangereux. Le garbage collection fait partie de la gestion de la mémoire et est donc déclenché par les besoins en mémoire. Il est possible que le ramasse-miettes ne s'exécute jamais car il y a suffisamment de mémoire 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 ait la chance de s'exécuter"). Il est également possible que le GC s'exécute, mais par la suite, il y a suffisamment de mémoire récupérée, de sorte que la file d'attente du finaliseur n'est 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 OutOfMemoryError
ne soit lancé qu'après suffisamment de tentatives pour libérer de la mémoire, cela ne s'applique pas aux ressources natives et à 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 qui pourraient libérer ces ressources, si jamais le finaliseur s'exécutait…