200 votes

Comprendre le ramassage des déchets 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 dans la méthode main est hors de portée et n'est plus référencée par aucun autre objet lorsque GC.Collect() est appelé, pourquoi n'est-il pas finalisé à cet endroit ?

9 votes

Le GC ne libère pas immédiatement les instances lorsqu'elles sont hors de portée. Il le fait quand il pense que c'est nécessaire. Vous pouvez lire tout ce qui concerne le GC ici : msdn.microsoft.com/fr/US/library/vstudio/0xy59wtx.aspx

0 votes

@user1908061 (Pssst. Votre lien est cassé.)

0 votes

Quelques articles : GC | GC | GC | GC | GC | GC

396voto

Hans Passant Points 475940

Vous vous trompez ici et tirez des conclusions erronées parce que vous utilisez un débogueur. Vous devez exécuter votre code de la manière dont il s'exécute sur la machine de votre utilisateur. Passez d'abord à la version Release avec le gestionnaire Build + Configuration, changez la combinaison "Active solution configuration" 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, exécutez à nouveau votre programme et manipulez le code source. Remarquez que les accolades supplémentaires n'ont aucun effet. Et notez comment le fait de mettre la variable à null ne fait aucune différence. Le programme imprimera toujours "1". Le programme fonctionne maintenant comme vous l'espériez et l'attendiez.

Ce qui laisse la tâche d'expliquer pourquoi cela fonctionne si différemment lorsque vous exécutez la version Debug. Pour cela, il faut expliquer comment le ramasseur d'ordures découvre les variables locales et comment cela est affecté par la présence d'un débogueur.

Tout d'abord, la gigue exécute deux des fonctions importantes lorsqu'il compile l'IL d'une méthode en code machine. La première est très visible dans le débogueur, vous pouvez voir le code machine avec la fenêtre Debug + Windows + Disassembly. La deuxième tâche est en revanche totalement invisible. Il génère également un tableau qui décrit comment les variables locales à l'intérieur du corps de la méthode sont utilisées. Ce tableau comporte une entrée pour chaque argument de méthode et chaque variable locale avec deux adresses. L'adresse où la variable va d'abord stocker une référence d'objet. Et l'adresse de l'instruction de code machine où cette variable n'est plus utilisée. Il est également possible de savoir si cette variable est stockée sur la pile ou dans un registre du processeur.

Cette table est essentielle pour le ramasseur d'ordures, il doit savoir où chercher les références d'objets lorsqu'il effectue une collecte. C'est assez facile à faire quand la référence fait partie d'un objet sur le tas du GC. Ce n'est certainement pas facile à faire lorsque la référence de l'objet est stockée dans un registre du CPU. La table indique où chercher.

L'adresse "plus utilisée" dans la table est très importante. Elle rend le ramasseur d'ordures très efficace . Il peut collecter une référence d'objet, même si elle est utilisée à l'intérieur d'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 s'arrêtera que juste avant la fin de votre programme. Il est clair que vous ne voudriez pas que les références d'objets utilisées dans cette méthode Main() soient conservées pendant toute la durée du programme, ce qui constituerait une fuite. La gigue peut utiliser la table pour découvrir qu'une telle variable locale n'est plus utile, en fonction de l'avancement du programme dans cette méthode Main() avant qu'il n'effectue un appel.

Une méthode presque magique qui est liée à cette table est GC.KeepAlive(). Il s'agit d'une méthode très méthode spéciale, elle ne génère pas de code du tout. Son seul devoir est de modifier ce tableau. Il étend la durée de vie de la variable locale, empêchant la référence qu'elle stocke d'être collectée par la GC. La seule fois où vous devez l'utiliser est pour empêcher le GC d'être trop impatient de collecter une référence, ce qui peut arriver dans des scénarios d'interopérabilité où une référence est passée à du code non géré. Le ramasseur d'ordures ne peut pas voir de telles références utilisées par un tel code puisqu'il n'a pas été compilé par le jitter et ne dispose donc pas de la table qui indique où chercher la référence. Le passage d'un objet délégué à une fonction non gérée comme EnumWindows() est l'exemple type où vous devez utiliser GC.KeepAlive().

Ainsi, comme vous pouvez le constater à partir de votre exemple après l'avoir exécuté dans la version Release, les variables locales peut sont collectées tôt, avant que la méthode ne finisse de s'exécuter. Encore plus puissant, un objet peut être collecté pendant l'exécution d'une de ses méthodes si cette méthode ne fait plus référence à ce . Il y a un problème avec cela, il est très difficile de déboguer une telle méthode. Puisque vous pouvez très bien mettre la variable dans la fenêtre de surveillance ou l'inspecter. Et cela disparaître pendant que vous déboguez si un GC se produit. Ce serait très désagréable, c'est pourquoi la gigue est de 10 %. conscient qu'il y ait un débogueur attaché. Ensuite, il modifie le tableau et modifie l'adresse "dernière utilisation". Et la fait passer de sa valeur normale à l'adresse de la dernière instruction de la méthode. Ce qui maintient la variable en vie tant que la méthode n'est pas revenue. Ce qui vous permet de continuer à la surveiller jusqu'à ce que la méthode revienne.

Cela explique maintenant aussi ce que vous avez vu plus tôt et pourquoi vous avez posé la question. Il imprime "0" parce que l'appel GC.Collect ne peut pas collecter la référence. Le tableau indique que la variable est utilisée passé l'appel GC.Collect(), jusqu'à la fin de la méthode. Je suis forcé de le dire parce que le débogueur est attaché. y en exécutant le build Debug.

Mettre la variable à null a un effet maintenant parce que le GC va inspecter la variable et ne verra plus une référence. Mais assurez-vous de ne pas tomber dans le piège dans lequel de nombreux programmeurs C# sont tombés, en fait écrire ce code était inutile. Cela ne fait aucune différence que cette déclaration soit présente ou non lorsque vous exécutez le code dans la version Release. En fait, l'optimiseur de gigue supprimer cette déclaration car elle n'a aucun effet. Veillez donc à ne pas écrire de code de ce type, même s'il semblait pour avoir un effet.


Une dernière remarque à ce sujet, c'est ce qui met en difficulté les programmeurs qui écrivent de petits programmes pour faire quelque chose avec une application Office. Le débogueur les met généralement sur la mauvaise voie, ils veulent que le programme Office se termine à la demande. La façon appropriée de le faire est d'appeler GC.Collect(). Mais ils découvriront que cela ne fonctionne pas lorsqu'ils débogueront leur application, ce qui les mènera sur un terrain inconnu en appelant Marshal.ReleaseComObject(). La gestion manuelle de la mémoire, elle fonctionne rarement correctement car ils vont facilement négliger une référence d'interface invisible. GC.Collect() fonctionne réellement, mais pas lorsque vous déboguez l'application.

1 votes

Voir aussi ma question à laquelle Hans a bien répondu pour moi. stackoverflow.com/questions/15561025/

1 votes

@HansPassant Je viens de trouver cette explication géniale, qui répond aussi à une partie de ma question ici : stackoverflow.com/questions/30529379/ sur la GC et la synchronisation des threads. Une question que je me pose encore : Je me demande si la GC compacte et met à jour les adresses qui sont utilisées dans un registre (stocké en mémoire pendant la suspension), ou si elle les ignore simplement ? Un processus qui met à jour les registres après avoir suspendu le fil d'exécution (avant la reprise) me semble être un fil de sécurité sérieux qui est bloqué par le système d'exploitation.

1 votes

Indirectement, oui. Le thread est suspendu, le GC met à jour le backing store pour les registres du CPU. Une fois que le thread reprend son exécution, il utilise maintenant les valeurs de registre mises à jour.

44voto

R.C Points 9982

[ Je voulais juste ajouter quelque chose sur les aspects internes du processus de finalisation ].

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

CONCEPTS COURTS : :

  1. Objets ne mettant PAS en œuvre Finalize méthodes, leur mémoire est récupérée immédiatement, à moins, bien sûr, qu'elles ne soient pas accessibles par les utilisateurs.
    plus de code d'application

  2. Objets mettant en œuvre Finalize La méthode, le concept/la mise en œuvre 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 l'application. Code

Supposons : : Les classes/objets A, B, D, G, H N'implémentent PAS Finalize Méthode et mise en œuvre de C, E, F, I, J Finalize Méthode.

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

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

Le site file d'attente de finalisation est une structure de données interne contrôlée par le garbage collector. Chaque entrée de la file d'attente pointe vers un objet qui devrait avoir son numéro d'enregistrement. Finalize appelée avant que la mémoire de l'objet puisse être récupérée. La figure ci-dessous montre un tas contenant plusieurs objets. Certains de ces objets sont accessibles à partir de la méthode les racines de l'application et d'autres non. Lorsque les objets C, E, F, I et J ont été créés, l'infrastructure .Net détecte que ces objets ont des noms de domaine. Finalize et les pointeurs vers ces objets sont ajoutés à l'objet file d'attente de finalisation .

enter image description here

Lorsqu'une GC se produit (1ère collecte), 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 de la boîte jaune ci-dessus.

Le ramasseur d'ordures parcourt le file d'attente de finalisation à la recherche de pointeurs vers ces objets. Lorsqu'un pointeur est trouvé, il est retiré de la file de finalisation et ajouté à la file de libération. ("F-reachable").

Le site file d'attente détachable est une autre structure de données interne contrôlée par le garbage collector. Chaque pointeur de la structure file d'attente détachable identifie un objet qui est prêt à avoir son Finalize appelé.

Après la collecte (1ère collecte), 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 possédaient pas de méthode finalize qui devait être appelée .

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

3.) A, C, D, F sont toujours accessibles par le code d'application représenté par les flèches de la boîte jaune ci-dessus. flèches de la boîte jaune ci-dessus, ils ne seront donc PAS collectés dans tous les cas. cas

enter image description here

Il existe un fil d'exécution spécial dédié à l'appel des méthodes Finalize. Lorsque la file d'attente est vide (ce qui est généralement le cas), ce thread dort. Mais lorsque des entrées apparaissent, ce thread se réveille, supprime chaque entrée de la file et appelle la méthode Finalize de chaque objet. Le ramasseur d'ordures compacte la mémoire récupérable et le thread d'exécution spécial vide la file d'attente des objets récupérables. freachable la file d'attente, en exécutant les Finalize méthode. Voici enfin comment votre méthode Finalize est exécutée.

La prochaine fois que le ramasseur d'ordures est invoqué (2nd Collection), il constate que les objets finalisés sont vraiment des ordures, puisque les racines de l'application ne pointent pas vers eux et que l'objet file d'attente détachable ne pointe plus vers lui (il est VIDE aussi), Donc la mémoire pour les objets (E, I, J) sont simplement récupérés du tas. Voir la figure ci-dessous et la comparer avec la figure juste au-dessus

enter image description here

La chose importante à comprendre ici est que deux GCs sont nécessaires pour récupérer la mémoire utilisée par les 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: : Le site file d'attente détachable est considérée comme une racine, tout comme les variables globales et statiques sont des racines. Par conséquent, si un objet se trouve dans la file d'attente freachable, alors l'objet est atteignable et n'est pas un garbage.

Enfin, rappelez-vous que le débogage d'une application est une chose, le ramassage des déchets en est une autre et fonctionne différemment. Jusqu'à présent, vous ne pouvez pas SENTIR le ramassage des ordures en déboguant des applications, mais si vous souhaitez étudier le ramassage des ordures, vous pouvez obtenir les informations suivantes a commencé ici.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X