web-dev-qa-db-fra.com

Comprendre la récupération de place dans .NET

Considérons le code ci-dessous:

public class Class1
{
    public static int c;
    ~Class1()
    {
        c++;
    }
}

public class Class2
{
    public static void Main()
    {
        {
            var c1=new Class1();
            //c1=null; // If this line is not commented out, at the Console.WriteLine call, it prints 1.
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Class1.c); // prints 0
        Console.Read();
    }
}

Maintenant, même si la variable c1 de la méthode principale est hors de portée et n'est référencée plus par aucun autre objet lorsque GC.Collect() est appelée, pourquoi n'est-elle pas finalisée ici?

158
Victor Mukherjee

Vous êtes en train de trébucher ici et tirez des conclusions très fausses parce que vous utilisez un débogueur. Vous devrez exécuter votre code de la manière dont il est exécuté sur la machine de votre utilisateur. Basculez d'abord vers la version Release avec Build + Configuration Manager, puis modifiez le combo "Configuration de la solution active" dans le coin supérieur gauche en "Release". Ensuite, allez dans Outils + Options, Débogage, Général et décochez l'option "Supprimer l'optimisation JIT".

Maintenant, relancez votre programme et bricolez le code source. Notez que les accolades supplémentaires n'ont aucun effet. Et notez comment définir la variable sur null ne fait aucune différence. Il imprimera toujours "1". Cela fonctionne maintenant comme vous le souhaitiez et espérait que cela fonctionnerait.

Ce qui laisse à la tâche d’expliquer pourquoi cela fonctionne si différemment lorsque vous exécutez la construction Debug. Cela nécessite d'expliquer comment le ramasse-miettes découvre les variables locales et comment cela est affecté par la présence d'un débogueur.

Tout d’abord, la gigue remplit deux tâches importantes lorsqu’elle compile l’IL d’une méthode en code machine. Le premier est très visible dans le débogueur, vous pouvez voir le code machine avec la fenêtre Débogage + Windows + Désassemblage. Le second devoir est cependant complètement invisible. Il génère également un tableau décrivant comment les variables locales du corps de la méthode sont utilisées. Cette table a une entrée pour chaque argument de méthode et une variable locale avec deux adresses. Adresse où la variable stockera d'abord une référence d'objet. Et l'adresse de l'instruction de code machine où cette variable n'est plus utilisée. Indiquez également si cette variable est stockée dans le cadre de la pile ou dans un registre cpu.

Ce tableau est essentiel pour le ramasse-miettes. Il doit savoir où chercher les références d’objet lorsqu’il effectue une collecte. Assez facile à faire lorsque la référence fait partie d’un objet du tas GC. Ce n'est vraiment pas facile à faire lorsque la référence de l'objet est stockée dans un registre de la CPU. Le tableau indique où regarder.

L'adresse "non utilisée" dans le tableau est très importante. Cela rend le ramasse-miettes très efficace . Il peut collecter une référence d'objet, même si elle est utilisée dans une méthode et que cette méthode n'a pas encore fini de s'exécuter. Ce qui est très courant, votre méthode Main () par exemple, ne cessera de s’exécuter que juste avant la fin de votre programme. Il est clair que vous ne voudriez pas que les références d’objet utilisées dans cette méthode Main () restent en vie pendant la durée du programme, cela équivaudrait à une fuite. La gigue peut utiliser la table pour découvrir qu'une telle variable locale n'est plus utile, en fonction de l'état d'avancement du programme dans la méthode Main () avant l'appel.

Une méthode presque magique liée à cette table est GC.KeepAlive (). C'est une méthode spéciale très , elle ne génère aucun code. Son seul devoir est de modifier cette table. Il étend la durée de vie de la variable locale, empêchant ainsi la référence stockée de se faire ramasser. La seule fois que vous devez l'utiliser est d'empêcher le CPG d'être trop pressé de collecter une référence, ce qui peut arriver dans les scénarios d'interopérabilité dans lesquels une référence est transmise au code non managé. Le ramasse-miettes ne peut pas voir que de telles références sont utilisées par un tel code, car il n'a pas été compilé par la gigue. Par conséquent, il n'a pas de table qui indique où chercher la référence. Le passage d'un objet délégué à une fonction non gérée, telle que EnumWindows (), constitue l'exemple type à suivre lorsque vous devez utiliser GC.KeepAlive ().

Ainsi, comme vous pouvez le constater à partir de votre extrait de code après l'avoir exécuté dans la version Release, les variables locales peuvent être collectées tôt, avant l'exécution de la méthode. Encore plus puissamment, un objet peut être collecté pendant qu'une de ses méthodes s'exécute si cette méthode ne fait plus référence à this . Cela pose un problème, il est très délicat de déboguer une telle méthode. Puisque vous pouvez bien mettre la variable dans la fenêtre de surveillance ou l'inspecter. Et il disparaîtrait pendant le débogage si un GC se produisait. Ce serait très désagréable, donc la gigue est consciente qu'il y a un débogueur attaché. Il modifie alors la table et modifie la "dernière adresse utilisée". Et le change de sa valeur normale à l'adresse de la dernière instruction de la méthode. Ce qui maintient la variable active tant que la méthode n'est pas retournée. Ce qui vous permet de continuer à l'observer jusqu'au retour de la méthode.

Cela explique aussi ce que vous avez vu précédemment et pourquoi vous avez posé la question. Il imprime "0" car l'appel GC.Collect ne peut pas collecter la référence. Le tableau indique que la variable est en cours d'utilisation après l'appel de GC.Collect (), jusqu'à la fin de la méthode. Obligé de le dire en ayant le débogueur attaché et en exécutant la construction Debug.

Définir la variable sur null a maintenant un effet, car le CPG inspectera la variable et ne verra plus de référence. Mais assurez-vous de ne pas tomber dans le piège dans lequel sont tombés de nombreux programmeurs C #, écrire ce code était inutile. Peu importe que cette déclaration soit présente ou non, que vous exécutiez le code dans la version Release. En fait, l’optimiseur de gigue va enlever cette instruction car elle n’a aucun effet. Veillez donc à ne pas écrire un code comme celui-ci, même s'il semble avoir un effet.


Une dernière note à ce sujet, c’est ce qui cause des problèmes aux programmeurs qui écrivent de petits programmes pour faire quelque chose avec une application Office. Le débogueur les met généralement sur le chemin incorrect, ils souhaitent que le programme Office se ferme à la demande. La manière appropriée de le faire est d'appeler GC.Collect (). Mais ils découvriront que cela ne fonctionne pas lorsqu'ils déboguent leur application, ce qui les conduit à ne jamais atterrir en appelant Marshal.ReleaseComObject (). Gestion manuelle de la mémoire, cela fonctionne rarement correctement car ils oublient facilement une référence d'interface invisible. GC.Collect () fonctionne réellement, mais pas lorsque vous déboguez l'application.

329
Hans Passant

[Je voulais juste ajouter davantage sur le processus de finalisation des éléments internes du processus]

Donc, vous créez un objet et lorsque l'objet est collecté, la méthode Finalize de l'objet doit être appelée. Mais la finalisation ne se limite pas à cette hypothèse très simple.

COURTS CONCEPTS ::

  1. Les objets n'implémentant pas les méthodes Finalize, la mémoire y est immédiatement récupérée, à moins bien sûr, ils ne sont pas accessibles par
    code d'application plus

  2. Objets implémentant Finalize Méthode, Le Concept/Implémentation de Application Roots, Finalization Queue, Freacheable Queue vient avant qu'ils puissent être récupérés.

  3. Tout objet est considéré comme un déchet s'il n'est PAS accessible par le code d'application.

Assume :: Les classes/objets A, B, D, G, H n'implémentent PAS la méthode Finalize ni les méthodes C, E, F, I, J Finalize la méthode.

Lorsqu'une application crée un nouvel objet, l'opérateur new alloue la mémoire du tas. Si le type de l'objet contient une méthode Finalize, un pointeur sur l'objet est placé dans la file d'attente de finalisation.

les pointeurs sur les objets C, E, F, I, J sont donc ajoutés à la file d'attente de finalisation.

La file d'attente de finalisation est une structure de données interne contrôlée par le ramasse-miettes. Chaque entrée de la file d'attente pointe vers un objet dont la méthode Finalize doit être appelée avant que la mémoire de l'objet puisse être récupérée. La figure ci-dessous montre un segment de mémoire contenant plusieurs objets. Certains de ces objets sont accessibles depuis les racines de l'application, d'autres non. Lorsque les objets C, E, F, I et J ont été créés, le framework .Net détecte que ces objets ont des méthodes Finalize et des pointeurs sur ces objets sont ajoutés à l'élément = file d'attente de finalisation.

enter image description here

Lorsqu'un GC se produit (1ère collection), les objets B, E, G, H, I et J sont considérés comme des déchets. Parce que A, C, D, F sont toujours accessibles par le code d'application représenté par les flèches à partir de l'encadré jaune ci-dessus.

Le garbage collector analyse la file d'attente de finalisation à la recherche de pointeurs sur ces objets. Lorsqu'un pointeur est trouvé, il est supprimé de la file d'attente de finalisation et ajouté à la file d'attente pouvant être parcourue ("F-accessible").

La file d'attente consultable est une autre structure de données interne contrôlée par le ramasse-miettes. Chaque pointeur de la file d'attente consultable identifie un objet prêt à faire appeler sa méthode Finalize .

Après la collecte (1ère collection), le tas géré ressemble à la figure ci-dessous. Explication donnée ci-dessous ::
1.) La mémoire occupée par les objets B, G et H a été récupérée immédiatement car ces objets ne disposaient pas d'une méthode de finalisation devant être appelée.

2.) Cependant, la mémoire occupée par les objets E, I et J n'a pas pu être récupérée car leur méthode Finalize n'a pas encore été appelée. L'appel de la méthode Finalize est terminé par file d'attente fragile.

3.) Les codes A, C, D, F sont toujours accessibles via le code d'application représenté par les flèches dans l'encadré jaune ci-dessus, ils ne seront donc en aucun cas collectés

enter image description here

Un thread d'exécution spécial est dédié à l'appel des méthodes Finalize. Lorsque la file d'attente consultable est vide (ce qui est généralement le cas), ce thread est en veille. Mais lorsque des entrées apparaissent, ce thread se réveille, supprime chaque entrée de la file d'attente et appelle la méthode Finalize de chaque objet. Le ramasse-miettes compacte la mémoire récupérable et le thread d'exécution spécial vide la file d'attente pouvant être parcourue , en exécutant la méthode Finalize de chaque objet. Donc voici enfin le moment où votre méthode Finalize est exécutée

La prochaine fois que le ramasse-miettes est appelé (2nd Collection), il voit que les objets finalisés sont vraiment des miettes, car les racines de l'application ne pointent pas dessus et la file d'attente consultable ne pointe plus vers c'est (c'est VIDE aussi), donc la mémoire pour les objets (E, I, J) est simplement récupérée à partir de Heap.Voir la figure ci-dessous et comparez-la avec la figure juste au-dessus

enter image description here

La chose importante à comprendre ici est que deux GC sont nécessaires pour récupérer la mémoire utilisée par objets qui nécessitent une finalisation . En réalité, plus de deux collections peuvent même être nécessaires puisque ces objets peuvent être promus à une génération plus ancienne

NOTE :: La file d'attente pouvant être parcourue est considérée comme étant une racine comme les variables globales et statiques sont des racines. Par conséquent, si un objet se trouve dans la file d'attente consultable, il est accessible et n'est pas corrompu.

Enfin, rappelez-vous que l’application de débogage est une chose, la récupération de place en est une autre et fonctionne différemment. Jusqu’à présent, vous ne pouvez pas avoir le sentiment que la récupération de place a été effectuée simplement en déboguant des applications. Si vous souhaitez également étudier la mémoire, commencez ici .

31
R.C

Vous pouvez implémenter la gestion de la mémoire de trois manières différentes: -

Le GC ne fonctionne que pour les ressources gérées. Par conséquent, .NET fournit Dispose et Finalize pour libérer les ressources non gérées telles que le flux, la connexion à la base de données, les objets COM, etc.

1) Éliminer

Dispose doit être appelé explicitement pour les types qui implémentent IDisposable.

Le programmeur doit appeler cela soit en utilisant Dispose (), soit en utilisant Using

Utilisez GC.SuppressFinalize (this) pour empêcher l’appel de Finalizer si vous avez déjà utilisé dispose ().

2) Finaliser ou Distructeur

Il est appelé implicitement après que l'objet est éligible pour le nettoyage, le finaliseur pour les objets est appelé séquentiellement par le fil du finaliseur.

L’inconvénient de l’implémentation du finaliseur est que sa récupération de mémoire est retardée car le finaliseur de ces classes/types doit être appelé nettoyage préalable, ce qui en fait un autre moyen de récupérer de la mémoire.

3) GC.Collect ()

L'utilisation de GC.Collect () ne met pas nécessairement GC en collection, il peut toujours remplacer et exécuter à tout moment.

de plus, GC.Collect () n'exécutera que la partie traçage du garbage collection et ajoutera des éléments à la file d'attente du finaliseur, mais n'appellera pas les finaliseurs pour les types gérés par un autre thread.

Utilisez WaitForPendingFinalizers si vous voulez vous assurer que tous les finaliseurs ont été appelés après l'appel de GC.Collect ()

2
Pankaj Singh