Collecteur d'ordures. Cours complet + transfert de BOTR

Dans cet article, vous trouverez deux sources d'informations Ă  la fois:


  1. Cours complet de ramassage des ordures en russe: CLRium # 6 ( atelier actuel ici )
  2. 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

  1. Le thread utilisateur a utilisé toute la mémoire qui lui est allouée et appelle le garbage collector.
  2. Le collecteur appelle SuspendEE pour suspendre tous les threads gérés.
  3. Le collectionneur choisit une génération pour le nettoyage.
  4. Le marquage des objets commence.
  5. Le collecteur passe à l'étape de planification et détermine le besoin de compression.
  6. Si nécessaire, le collecteur déplace les objets et effectue la compression. Dans un autre cas, il fait juste le ménage.
  7. Le collecteur appelle RestartEE pour redémarrer les threads gérés.
  8. 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.


  1. Le thread utilisateur a utilisé toute la mémoire qui lui est allouée et appelle le garbage collector.
  2. Le collecteur appelle SuspendEE pour suspendre tous les threads gérés.
  3. Le collecteur détermine s'il faut exécuter la récupération de place en arrière-plan.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. La collecte des ordures en arrière-plan est terminée.

SVR GC sans garbage collection parallèle

  1. Le thread utilisateur a utilisé toute la mémoire qui lui est allouée et appelle le garbage collector.
  2. 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.
  3. 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.
  4. Les threads de récupération de place en mode serveur RestartEE pour démarrer les threads gérés.
  5. 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


Source: https://habr.com/ru/post/fr463293/


All Articles