GC.Collect () et finaliser

Ok, on sait que GC appelle implicitement les méthodes Finalize sur les objects quand il identifie cet object comme une erreur. Mais que se passe-t-il si je fais un GC.Collect() ? Les finaliseurs sont-ils toujours exécutés? Une question stupide peut-être, mais quelqu’un m’a posé la question et j’ai répondu “oui”, puis j’ai pensé: ” Est-ce que c’était tout à fait correct?

Ok, on sait que GC appelle implicitement les méthodes Finalize sur les objects quand il identifie cet object comme une erreur.

Non non Non. Cela n’est pas connu, car pour être informé, un énoncé doit être vrai . Cette déclaration est fausse . Le garbage collector n’exécute pas les finaliseurs au fur et à mesure de leur suivi , qu’il s’exécute lui-même ou que vous appeliez Collect . Le thread du finaliseur exécute les finaliseurs après que le collecteur de suivi a détecté le problème et que cela se produit de manière asynchrone par rapport à un appel à Collect . (Si cela se produit, ce qui peut ne pas être le cas, comme l’indique une autre réponse). En d’autres termes, vous ne pouvez pas compter sur le thread du finaliseur qui s’exécute avant que le contrôle ne revienne de Collect .

Voici un schéma simplifié de son fonctionnement:

  • Lorsqu’une collection se produit, le thread de traçage du ramasse-miettes trace les racines – les objects connus pour être vivants et tous les objects auxquels ils se réfèrent, etc. – pour déterminer les objects morts.
  • Les objects “morts” qui ont des finaliseurs en attente sont déplacés dans la queue du finaliseur. La queue du finaliseur est une racine . Par conséquent, ces objects “morts” sont encore vivants .
  • Le thread du finaliseur, qui est généralement un thread différent du thread de suivi du GC, est finalement exécuté et vide la queue du finaliseur. Ces objects deviennent alors vraiment morts et sont rassemblés dans la collection suivante sur le fil de trace. (Bien sûr, comme ils ont juste survécu à la première collection, ils pourraient être dans une génération supérieure.)

Comme je l’ai dit, c’est trop simplifié; les détails exacts du fonctionnement de la queue du finaliseur sont un peu plus compliqués que cela. Mais l’idée est assez répandue. Le résultat pratique ici est que vous ne pouvez pas supposer que l’appel de Collect exécute également des finaliseurs , car ce n’est pas le cas. Permettez-moi de répéter cela une fois de plus: la partie de traçage du ramasse-miettes ne lance pas les finaliseurs , et Collect exécute uniquement la partie traçage du mécanisme de collecte.

Appelez le bien nommé WaitForPendingFinalizers après avoir appelé Collect si vous voulez garantir que tous les finaliseurs ont été exécutés. Cela mettra le thread actuel en pause jusqu’à ce que le thread du finaliseur parvienne à vider la file d’attente. Et si vous voulez vous assurer que ces objects finalisés récupèrent leur mémoire, vous devrez appeler Collect une deuxième fois.

Et bien sûr, il va sans dire que vous ne devriez le faire qu’à des fins de débogage et de test. Ne faites jamais ce non-sens dans le code de production sans une très bonne raison.

En fait, la réponse “ça dépend”. En fait, il existe un thread dédié qui exécute tous les finaliseurs. Cela signifie que l’appel à GC.Collect n’a déclenché que ce processus et que l’exécution de tous les finaliseurs serait appelée de manière asynchrone.

Si vous voulez attendre que tous les finaliseurs soient appelés, vous pouvez utiliser l’astuce suivante:

 GC.Collect(); // Waiting till finilizer thread will call all finalizers GC.WaitForPendingFinalizers(); 

Oui, mais pas tout de suite. Cet extrait provient de Garbage Collection: gestion automatique de la mémoire dans Microsoft .NET Framework (MSDN Magazine) (*)

“Lorsqu’une application crée un nouvel object, le nouvel opérateur alloue la mémoire à partir du tas. Si le type de l’object contient une méthode Finalize, un pointeur sur l’object est placé dans la queue de finalisation. La queue de finalisation est contrôlée Chaque entrée de la queue pointe sur un object dont la méthode Finalize doit être appelée avant que la mémoire de l’object puisse être récupérée.

Lorsqu’un GC se produit … le ramasse-miettes parsing la queue de finalisation à la recherche de pointeurs vers ces objects. Lorsqu’un pointeur est trouvé, le pointeur est supprimé de la queue de finalisation et ajouté à la queue accessible (prononcé “F-accessible”). La queue interchangeable est une autre structure de données interne contrôlée par le ramasse-miettes. Chaque pointeur de la queue à access direct identifie un object prêt à faire appel à sa méthode Finalize.

Il existe un thread d’exécution spécial dédié à l’appel des méthodes Finalize. Lorsque la queue accessible est vide (ce qui est généralement le cas), ce thread s’endort. Mais lorsque des entrées apparaissent, ce thread se réveille, supprime chaque entrée de la queue et appelle la méthode Finalize de chaque object. Pour cette raison, vous ne devez pas exécuter de code dans une méthode Finalize qui émet une hypothèse sur le thread qui exécute le code. Par exemple, évitez d’accéder au stockage local de threads dans la méthode Finalize. ”

(*) Depuis novembre 2000, les choses ont peut-être changé depuis.

Lorsque la corbeille est collectée (que ce soit en réponse à la pression de la mémoire ou à GC.Collect() ), les objects à finaliser sont placés dans la queue de finalisation.

Sauf si vous appelez GC.WaitForPendingFinalizers() , les finaliseurs peuvent continuer à s’exécuter en arrière-plan longtemps après la fin de la récupération de place.


BTW, il n’y a pas de garantie que les finaliseurs seront appelés du tout . De MSDN …

La méthode Finalize peut ne pas s’exécuter complètement ou peut ne pas s’exécuter du tout dans les circonstances exceptionnelles suivantes:

  • Un autre finaliseur se bloque indéfiniment (entre dans une boucle infinie, essaie d’obtenir un verrou impossible à obtenir, etc.). Étant donné que le runtime tente d’exécuter les finaliseurs, d’autres finaliseurs peuvent ne pas être appelés si un finaliseur se bloque indéfiniment.
  • Le processus se termine sans que le runtime ait une chance de se nettoyer. Dans ce cas, la première notification de l’exécution du processus à l’exécution est une notification DLL_PROCESS_DETACH.

Le moteur d’exécution continue à finaliser les objects lors de l’arrêt uniquement pendant que le nombre d’objects finalisables continue à diminuer.

Quelques points supplémentaires valent la peine d’être mentionnés ici.

Finalizer est le dernier point où les objects .net peuvent libérer des ressources non managées. Les finaliseurs ne doivent être exécutés que si vous ne disposez pas de vos instances correctement. Idéalement, les finaliseurs ne devraient jamais être exécutés dans de nombreux cas. Parce que l’implémentation de la disposition appropriée doit supprimer la finalisation .

Voici un exemple pour une implémentation IDispoable correcte .

Si vous appelez la méthode Dispose de tous les objects jetables, toutes les références doivent être effacées et la suppression terminée. S’il y a des développeurs qui ne sont pas très bons et qui oublient d’appeler la méthode Dispose, Finalizer est le sauveteur.