Dans cet article, vous trouverez deux sources d'informations Ă la fois:
- Cours complet de ramassage des ordures en russe: CLRium # 6 ( atelier actuel ici )
- Traduction d'un article de BOTR "Garbage Collector Device" par Maoni Stevens.

1. CLRium # 5: Cours complet de ramassage des ordures

2. Dispositif de ramassage des ordures par Maoni Stephens ( @ maoni0 )
Remarque: pour en savoir plus sur la collecte des ordures en général, consultez le manuel de collecte des ordures ; des informations spécialisées sur le garbage collector dans le CLR sont fournies dans le livre Pro .NET Memory Management . Des liens vers les deux ressources sont fournis à la fin du document.
Architecture des composants
La collecte des ordures est associée à deux composants: un distributeur et un collecteur. L'allocateur est responsable d'allouer de la mémoire et d'appeler le collecteur si nécessaire. Le collecteur collecte les déchets ou la mémoire des objets qui ne sont plus utilisés par le programme.
Il existe d'autres façons d'appeler le collecteur, par exemple manuellement, à l'aide de GC.Collect. En outre, le thread de finalisation peut recevoir une notification asynchrone indiquant que la mémoire est épuisée (ce qui entraînera le collecteur).
Dispositif distributeur
Le distributeur est appelé par les composants auxiliaires du runtime avec les informations suivantes:
- la taille nécessaire de la parcelle attribuée;
- contexte d'allocation de mémoire pour le thread d'exécution;
- des drapeaux qui indiquent, par exemple, si l'objet est finalisable.
Le garbage collector ne fournit pas de méthodes de traitement spéciales pour différents types d'objets. Il reçoit des informations sur la taille de l'objet à partir de l'exécution.
Selon la taille, le collecteur divise les objets en deux catégories: petit (<85 000 octets) et grand (> = 85 000 octets). En général, l'assemblage de petits et grands objets peut se produire de la même manière. Cependant, le collecteur les sépare par taille, car la compression de gros objets nécessite beaucoup de ressources.
Le garbage collector alloue de la mémoire à l'allocateur en fonction des contextes d'allocation. La taille du contexte d'allocation est déterminée par les blocs de mémoire alloués.
Les contextes de sélection sont de petites zones d'un segment de segment de mémoire spécifique, chacune étant destinée à un flux d'exécution spécifique. Sur une machine avec un processeur (c'est-à -dire 1 processeur logique), un seul contexte d'allocation de mémoire est utilisé pour les objets de génération 0.
Bloc de mémoire allouée - la quantité de mémoire allouée par l'allocateur chaque fois qu'il a besoin de plus de mémoire pour positionner un objet à l'intérieur de la zone. La taille de bloc est généralement de 8 Ko et la taille moyenne des objets gérés est de 35 octets. Par conséquent, dans un bloc, vous pouvez placer de nombreux objets.
Les grands objets n'utilisent pas de contextes et de blocs. Un grand objet peut être plus grand que ces petits morceaux de mémoire. De plus, les avantages de l'utilisation de ces zones (décrites ci-dessous) ne sont visibles que lorsque vous travaillez avec de petits objets. L'espace pour les grands objets est alloué directement dans le segment de segment de mémoire.
Le distributeur est conçu pour que:
appeler le garbage collector si nécessaire: l' allocateur appelle le collecteur lorsque la quantité de mémoire allouée aux objets dépasse la valeur de seuil (définie par le collecteur), ou si l'allocateur ne peut plus allouer de mémoire dans ce segment. Les seuils et les segments contrôlés seront décrits en détail plus loin.
enregistrer l'emplacement des objets: les objets situés ensemble dans un segment du tas sont stockés à des adresses virtuelles proches les unes des autres.
utiliser efficacement le cache: l' allocateur alloue de la mémoire en blocs , et non pour chaque objet. Il met à zéro autant de mémoire pour préparer le cache du processeur, car certains objets y seront placés directement. Le bloc de mémoire alloué est généralement de 8 Ko.
limiter efficacement la zone allouée au thread d'exécution: la proximité des contextes et des blocs de mémoire alloués au thread garantit qu'un seul thread écrira des données dans l'espace alloué. Par conséquent, il n'est pas nécessaire de limiter l'allocation de mémoire tant que l'espace dans le contexte d'allocation actuel n'est pas terminé.
assurer l'intégrité de la mémoire: le garbage collector remet toujours à zéro la mémoire pour les objets nouvellement alloués afin que leurs liens ne pointent pas vers des sections de mémoire arbitraires.
assurer la continuité du tas: l' allocateur crée un objet libre à partir de la mémoire restante dans chaque bloc alloué. Par exemple, si 30 octets restent dans le bloc et 40 octets sont nécessaires pour héberger l'objet suivant, l'allocateur transformera ces 30 octets en un objet libre et demandera un nouveau bloc de mémoire.
API
Object* GCHeap::Alloc(size_t size, DWORD); Object* GCHeap::Alloc(alloc_context* acontext, size_t size, DWORD);
À l'aide de ces fonctions, vous pouvez allouer de la mémoire pour des objets petits et grands. Il existe une fonction pour allouer de l'espace directement sur le tas de gros objets (LOH):
Object* GCHeap::AllocLHeap(size_t size, DWORD);
Dispositif collecteur
Tâches de garbage collector
GC est conçu pour une gestion efficace de la mémoire. Les développeurs qui écrivent du code managé peuvent l'utiliser sans trop d'effort. La bonne gouvernance signifie:
- le ramasse-miettes doit se produire suffisamment souvent pour ne pas encombrer le tas géré d'un grand nombre (par rapport ou en quantité absolue) d'objets inutilisés (poubelles) pour lesquels de la mémoire est allouée;
- le ramasse-miettes doit se produire aussi rarement que possible afin de ne pas perdre de temps processeur utile, même si un ramassage plus fréquent permettra une utilisation moindre de la mémoire;
- le ramasse-miettes devrait être productif, car si, à la suite de l'assemblage, seul un petit morceau de mémoire était libéré, l'assemblage et le temps processeur utilisé étaient en vain;
- la collecte des ordures doit être rapide, car de nombreuses charges de travail nécessitent un court délai;
- les développeurs qui écrivent du code managé n'ont pas besoin d'en savoir beaucoup sur la collecte des ordures pour parvenir à une utilisation efficace de la mémoire (par rapport à leur charge de travail);
- Le garbage collector doit s'adapter à la nature différente de l'utilisation de la mémoire.
Description logique du tas géré
Le garbage collector CLR collecte les objets qui sont logiquement séparés par génération. Après avoir assemblé des objets dans la génération N , les objets restants sont marqués comme appartenant à la génération N + 1 . Ce processus est appelé la promotion des objets à travers les générations. Il existe des exceptions dans ce processus lorsqu'il est nécessaire de transférer un objet vers une génération inférieure ou de ne pas l'avancer du tout.
Dans le cas de petits objets, le tas est divisé en trois générations: gen0, gen1 et gen2. Pour les gros objets, il n'y a qu'une seule génération - gen3. Gen0 et gen1 sont appelées générations éphémères (les objets y vivent peu de temps).
Pour un tas de petits objets, le nombre de génération signifie leur âge. Par exemple, gen0 est la plus jeune génération. Cela ne signifie pas que tous les objets de gen0 sont plus jeunes que les objets de gen1 ou gen2. Il existe des exceptions décrites ci-dessous. Assembler une génération signifie assembler des objets dans cette génération, ainsi que dans toutes ses générations plus jeunes.
Théoriquement, l'assemblage de grands et petits objets peut se produire de la même manière. Cependant, comme la compression de gros objets nécessite beaucoup de ressources, leur assemblage se déroule de manière différente. Les objets volumineux sont contenus uniquement dans gen2 et ne sont collectés que lors du ramasse-miettes de cette génération pour des raisons de performances. Gen2 et gen3 peuvent être volumineux, et la construction d'un objet dans les générations éphémères (gen0 et gen1) ne devrait pas être trop gourmande en ressources.
Les objets sont placés dans la plus jeune génération. Pour les petits objets, c'est gen0 et pour les gros objets, gen3.
Description physique du tas géré
Un tas géré se compose d'un ensemble de segments. Un segment est un bloc de mémoire continu que le système d'exploitation transmet au garbage collector. Les segments de tas sont divisés en petites et grandes sections pour accueillir de petits et grands objets. Les segments de chaque tas sont connectés ensemble. Au moins un segment pour un petit objet et un pour un grand sont réservés lors du chargement du CLR.
Dans chaque tas de petits objets, il n'y a qu'un seul segment éphémère, où se trouvent les générations gen0 et gen1. Ce segment peut contenir ou non des objets de génération gen2. En plus des segments éphémères, un ou plusieurs segments supplémentaires peuvent exister, qui seront des segments gen2, car ils contiennent des objets de génération 2.
Une pile de gros objets se compose d'un ou plusieurs segments.
Le segment de segment de mémoire est rempli d'adresses inférieures à supérieures. Cela signifie que les objets situés aux adresses inférieures du segment sont plus anciens que ceux situés aux personnes âgées. Il existe également des exceptions décrites ci-dessous.
Les segments de segment sont alloués selon les besoins. S'ils ne contiennent pas d'objets utilisés, les segments sont supprimés. Cependant, le segment initial sur le tas existe toujours. Un segment est alloué à la fois pour chaque segment. Dans le cas de petits objets, cela se produit lors de la récupération de place, et pour les gros objets, lors de l'allocation de mémoire pour eux. Un tel schéma augmente la productivité, car les gros objets ne sont assemblés que dans la génération 2 (ce qui nécessite beaucoup de ressources).
Les segments de tas sont réunis dans des sélections. Le dernier segment de la chaîne est toujours éphémère. Les segments dans lesquels tous les objets sont collectés peuvent être réutilisés, par exemple, comme éphémères. La réutilisation des segments ne s'applique qu'aux tas de petits objets. Pour accueillir de gros objets à chaque fois, l'ensemble des gros objets est pris en compte. Les petits objets ne sont placés que dans des segments éphémères.
Valeur seuil de la mémoire allouée
Il s'agit d'un concept logique lié à la taille de chaque génération. S'il est dépassé, la génération commence la récupération de place.
La valeur de seuil pour une génération particulière est définie en fonction du nombre d'objets survivants dans cette génération. Si ce montant est élevé, la valeur seuil devient plus élevée. Il est prévu que le rapport des objets utilisés et inutilisés sera meilleur lors de la session de récupération de place de la prochaine génération.
Sélection de génération pour la collecte des ordures
Lorsqu'il est activé, le collecteur doit déterminer dans quelle génération construire. Outre la valeur seuil, d'autres facteurs influencent ce choix:
- fragmentation d'une génération - si une génération est très fragmentée, la collecte des ordures ménagères est susceptible d'être productive;
- si la mémoire de la machine est trop occupée, le collecteur peut effectuer un nettoyage plus approfondi, si un tel nettoyage est plus susceptible de libérer de l'espace et d'éviter un échange de page inutile (mémoire dans toute la machine);
- si un segment éphémère manque d'espace, le collecteur peut effectuer un nettoyage plus approfondi dans ce segment (collecter plus d'objets de génération 1) pour éviter d'allouer un nouveau segment de segment.
Processus de collecte des ordures
Étape de marquage
Pendant cette phase, le CLR devrait trouver tous les objets vivants.
L'avantage d'un collecteur prenant en charge les générations est sa capacité à nettoyer les déchets uniquement dans une partie du tas, au lieu d'observer constamment tous les objets. En collectant les ordures dans les générations éphémères, le collecteur doit obtenir des informations de l'environnement d'exécution sur les objets de ces générations qui sont encore utilisés par le programme. De plus, les objets des générations plus âgées peuvent utiliser des objets des générations plus jeunes en se référant à eux.
Pour marquer les anciens objets en référençant de nouveaux, le garbage collector utilise des bits spéciaux. Les bits sont définis par le mécanisme du compilateur JIT pendant les opérations d'affectation. Si l'objet appartient à la génération éphémère, le compilateur JIT définira l'octet contenant le bit indiquant la position initiale. En collectant les ordures dans les générations éphémères, le collecteur peut utiliser ces bits pour l'ensemble du tas restant et afficher uniquement les objets auxquels ces bits correspondent.
Étape de planification
À ce stade, la compression est modélisée pour déterminer son efficacité. Si le résultat est productif, le collecteur commence la compression réelle. Sinon, il fait juste le ménage.
Étape en mouvement
Si le collecteur effectue une compression, cela entraînera le déplacement des objets. Dans ce cas, vous devez mettre à jour les liens vers ces objets. Pendant la phase de déplacement, le collecteur doit trouver tous les liens pointant vers des objets dans les générations où le garbage collection a lieu. En revanche, lors de l'étape de marquage, le collecteur ne marque que les objets vivants, il n'a donc pas besoin de prendre en compte les maillons faibles.
Étape de compression
Cette étape est assez simple, car le collectionneur a déjà déterminé de nouvelles adresses pour les objets en mouvement lors de la phase de planification. Une fois compressés, les objets seront copiés vers ces adresses.
Étape de nettoyage
Au cours de cette phase, le collectionneur recherche l'espace inutilisé entre les objets vivants. Au lieu de cet espace, il crée des objets libres. Les objets non utilisés à proximité deviennent un objet libre. Tous les objets libres sont placés dans la liste des objets libres .
Flux de code
Termes:
- WKS GC: garbage collection en mode station de travail
- SVR GC: garbage collection en mode serveur
Comportement fonctionnel
GC WKS sans garbage collection parallèle
- Le thread utilisateur a utilisé toute la mémoire qui lui est allouée et appelle le garbage collector.
- Le collecteur appelle
SuspendEE
pour suspendre tous les threads gérés. - Le collectionneur choisit une génération pour le nettoyage.
- Le marquage des objets commence.
- Le collecteur passe à l'étape de planification et détermine le besoin de compression.
- Si nécessaire, le collecteur déplace les objets et effectue la compression. Dans un autre cas, il fait juste le ménage.
- Le collecteur appelle
RestartEE
pour redémarrer les threads gérés. - Les threads utilisateur continuent de fonctionner.
GC WKS avec garbage collection parallèle
Cet algorithme décrit la récupération de place en arrière-plan.
- Le thread utilisateur a utilisé toute la mémoire qui lui est allouée et appelle le garbage collector.
- Le collecteur appelle
SuspendEE
pour suspendre tous les threads gérés. - Le collecteur détermine s'il faut exécuter la récupération de place en arrière-plan.
- Si c'est le cas, le thread de récupération de place en arrière-plan est activé. Ce thread appelle
RestartEE
pour reprendre les threads gérés. - L'allocation de mémoire pour les processus gérés se poursuit en même temps que la récupération de place en arrière-plan.
- Un thread utilisateur peut utiliser toute la mémoire qui lui est allouée et démarrer le garbage collection éphémère (également appelé garbage collection haute priorité). Il fonctionne de la même manière qu'en mode station de travail sans récupération de place parallèle.
- Le
SuspendEE
récupération de place en arrière-plan appelle à nouveau SuspendEE
pour terminer le marquage, puis appelle RestartEE
pour démarrer un nettoyage parallèle avec les threads utilisateur en cours d'exécution. - La collecte des ordures en arrière-plan est terminée.
SVR GC sans garbage collection parallèle
- Le thread utilisateur a utilisé toute la mémoire qui lui est allouée et appelle le garbage collector.
- Les threads de récupération de place en mode serveur sont activés et provoquent la
SuspendEE
de l'exécution des threads gérés par SuspendEE. - Les flux de récupération de place en mode serveur effectuent les mêmes opérations qu'en mode station de travail sans récupération de place parallèle.
- Les threads de récupération de place en mode serveur
RestartEE
pour démarrer les threads gérés. - Les threads utilisateur continuent de fonctionner.
GC SVR avec ramasse-miettes parallèle
L'algorithme est le même que dans le cas de la récupération de place parallèle en mode poste de travail, seul l'assemblage non phonon est effectué dans les threads du serveur.
Architecture physique
Cette section vous aidera Ă comprendre le flux de code.
Lorsque le thread utilisateur manque de mémoire, il peut obtenir de l'espace libre à l'aide de la fonction try_allocate_more_space
.
La fonction try_allocate_more_space
appelle GarbageCollectGeneration
lorsque vous devez démarrer le garbage collector.
Si le garbage collection en mode station de travail n'est pas parallèle, GarbageCollectGeneration
est exécuté dans le thread utilisateur appelé par le garbage collector. Le flux de code est le suivant:
GarbageCollectGeneration() { SuspendEE(); garbage_collect(); RestartEE(); } garbage_collect() { generation_to_condemn(); gc1(); } gc1() { mark_phase(); plan_phase(); } plan_phase() { // , // if (compact) { relocate_phase(); compact_phase(); } else make_free_lists(); }
Si la récupération de place parallèle est effectuée en mode station de travail (par défaut), le flux de code pour la récupération de place en arrière-plan ressemble à ceci:
GarbageCollectGeneration() { SuspendEE(); garbage_collect(); RestartEE(); } garbage_collect() { generation_to_condemn(); // // do_background_gc(); } do_background_gc() { init_background_gc(); start_c_gc (); // . wait_to_proceed(); } bgc_thread_function() { while (1) { // // gc1(); } } gc1() { background_mark_phase(); background_sweep(); }
Liens vers les ressources