Texte original en anglais : Jeffrey Richter
Compilé par : Zhao Yukai
Lien : http://www.php.cn/
Avec le mécanisme de récupération de place dans Microsoft.Net clr, les programmeurs n'ont plus besoin de faire attention au moment où libérer de la mémoire. La question de la libération de mémoire est entièrement réglée par le GC et est transparente pour les programmeurs. Néanmoins, en tant que programmeur .Net, il est nécessaire de comprendre comment fonctionne le garbage collection. Dans cet article, nous examinerons comment .Net alloue et gère la mémoire gérée, puis décrirons étape par étape le mécanisme algorithmique du garbage collector.
Concevoir une stratégie de gestion de la mémoire appropriée pour un programme est difficile et fastidieux, et cela vous empêche également de vous concentrer sur la résolution du problème que le programme lui-même essaie de résoudre. Existe-t-il une méthode intégrée qui peut aider les développeurs à résoudre les problèmes de gestion de la mémoire ? Bien sûr, il s'agit de GC dans .Net, garbage collection.
Réfléchissons-y, chaque programme utilise des ressources mémoire : telles que l'affichage de l'écran, la connexion réseau, les ressources de la base de données, etc. En fait, dans un environnement orienté objet, chaque type doit occuper certaines ressources mémoire pour stocker ses données. Les objets doivent utiliser de la mémoire selon les étapes suivantes :
1. Espace
2. Initialisez la mémoire et réglez la mémoire dans un état utilisable
3. Accédez aux membres de l'objet
4. .Détruisez l'objet et libérez la mémoire
5. Libérez la mémoire
Ce modèle d'utilisation de la mémoire apparemment simple a causé de nombreux problèmes de programme. Parfois, les programmeurs peuvent oublier de libérer des objets. qui ne sont plus utilisés, et tentent parfois d'accéder à des objets déjà publiés. Ces deux types de bugs sont généralement cachés dans une certaine mesure et ne sont pas faciles à trouver. Contrairement aux erreurs logiques, ils peuvent être modifiés une fois découverts. Ils peuvent perdre de la mémoire et provoquer des plantages inattendus après un certain temps d'exécution du programme. En fait, il existe de nombreux outils qui peuvent aider les développeurs à détecter les problèmes de mémoire, tels que : Gestionnaire des tâches, Système
Surveillez AcitvieX Control et Rational's Purify.
Le GC n'exige pas que les développeurs fassent attention au moment où libérer de la mémoire. Cependant, le garbage collector ne peut pas gérer toutes les ressources en mémoire. Le garbage collector ne sait pas comment recycler certaines ressources. Pour ces ressources, les développeurs doivent écrire leur propre code pour les recycler. Dans .Net
Dans les frameworks, les développeurs écrivent généralement le code pour nettoyer ces ressources dans les méthodes Close, Dispose ou Finalize. Nous examinerons la méthode Finalize plus tard. Cette méthode sera automatiquement appelée par le garbage collector.
Cependant, il existe de nombreux objets qui n'ont pas besoin d'implémenter le code pour libérer les ressources par eux-mêmes, tels que : Rectangle Pour l'effacer, il vous suffit d'effacer ses champs gauche, droite, largeur et hauteur. le collectionneur peut le faire. Jetons un coup d'œil à la manière dont la mémoire est allouée aux objets.
Allocation d'objet :
.Net clr alloue tous les objets de référence au tas géré. Ceci est très similaire au tas du runtime c, mais vous n'avez pas besoin de faire attention au moment où libérer l'objet, l'objet sera libéré automatiquement lorsqu'il n'est pas utilisé. De cette manière, une question se pose : comment le garbage collector sait-il qu’un objet n’est plus utilisé et doit être recyclé ? Nous expliquerons cela plus tard.
Il existe plusieurs algorithmes de garbage collection. Chaque algorithme a une optimisation des performances pour un environnement spécifique. Dans cet article, nous nous concentrons sur l'algorithme de garbage collection de clr. Commençons par un concept de base.
Lorsqu'un processus est initialisé, un espace mémoire vide continu sera réservé pendant l'exécution. Cet espace mémoire est le tas géré. Le tas géré enregistrera un pointeur, nous l'appelons NextObjPtr, qui pointe vers l'adresse d'allocation de l'objet suivant. Initialement, ce pointeur pointe vers l'emplacement de départ du tas géré.
L'application utilise l'opérateur new pour créer un nouvel objet. Cet opérateur doit d'abord confirmer que l'espace restant du tas géré peut accueillir l'objet. S'il peut l'accueillir, pointez le pointeur NextObjPtr vers. l'objet. , puis appelez le constructeur de l'objet, et l'opérateur new renvoie l'adresse de l'objet.
Figure 1 Tas géré
À ce stade, NextObjPtr pointe vers l'emplacement où l'objet suivant est alloué sur le tas géré. La figure 1 montre qu'il y a trois objets A, B et C dans un tas géré. L'objet suivant sera placé à l'emplacement pointé par NextObjPtr (à côté de l'objet C)
Examinons maintenant à nouveau comment le tas du runtime c alloue la mémoire. Dans le tas d'exécution C, l'allocation de mémoire nécessite de parcourir la structure de données d'une liste chaînée jusqu'à ce qu'un bloc mémoire suffisamment grand soit trouvé. Ce bloc mémoire peut être divisé. Après la division, le pointeur dans la liste chaînée doit pointer vers l'espace mémoire restant. . Assurez-vous que la liste chaînée est intacte. Pour le tas géré, l'allocation d'un objet modifie uniquement le pointeur du pointeur NextObjPtr, ce qui est très rapide. En fait, l’allocation d’un objet sur le tas géré est très proche de l’allocation de mémoire sur la pile de threads.
Jusqu'à présent, la vitesse d'allocation de mémoire sur le tas géré semble être plus rapide que celle sur le tas du runtime c, et la mise en œuvre est également plus simple. Bien entendu, le tas géré bénéficie de cet avantage car il repose sur une hypothèse : l’espace d’adressage est illimité. Il est évident que cette hypothèse est fausse. Il doit y avoir un mécanisme pour garantir que cette hypothèse est vraie. Ce mécanisme est le garbage collector. Voyons comment cela fonctionne.
Lorsque l'application appelle le nouvel opérateur pour créer un objet, il se peut qu'il n'y ait pas de mémoire pour stocker l'objet. Le tas géré peut détecter si l'espace pointé par NextObjPtr dépasse la taille du tas, cela signifie que le tas géré est plein et qu'un garbage collection est requis.
En réalité, un garbage collection sera déclenché une fois que le tas de génération 0 sera plein. La « génération » est un mécanisme d'implémentation permettant au garbage collector d'améliorer les performances. « Génération » signifie : les objets nouvellement créés sont la jeune génération, et les objets qui ne sont pas recyclés avant l'opération de recyclage sont des objets plus anciens. La division des objets en générations permet au garbage collector de collecter uniquement les objets d'une certaine génération au lieu de collecter tous les objets.
Algorithme de récupération de place :
Le garbage collector vérifie s'il y a des objets qui ne sont plus utilisés par l'application. Si de tels objets existent, l'espace occupé par ces objets peut être récupéré (s'il n'y a pas assez de mémoire disponible sur le tas, le nouvel opérateur lancera une exception OutofMemoryException). Vous vous demandez peut-être comment le garbage collector détermine si un objet est toujours utilisé ? Il n’est pas facile de répondre à cette question.
Chaque application possède un ensemble d'objets racine. Les racines sont des emplacements de stockage. Elles peuvent pointer vers une adresse sur le tas géré, ou elles peuvent être nulles. Par exemple, tous les pointeurs d'objet globaux et statiques sont des objets racine de l'application. De plus, les variables/paramètres locaux sur la pile de threads sont également des objets racine de l'application, et les objets dans les registres CPU pointant vers le tas géré sont également des objets racine. . La liste des objets racine survivants est maintenue par le compilateur JIT (juste à temps) et clr, et le garbage collector peut accéder à ces objets racine.
Lorsque le garbage collector démarre, il suppose que tous les objets du tas géré sont des déchets. Autrement dit, on suppose qu’il n’existe aucun objet racine ni aucun objet référencé par l’objet racine. Le garbage collector commence ensuite à parcourir l'objet racine et crée un graphique de tous les objets qui ont une référence à l'objet racine.
La figure 2 montre que les objets racine de l'application sur le tas géré sont A, C, D et F. Ces objets font partie du graphe. Ensuite, l'objet D fait référence à l'objet H, donc l'objet H est également ajouté au graphe. graph. ;Le garbage collector parcourra tous les objets accessibles.
Photo 2
Objets sur le tas géré
Le garbage collector parcourra l'objet racine et référencera les objets un par un. Si le garbage collector constate qu'un objet est déjà dans le graphique, il modifiera le chemin et continuera à le parcourir. Cela a deux objectifs : l’un est d’améliorer les performances et l’autre est d’éviter les boucles infinies.
Une fois tous les objets racine vérifiés, le graphique du garbage collector contiendra tous les objets accessibles dans l'application. Tous les objets du tas géré qui ne figurent pas sur ce graphique sont des objets inutiles à recycler. Après avoir construit le graphe d'objets accessibles, le garbage collector commence à parcourir linéairement le tas géré pour trouver des blocs d'objets garbage consécutifs (qui peuvent être considérés comme de la mémoire libre). Le garbage collector déplace ensuite les objets non-garbage ensemble (à l'aide de la fonction memcpy en C), couvrant tous les fragments de mémoire. Bien sûr, désactivez tous les pointeurs d'objet lorsque vous déplacez des objets (car ils peuvent être erronés). Le garbage collector doit donc modifier les objets racine de l'application afin qu'ils pointent vers la nouvelle adresse mémoire de l'objet. De plus, si un objet contient un pointeur vers un autre objet, le garbage collector se charge également de modifier la référence. La figure 3 montre le tas géré après une collection.
Image 3
Le tas géré après recyclage
est illustré à la figure 3. Après le recyclage, tous les objets poubelles sont identifiés et tous les objets non poubelles sont déplacés ensemble. Les pointeurs de tous les objets non-garbage sont également modifiés vers les adresses mémoire déplacées, et NextObjPtr pointe vers l'arrière du dernier objet non-garbage. À ce stade, le nouvel opérateur peut continuer à créer des objets avec succès.
Comme vous pouvez le constater, la récupération de place entraîne une pénalité de performances significative, ce qui constitue un inconvénient évident de l'utilisation du tas géré. Toutefois, n'oubliez pas que l'opération de récupération de mémoire n'est effectuée que lorsque le tas géré est lent. Les performances du tas géré sont meilleures que celles du tas d’exécution C jusqu’à ce qu’il soit plein. Le garbage collector d'exécution effectue également certaines optimisations de performances, dont nous parlerons dans le prochain article.
Le code suivant illustre la façon dont les objets sont créés et gérés :
class Application { public static int Main(String[] args) { // ArrayList object created in heap, myArray is now a root ArrayList myArray = new ArrayList(); // Create 10000 objects in the heap for (int x = 0; x < 10000; x++) { myArray.Add(new Object()); // Object object created in heap } // Right now, myArray is a root (on the thread's stack). So, // myArray is reachable and the 10000 objects it points to are also // reachable. Console.WriteLine(a.Length); // After the last reference to myArray in the code, myArray is not // a root. // Note that the method doesn't have to return, the JIT compiler // knows // to make myArray not a root after the last reference to it in the // code. // Since myArray is not a root, all 10001 objects are not reachable // and are considered garbage. However, the objects are not // collected until a GC is performed. } }
Peut-être vous demanderez-vous, GC est si bon, pourquoi n'est-il pas inclus dans ANSI C ? La raison en est que le ramasse-miettes doit être capable de trouver la liste d'objets racine de l'application et doit trouver le pointeur de l'objet. En C, les pointeurs d’objet peuvent être convertis les uns aux autres et il n’existe aucun moyen de savoir sur quel objet pointe le pointeur. Dans le CLR, le tas géré connaît le type réel de l'objet. Les informations de métadonnées peuvent être utilisées pour déterminer à quels objets membres l'objet fait référence.
Garbage Collection et finalisation
Le garbage collector fournit une fonctionnalité supplémentaire, qui peut appeler automatiquement la méthode Finalize d'un objet après qu'il ait été marqué comme garbage (à condition que le object Remplaçant la méthode Finalize de l'objet).
La méthode Finalize est une méthode virtuelle de l'objet objet. Vous pouvez remplacer cette méthode si nécessaire, mais cette méthode ne peut être remplacée que d'une manière similaire au destructeur c. Par exemple :
{ ~Foo(){ Console.WriteLine(“Foo Finalize”); } }
Les programmeurs qui ont utilisé C ici doivent accorder une attention particulière au fait que la méthode Finalize est écrite exactement de la même manière que le destructeur de C. Cependant, la méthode Finalize et le destructeur de . Net are Différemment, les objets gérés ne peuvent pas être détruits et ne peuvent être recyclés que via le garbage collection.
Lorsque vous concevez une classe, il est préférable d'éviter de surcharger la méthode Finalize pour les raisons suivantes :
1. Les objets qui implémentent Finalize seront promus vers une "génération" plus ancienne, ce qui augmentera la pression mémoire et rendra la classe plus ancienne. object et Les objets associés à cet objet ne peuvent pas être recyclés la première fois qu'ils deviennent des déchets.
2. L'allocation de ces objets prendra plus de temps
3. Laisser le garbage collector exécuter la méthode Finalize réduira considérablement les performances. N'oubliez pas que chaque objet qui implémente la méthode Finalize doit exécuter la méthode Finalize. S'il existe un objet tableau d'une longueur de 10 000, chaque objet doit exécuter la méthode Finalize
4. Les objets qui remplacent la méthode Finalize peuvent faire référence. Les autres objets qui n'implémentent pas la méthode Finalize seront également retardés pour le recyclage
5. Vous n'avez aucun moyen de contrôler le moment où la méthode Finalize est exécutée. Si vous souhaitez libérer des ressources telles que des connexions à la base de données dans la méthode Finalize, cela peut entraîner la libération des ressources de la base de données longtemps après le temps
6. Lorsque le programme plante, certains objets sont toujours référencés et leurs méthodes Finalize ne le sont pas. Opportunité exécutée. Cette situation se produira lorsque l'objet est utilisé dans un thread d'arrière-plan, ou lorsque l'objet quitte le programme, ou lorsque l'AppDomain est déchargé. De plus, par défaut, la méthode Finalize ne sera pas exécutée lorsque l'application sera forcée à se terminer. Bien sûr, toutes les ressources du système d'exploitation seront récupérées ; mais les objets sur le tas géré ne seront pas récupérés. Vous pouvez modifier ce comportement en appelant la méthode RequestFinalizeOnShutdown du GC.
7. Le runtime ne peut pas contrôler l'ordre dans lequel les méthodes Finalize de plusieurs objets sont exécutées. Parfois la destruction des objets peut être séquentielle
Si l'objet que vous définissez doit implémenter la méthode Finalize, alors assurez-vous que la méthode Finalize est exécutée le plus rapidement possible et évitez toutes les opérations pouvant provoquer un blocage, y compris les éventuelles opérations de synchronisation des threads. De plus, assurez-vous que la méthode Finalize ne provoque aucune exception. S'il y a une exception, le garbage collector continuera à exécuter la méthode Finalize des autres objets et ignorera directement l'exception.
Lorsque le compilateur génère du code, il appellera automatiquement le constructeur de la classe de base sur le constructeur. De même, le compilateur C ajoutera également automatiquement un appel au destructeur de classe de base pour le destructeur. Cependant, la fonction Finalize dans .Net n'est pas comme ça et le compilateur n'effectuera pas de traitement spécial pour la méthode Finalize. Si vous souhaitez appeler la méthode Finalize de la classe parent dans la méthode Finalize, vous devez ajouter explicitement vous-même le code appelant.
Veuillez noter que la méthode Finalize en C# est écrite de la même manière que le destructeur en c, mais C# ne prend pas en charge les destructeurs. Ne vous laissez pas tromper par cette façon d'écrire.
L'implémentation interne du GC appelant la méthode Finalize
En apparence, il est très simple pour le garbage collector d'utiliser la méthode Finalize. Vous créez un objet et l'appelez. Méthode Finalize lorsque l’objet est recyclé. Mais c'est en réalité un peu plus compliqué.
Lorsqu'une application crée un nouvel objet, le nouvel opérateur alloue de la mémoire sur le tas. Si l'objet implémente la méthode Finalize. Le pointeur d'objet sera placé dans la file d'attente de finalisation. La file d'attente de finalisation est une structure de données interne contrôlée par le garbage collector. Chaque objet de la file d'attente doit appeler sa méthode Finalize lors du recyclage.
Le tas illustré dans la figure ci-dessous contient plusieurs objets, dont certains sont des objets et d'autres ne le sont pas. Lorsque les objets C, E, F, I et J sont créés, le système détecte que ces objets implémentent la méthode Finalize et place leurs pointeurs dans la file d'attente de finalisation.
Ce que fait la méthode Finalize, c'est généralement un recyclage qui ne peut pas être recyclé par le garbage collector. ressources, telles que les descripteurs de fichiers, les connexions à la base de données, etc.
Lorsque la collecte des déchets a lieu, les objets B, E, G, H, I et J sont marqués comme déchets. Le garbage collector analyse la file d'attente de finalisation pour trouver des pointeurs vers ces objets. Lorsqu'un pointeur d'objet est trouvé, le pointeur est déplacé vers la file d'attente Freachable. La file d'attente Freachable est une autre structure de données interne contrôlée par le garbage collector. La méthode Finalize de chaque objet de la file d'attente Freachable sera exécutée.
Après le garbage collection, le tas géré est tel qu'illustré dans la figure 6. Vous pouvez voir que les objets B, G et H ont été recyclés car ces objets n'ont pas de méthode Finalize. Cependant, les objets E, I et J n'ont pas encore été recyclés car leurs méthodes Finalize n'ont pas encore été exécutées.
Image 5
Tas géré après la collecte des déchets
Lorsque le programme est en cours d'exécution, il y aura un thread dédié chargé d'appeler la méthode Finalize de l'objet dans le File d'attente accessible. Lorsque la file d'attente Freachable est vide, ce thread se met en veille Lorsqu'il y a des objets dans la file d'attente, le thread est réveillé, supprime les objets de la file d'attente et appelle sa méthode Finalize. Par conséquent, n’essayez pas d’accéder au local du thread lors de l’exécution de la méthode Finalize.
stockage.
L'interaction entre la file de finalisation et la file d'attente Freachable est très intelligente. Laissez-moi d’abord vous dire d’où vient son nom accessible. F est évidemment une finalisation ; chaque objet de cette file d'attente attend d'exécuter sa méthode Finalize accessible ; cela signifie que ces objets arrivent. En d'autres termes, les objets d'une file d'attente Freachable sont considérés comme des objets liés, au même titre que les variables globales ou les variables statiques. Par conséquent, si un objet se trouve dans la file d’attente accessible, alors cet objet n’est pas un déchet.
Pour faire simple, lorsqu'un objet est inaccessible, le ramasse-miettes considérera l'objet comme un déchet. Ensuite, lorsque le garbage collector déplace les objets de la file d'attente de finalisation vers la file d'attente Freachable, ces objets ne sont plus des déchets et leur mémoire ne sera pas récupérée. À ce stade, le ramasse-miettes a terminé de marquer les déchets et certains objets marqués comme déchets ont été reconsidérés comme des objets non poubelles. Le garbage collector récupère la mémoire compressée, efface la file d'attente accessible et exécute la méthode Finalize de chaque objet de la file d'attente.
Figure 6 Tas géré après avoir effectué à nouveau le garbage collection
再次出发垃圾回收之后,实现Finalize方法的对象才被真正的回收。这些对象的Finalize方法已经执行过了,Freachable队列清空了。
垃圾回收让对象复活
在前面部分我们已经说了,当程序不使用某个对象时,这个对象会被回收。然而,如果对象实现了Finalize方法,只有当对象的Finalize方法执行之后才会认为这个对象是可回收对象并真正回收其内存。换句话说,这类对象会先被标识为垃圾,然后放到freachable队列中复活,然后执行Finalize之后才被回收。正是Finalize方法的调用,让这种对象有机会复活,我们可以在Finalize方法中让某个对象强引用这个对象;那么垃圾回收器就认为这个对象不再是垃圾了,对象就复活了。
如下复活演示代码:
public class Foo { ~Foo(){ Application.ObjHolder = this; } } class Application{ static public Object ObjHolder = null; }
在这种情况下,当对象的Finalize方法执行之后,对象被Application的静态字段ObjHolder强引用,成为根对象。这个对象就复活了,而这个对象引用的对象也就复活了,但是这些对象的Finalize方法可能已经执行过了,可能会有意想不到的错误发生。
事实上,当你设计自己的类型时,对象的终结和复活有可能完全不可控制。这不是一个好现象;处理这种情况的常用做法是在类中定义一个bool变量来表示对象是否执行过了Finalize方法,如果执行过Finalize方法,再执行其他方法时就抛出异常。
现在,如果有其他的代码片段又将Application.ObjHolder设置为null,这个对象变成不可达对象。最终垃圾回收器会把对象当成垃圾并回收对象内存。请注意这一次对象不会出现在finalization队列中,它的Finalize方法也不会再执行了。
复活只有有限的几种用处,你应该尽可能避免使用复活。尽管如此,当使用复活时,最好重新将对象添加到终结队列中,GC提供了静态方法ReRegisterForFinalize方法做这件事:
如下代码:
public class Foo{ ~Foo(){ Application.ObjHolder = this; GC.ReRegisterForFinalize(this); } }
当对象复活时,重新将对象添加到复活队列中。需要注意的时如果一个对象已经在终结队列中,然后又调用了GC.ReRegisterForFinalize(obj)方法会导致此对象的Finalize方法重复执行。
垃圾回收机制的目的是为开发人员简化内存管理。
下一篇我们谈一下弱引用的作用,垃圾回收中的“代”,多线程中的垃圾回收和与垃圾回收相关的性能计数器。
以上就是.Net 垃圾回收机制原理(一)的内容,更多相关内容请关注PHP中文网(www.php.cn)!