Vous ne pouvez vraiment pas faire de déclarations générales sur la manière appropriée d'utiliser toutes les implémentations du GC. Ils varient énormément. Je parlerai donc de celui .NET auquel vous avez fait référence à l’origine.
Vous devez connaître le comportement du GC assez intimement pour le faire avec n'importe quelle logique ou raison.
Le seul conseil que je puisse donner sur la collecte est le suivant: ne le faites jamais.
Si vous connaissez vraiment les détails complexes du GC, vous n'aurez pas besoin de mes conseils, cela n'aura donc aucune importance. Si vous ne savez pas déjà avec 100% de confiance, cela vous aidera, et vous devrez chercher en ligne et trouver une réponse comme celle-ci: vous ne devriez pas appeler GC.Collect , ou bien vous devriez aller voir les détails du fonctionnement du GC. à l'intérieur et à l'extérieur, et alors seulement, vous saurez la réponse .
Il y a un endroit sûr où il est logique d'utiliser GC.Collect :
GC.Collect est une API disponible que vous pouvez utiliser pour définir le minutage des événements. Vous pouvez profiler un algorithme, collecter et profiler un autre algorithme immédiatement après, sachant que la GC du premier algorithme ne se produisait pas pendant le second altérant les résultats.
Ce type de profilage est la seule fois que je suggère de collecter manuellement à qui que ce soit.
Exemple contrarié quand même
Un cas d'utilisation possible est que, si vous chargez des objets très volumineux, ils se retrouveront dans le tas d'objets volumineux qui ira directement à la génération 2, bien que la génération 2 soit destinée aux objets à longue durée de vie car elle collecte moins fréquemment. Si vous savez que vous chargez des objets éphémères dans la génération 2 pour une raison quelconque, vous pouvez les nettoyer plus rapidement pour que votre génération 2 reste plus petite et que ses collections soient plus rapides.
C’est le meilleur exemple que je puisse donner, et ce n’est pas bon - la pression de la loi sur la santé que vous établissez ici provoquerait des collectes plus fréquentes, et les collections sont si fréquentes qu’il en est - il est probable que cela effacerait la mémoire au même moment. vite que vous le souffliez avec des objets temporaires. Simplement , je ne fais pas confiance moi - même de présumer une meilleure fréquence de collecte que le GC lui - même - à l' écoute par des gens bien loin plus intelligents que moi
Parlons donc de la sémantique et des mécanismes du GC .NET ... ou ..
Tout ce que je pense savoir sur le .NET GC
S'il vous plaît, toute personne qui trouve des erreurs ici - corrigez-moi. La plupart des membres de GC sont connus pour être de la magie noire et bien que j'essaie de laisser de côté des détails dont je n'étais pas sûr, je me trompe probablement encore.
Vous trouverez ci-dessous de nombreux détails dont je ne suis pas certain, ainsi que des informations beaucoup plus vastes que je ne connais pas. Utilisez ces informations à vos risques et périls.
Concepts GC
Le .NET GC se produit à des moments incohérents. C'est pourquoi on l'appelle "non déterministe". Cela signifie que vous ne pouvez pas vous fier à cela à des moments spécifiques. Il s'agit également d'un ramasse-miettes générationnel, ce qui signifie qu'il partitionne vos objets en fonction du nombre de passes GC qu'ils ont effectuées.
Les objets de la génération 0 ont vécu 0 collections. Celles-ci ont été récemment créées. Aucune collection ne s’est produite depuis leur instanciation. Les objets de votre segment de génération 1 ont traversé une passe de collecte, tout comme les objets de votre segment de génération 2 ont traversé 2 passes de collection.
Maintenant, il convient de noter la raison pour laquelle il qualifie ces générations et partitions en conséquence. Le .NET GC ne reconnaît que ces trois générations, car les passes de collection qui vont sur ces trois tas sont légèrement différentes. Certains objets peuvent survivre des passages de collection des milliers de fois. Le GC ne fait que les laisser de l’autre côté de la partition de pile de la génération 2, il est inutile de les partitionner ailleurs car il s’agit en fait de la génération 44; la collection transmise est la même chose que tout le tas de la génération 2.
Il existe des objectifs sémantiques pour ces générations spécifiques, ainsi que des mécanismes mis en œuvre qui les respectent, et j'y reviendrai dans un instant.
Que contient une collection
Le concept de base d'une passe de collection GC consiste à vérifier chaque objet dans un espace de tas pour voir s'il existe encore des références actives (racines GC) à ces objets. Si une racine GC est trouvée pour un objet, cela signifie que le code en cours d'exécution peut toujours atteindre et utiliser cet objet, de sorte qu'il ne peut pas être supprimé. Toutefois, si aucune racine GC n'est trouvée pour un objet, cela signifie que le processus en cours n'a plus besoin de cet objet. Il peut donc le supprimer pour libérer de la mémoire pour de nouveaux objets.
Maintenant, après avoir fini de nettoyer un tas d'objets et d'en laisser quelques-uns, il y aura un effet secondaire malheureux: des espaces libres entre les objets vivants, là où les objets morts ont été enlevés. Cette fragmentation de la mémoire, si elle est laissée à elle-même, gaspille simplement de la mémoire. Par conséquent, les collections effectuent ce que l'on appelle "compactage": elles prennent tous les objets en direct et les compressent dans le segment de mémoire de sorte que la mémoire disponible soit contiguë d'un côté du segment de mémoire pour Gen. 0.
Maintenant, étant donné l’idée de 3 tas de mémoire, tous partitionnés par le nombre de passes de collecte qu’ils ont effectuées, voyons pourquoi ces partitions existent.
Collection Gen 0
La génération 0 étant les objets les plus récents, elle a tendance à être très petite - vous pouvez donc la collecter en toute sécurité très fréquemment . La fréquence garantit que le segment de mémoire reste petit et les collections sont très rapides car elles sont collectées sur un aussi petit segment. Ceci est basé plus ou moins sur une heuristique affirmant que: Une grande majorité des objets temporaires que vous créez sont très temporaires, ils ne sont donc plus utilisés ou référencés presque immédiatement après leur utilisation, et peuvent donc être collectés.
Collection Gen 1
La génération 1 étant des objets qui ne font pas partie de cette catégorie très temporaire d'objets, leur durée de vie peut encore être assez courte, car une grande partie des objets créés ne sont pas utilisés longtemps. Par conséquent, la génération 1 collecte également assez fréquemment, encore une fois, la taille de son paquet est réduite afin que ses collections soient rapides. Cependant, l’hypothèse est que moins d’objets sont temporaires que la génération 0, donc elle collecte moins souvent que la génération 0
Je dirai franchement que je ne connais pas les mécanismes techniques qui diffèrent entre le laissez-passer de collection du groupe 0 et celui du groupe 1, s’il en existe un autre que la fréquence qu’ils collectent.
Collection Gen 2
Gen 2 doit maintenant être la mère de tous les tas, non? Eh bien oui, c'est plus ou moins vrai. C’est là où vivent tous vos objets permanents - l’objet Main()
dans lequel vous vivez, par exemple, et tout ce qui est Main()
référencé car ils seront enracinés jusqu’à votre Main()
retour à la fin de votre processus.
Étant donné que la génération 2 est un seau pour pratiquement tout ce que les autres générations ne pourraient pas collecter, ses objets sont en grande partie permanents, ou vivent au moins longtemps. Donc, sachant que très peu de ce qui est dans la génération 2 sera en réalité une chose qui peut être collectée, il n’a pas besoin de la collecter fréquemment. Cela permet également à sa collection d'être plus lente, car elle s'exécute beaucoup moins fréquemment. C'est donc essentiellement là où ils ont ajouté tous les comportements supplémentaires pour les scénarios impairs, car ils ont le temps de les exécuter.
Tas d'objets volumineux
Un exemple des comportements supplémentaires de la génération 2 est qu'il effectue également la collecte sur le segment d'objets volumineux. Jusqu'à présent, je parlais entièrement du segment de petits objets, mais le runtime .NET attribue des éléments de certaines tailles à un segment séparé en raison de ce que j'ai appelé le compactage ci-dessus. Le compactage nécessite de déplacer des objets lorsque les collections se terminent sur le segment de petits objets. S'il y a un objet vivant de 10 Mo dans la génération 1, il faudra beaucoup plus de temps pour terminer le compactage après la collecte, ce qui ralentira la collection de la génération 1. Ainsi, cet objet de 10 Mo est affecté au segment d'objets volumineux et collecté au cours de la génération 2, qui s'exécute si rarement.
La finalisation
Un autre exemple est celui des objets avec finaliseurs. Vous placez un finaliseur sur un objet qui fait référence à des ressources dépassant la portée de .NETs GC (ressources non managées). Le finaliseur est le seul moyen dont dispose le GC pour demander la collecte d'une ressource non gérée - vous l'implémentez pour effectuer la collecte / suppression / libération manuelle de la ressource non gérée afin de s'assurer qu'elle ne fuit pas de votre processus. Lorsque le GC commence à exécuter le finaliseur de vos objets, votre implémentation efface la ressource non gérée, ce qui le rend capable de supprimer votre objet sans risquer une fuite de ressource.
Le mécanisme utilisé par les finaliseurs est leur référence directe dans une file d'attente de finalisation. Lorsque le moteur d'exécution attribue un objet à un finaliseur, il ajoute un pointeur à cet objet dans la file d'attente de finalisation et verrouille votre objet à la place (appelé «épinglage») afin que le compactage ne le déplace pas, ce qui romprait la référence à la file d'attente de finalisation. Au fur et à mesure que les collectes se produisent, votre objet n'aura finalement plus de racine GC, mais la finalisation devra être exécutée avant de pouvoir être collectée. Ainsi, lorsque l'objet est mort, la collection déplacera sa référence de la file d'attente de finalisation et y placera une référence sur ce que l'on appelle la file d'attente "FReachable". Ensuite, la collection continue. À un autre moment "non déterministe" de l'avenir, un thread séparé, appelé thread de finaliseur, passera par la file d'attente FReachable, exécutant les finaliseurs pour chacun des objets référencés. Une fois terminé, la file d'attente FReachable est vide et il a basculé un bit sur l'en-tête de chaque objet indiquant qu'il n'est pas nécessaire de le finaliser (ce bit peut également être retourné manuellement avecGC.SuppressFinalize
ce qui est courant dans les Dispose()
méthodes), je soupçonne également que cela a désépinglé les objets, mais ne me citez pas là-dessus. La prochaine collection qui arrive sur le tas dans lequel se trouve cet objet le récupérera finalement. Les collections de la génération 0 ne font même pas attention aux objets avec ce bit de finalisation nécessaire, elles sont automatiquement promues, sans même vérifier leur racine. Un objet non raciné nécessitant une finalisation dans la génération 1 sera jeté dans la FReachable
file d'attente, mais la collection ne fait rien d'autre avec elle. Elle vit donc dans la génération 2. Ainsi, tous les objets dotés d'un finaliseur ne GC.SuppressFinalize
seront collectés dans Gen 2.