Notions de base du garbage collection

Dans la Prise en charge du Common Language Runtime (CLR), le récupérateur de mémoire (GC) a un rôle automatique de manager de mémoire. Le récupérateur de mémoire gère l’allocation et la mise en production de mémoire d’une application. Par conséquent, les développeurs qui utilisent du code managé n’ont pas besoin d’écrire du code pour effectuer des tâches de gestion de la mémoire. La gestion automatique de la mémoire permet d'éliminer des problèmes fréquents tels que l'oubli de libération d'un objet entraînant des fuites de mémoire ou encore les tentatives d'accès à la mémoire libérée à la recherche d'un objet qui a déjà été libéré.

Cet article décrit les concepts fondamentaux du nettoyage de la mémoire.

Avantages

Le récupérateur de mémoire offre les avantages suivants :

  • Il permet aux développeurs de ne pas avoir à mettre manuellement en production de la mémoire.

  • Il alloue efficacement les objets sur le tas managé.

  • Il libère les objets qui ne sont plus utilisés, efface leur mémoire et garde la mémoire disponible pour les futures allocations. Les objets managés obtiennent automatiquement un contenu propre au démarrage, ce qui fait que leurs constructeurs n'ont pas à initialiser chaque champ de données.

  • Il assure la sécurité de la mémoire en veillant à ce qu’un objet ne puisse pas utiliser pour lui-même la mémoire allouée à un autre objet.

Notions de base de la mémoire

La liste suivante résume les concepts importants de la mémoire CLR :

  • Chaque processus possède son propre espace d'adressage virtuel séparé. Tous les processus sur le même ordinateur partagent la même mémoire physique et le fichier d’échange, le cas échéant.

  • Par défaut, sur les ordinateurs 32 bits, chaque processus a un espace d'adressage virtuel en mode utilisateur de 2 Go.

  • En tant que développeur d'applications, vous travaillez uniquement avec l'espace d'adressage virtuel et ne gérez jamais directement la mémoire physique. Le garbage collector alloue et libère la mémoire virtuelle pour vous sur le tas managé.

    Si vous écrivez du code natif, vous utilisez des fonctions Windows pour utiliser l'espace d'adressage virtuel. Ces fonctions allouent et libèrent la mémoire virtuelle pour vous sur les tas natifs.

  • La mémoire virtuelle peut être dans trois états :

    State Description
    Gratuit Il n'existe aucune référence au bloc de mémoire et celui-ci est disponible pour allocation.
    Réservé Le bloc de mémoire est disponible pour votre utilisation et ne peut pas être utilisé pour une autre demande d'allocation. Toutefois, vous ne pouvez pas stocker de données dans ce bloc de mémoire tant qu'il n'est pas validé.
    Committed Le bloc de mémoire est assigné au stockage physique.
  • L’espace d’adressage virtuel peut être fragmenté, ce qui signifie qu’il existe des blocs libres appelés espaces dans l’espace d’adressage. Lorsqu'une allocation de mémoire virtuelle est demandée, le gestionnaire de mémoire virtuelle doit rechercher un bloc unique libre suffisamment grand pour satisfaire la demande d'allocation. Même si vous disposez de 2 Go d'espace libre, l'allocation qui requiert 2 Go échoue, sauf si tout cet espace libre est contenu dans un bloc d'adresses unique.

  • Vous pouvez manquer de mémoire s’il n’y a pas suffisamment d'espace d'adressage virtuel à réserver ou d'espace physique à valider.

    Votre fichier d’échange est utilisé, même si la pression exercée par la mémoire physique (c'est-à-dire, la demande de mémoire physique) est faible. La première fois que la demande de mémoire physique est élevée, le système d'exploitation doit libérer de la place dans la mémoire physique pour stocker des données et sauvegarde une partie des données dans la mémoire physique, dans le fichier d'échange. Les données ne sont pas paginées tant qu'elles ne sont pas nécessaires. Ainsi, la pagination peut se trouver dans des situations dans lesquelles la demande de mémoire physique est très faible.

Allocation de mémoire

Lorsque vous initialisez un nouveau processus, le runtime réserve une région d'espace d'adressage contigu pour le processus. Cet espace d'adressage est appelé le tas managé. Le tas managé garde un pointeur vers l'adresse qui sera allouée au nouvel objet du tas. À l’origine, ce pointeur indique l’adresse de base du tas managé. Tous les types référence sont alloués sur le tas managé. Lorsqu’une application crée le premier type référence, la mémoire est allouée pour le type à l’adresse de base du tas managé. Lorsque l'application crée l'objet suivant, la Prise en charge du Common Language Runtime lui alloue de la mémoire dans l'espace d'adressage qui suit immédiatement le premier objet. Aussi longtemps que de l'espace d'adressage est disponible, la Prise en charge du Common Language Runtime continue à allouer de l'espace pour de nouveaux objets, selon la même procédure.

L'allocation de mémoire à partir du tas managé est plus rapide que l'allocation de mémoire non managée. Étant donné que la Prise en charge du Common Language Runtime alloue de la mémoire pour un objet en ajoutant une valeur à un pointeur, elle est pratiquement aussi rapide que l'allocation de mémoire à partir de la pile. En outre, puisque les nouveaux objets auxquels un espace mémoire est alloué sont stockés à la suite dans le tas managé, une application peut accéder très rapidement aux objets.

Mise en production de la mémoire

Le moteur d'optimisation du « garbage collector » détermine le meilleur moment pour lancer une opération garbage collection sur base des allocations de mémoire effectuées. Lorsque le « garbage collector » effectue une opération garbage collection, il libère la mémoire pour les objets qui ne sont plus utilisées par l'application. Elle détermine les objets qui ne sont plus utilisés en examinant les racines de l'application. Les racines d’une application incluent des champs statiques, des variables locales sur la pile d’un thread, des registres de processeur, des descripteurs de nettoyage de la mémoire et la file d’attente de finalisation. Chaque racine fait référence à un objet du tas managé ou, à défaut, a la valeur Null. Le récupérateur de mémoire peut demander ces racines au reste de la Prise en charge du Common Language Runtime. Le récupérateur de mémoire utilise cette liste pour créer un graphique qui contient tous les objets accessibles à partir des racines.

Les objets non compris dans le graphique ne sont pas accessibles à partir des racines de l'application. Le récupérateur de mémoire examine les objets inaccessibles et met en production la mémoire qui leur a été allouée. Au cours d'une opération garbage collection, le « garbage collector » examine le tas managé pour y détecter les blocs de mémoire occupés par des objets inaccessibles. Chaque fois qu'il détecte un objet inaccessible, il utilise une fonction de copie de mémoire pour compacter les objets accessibles en mémoire et libérer les blocs d'espaces d'adressage alloués aux objets inaccessibles. Lorsque la mémoire allouée aux objets accessibles a été réduite, le « garbage collector » procède aux corrections de pointeurs nécessaires pour que les racines des applications pointent vers les nouveaux emplacements des objets. Il positionne aussi le pointeur du tas managé après le dernier objet accessible.

La mémoire est compactée uniquement si une collection découvre un nombre significatif d'objets inaccessibles. Si tous les objets du tas managé survivent à une collection, il n'est pas nécessaire de procéder à un compactage de la mémoire.

Pour améliorer les performances, le runtime alloue de la mémoire pour les objets de grandes dimensions dans un tas séparé. Le « garbage collector » libère automatiquement la mémoire des objets de grande dimension. Cependant, pour éviter de déplacer des objets volumineux dans la mémoire, cette mémoire est habituellement pas compactée.

Conditions pour une opération garbage collection

Le garbage collection se produit lorsque l'une des conditions suivantes est vraie :

  • Le système possède peu de mémoire physique. La taille de la mémoire est détectée soit par la notification de mémoire insuffisante du système d’exploitation soit par un message similaire provenant de l’hôte.

  • La mémoire utilisée par les objets alloués sur le tas managé dépasse un seuil acceptable. Ce seuil est continuellement ajusté à mesure que le processus s'exécute.

  • La méthode GC.Collect est appelée. Dans presque tous les cas, vous n'avez pas à appeler cette méthode, car le récupérateur de mémoire s'exécute continuellement. Cette méthode est principalement utilisée pour les situations uniques et les tests.

Tas managé

Une fois le récupérateur de mémoire initialisé par le CLR, il alloue un segment de mémoire pour stocker et gérer des objets. Cette mémoire est appelée tas managé, par opposition à un tas natif dans le système d'exploitation.

Il existe un tas managé pour chaque processus managé. Tous les threads du processus allouent de la mémoire pour les objets sur le même tas.

Pour réserver de la mémoire, le récupérateur de mémoire appelle la fonction VirtualAlloc de Windows et réserve un segment de mémoire à la fois pour les applications managées. Le récupérateur de mémoire réserve également des segments si nécessaire et met en production des segments dans le système d’exploitation (après avoir effacé tous leurs objets) en appelant la fonction VirtualFree de Windows.

Important

La taille des segments alloués par le garbage collector est spécifique à l'implémentation et susceptible de changer à tout moment, y compris les mises à jour périodiques. Votre application ne doit jamais faire d'hypothèses concernant une taille de segment particulière, ni dépendre de celle-ci. Elle ne doit pas non plus tenter de configurer la quantité de mémoire disponible pour les allocations de segments.

Moins il y a d'objets alloués sur le tas, moins le garbage collector a à faire. Lorsque vous allouez des objets, n'utilisez pas de valeurs arrondies qui dépassent vos besoins, par exemple en allouant un tableau de 32 octets lorsque vous n’avez besoin que de 15 octets.

Lorsqu'un nettoyage de la mémoire est déclenché, le récupérateur de mémoire libère la mémoire occupée par des objets morts. Le processus de libération compacte les objets vivants afin qu'ils soient déplacés ensemble et l'espace inutilisé est supprimé, ce qui entraîne la diminution du tas. Ce processus garantit que les objets alloués ensemble restent ainsi sur le tas managé, pour conserver leur emplacement.

Le déroulement (fréquence et durée) des garbage collection est le résultat du volume des allocations et de la quantité de mémoire restante sur le tas managé.

Le tas peut être considéré comme l’accumulation de deux tas : le tas de grands objets et le tas de petits objets. Le tas d’objets volumineux contient des objets de 85 000 octets et plus, qui sont généralement des tableaux. Il est rare qu'un objet d'instance soit extrêmement volumineux.

Conseil

Vous pouvez configurer la taille de seuil pour que les objets se trouvent sur le tas d’objets volumineux.

Générations

L’algorithme GC est basé sur plusieurs considérations :

  • il est plus rapide de compacter la mémoire pour une partie du tas managé que pour le tas tout entier ;
  • les nouveaux objets ont des durées de vie plus courtes que les objets plus anciens ;
  • les nouveaux objets sont fréquemment liés entre eux et l'application y accède à peu près au même moment ;

le nettoyage de la mémoire se produit principalement avec la récupération d’objets de courte durée de vie. Pour optimiser le niveau de performance du récupérateur de mémoire, le tas géré est divisé en trois générations, 0, 1 et 2, de sorte qu’il puisse gérer séparément des objets de longue et de courte durée de vie. Le récupérateur de mémoire stocke les nouveaux objets dans la génération 0. Les objets qui sont créés à un stade précoce de la durée de vie de l'application et qui survivent aux collectes sont promus et stockés dans les générations 1 et 2. Parce qu'il est plus rapide de compacter une partie du tas managé que le tas tout entier, ce schéma permet au récupérateur de mémoire de mettre la mémoire en production dans une génération spécifique plutôt que de mettre en production la mémoire de l'intégralité du tas managé chaque fois qu'une collection est effectuée.

  • Génération 0 : il s'agit de la génération la plus récente, qui contient des objets de courte durée de vie. Un exemple d'objet éphémère est une variable temporaire. Le garbage collection a le plus souvent lieu dans cette génération.

    Les objets nouvellement alloués forment une nouvelle génération d’objets et sont implicitement des collections de génération 0. Toutefois, s’il s’agit d’objets volumineux, ils vont sur le tas d’objets volumineux (LOH), qui est parfois appelé génération 3. La génération 3 est une génération physique qui est logiquement collectée dans le cadre de la génération 2.

    La plupart des objets sont libérés pour le nettoyage de la mémoire dans la génération 0 et ne survivent pas à la génération suivante.

    Si une application tente de créer un nouvel objet lorsque la génération 0 est complète, le récupérateur de mémoire exécute une collection pour libérer de l’espace d'adressage pour l’objet. Le « garbage collector » commence par examiner les objets de la génération 0 plutôt que tous les objets du tas managé. Une collection de génération 0 récupère souvent à elle seule suffisamment de mémoire pour permettre à l’application de continuer à créer de nouveaux objets.

  • Génération 1 : cette génération contient des objets de courte durée de vie et sert de tampon entre des objets de courte et de longue durée de vie.

    Une fois que le récupérateur de mémoire a effectué une collection de génération 0, il compacte la mémoire des objets accessibles et les promeut à la génération 1. Étant donné que les objets qui survivent aux collectes ont tendance à avoir de plus longues durées de vie, il est logique de les promouvoir à une génération supérieure. Le récupérateur de mémoire n'a pas à réexaminer les objets des générations 1 et 2 chaque fois qu'il effectue une collection de génération 0.

    Si une collection de génération 0 ne libère pas assez de mémoire pour que l'application puisse créer un nouvel objet, le récupérateur de mémoire peut exécuter une collection de génération 1, puis de génération 2. Les objets qui survivent aux collectes sont promus et stockés dans les générations 1 et 2.

  • Génération 2 : cette génération contient des objets à longue durée de vie. Un exemple d'objet à longue durée de vie est un objet dans une application serveur qui contient des données statiques vivantes pendant tout le processus.

    Les objets de génération 2 qui survivent à une collecte restent dans la génération 2 jusqu'à ce qu'ils soient considérés comme impossibles à atteindre lors d'une prochaine collection.

    Les objets sur le tas d’objets volumineux (parfois appelé génération 3) sont également collectés dans la génération 2.

Les nettoyages de la mémoire se produisent sur des générations spécifiques, selon les conditions spécifiées. La collecte d'une génération signifie la collecte des objets de cette génération et de toutes ses générations plus jeunes. Le nettoyage de la mémoire de génération 2 est également appelé nettoyage de la mémoire complet, car il libère les objets de toutes les générations (autrement dit, tous les objets du tas managé).

Survie et promotions

Les objets qui ne sont pas libérés dans un nettoyage de la mémoire sont appelés survivants et sont promus à la génération suivante :

  • Les objets qui survivent à un nettoyage de la mémoire de génération 0 sont promus à la génération 1.
  • Les objets qui survivent à un nettoyage de la mémoire de génération 1 sont promus à la génération 2.
  • Les objets qui survivent à un nettoyage de la mémoire de génération 2 sont promus à la génération 2.

Lorsque le récupérateur de mémoire détecte que le taux de survie est élevé dans une génération, il augmente le seuil des allocations de cette génération. La collection suivante obtient une taille importante de mémoire récupérée. Le CLR équilibre continuellement deux priorités : ne pas permettre au jeu de travail d'une application de devenir trop grand en retardant le nettoyage de la mémoire et ne pas permettre pas au nettoyage de la mémoire de s’exécuter trop souvent.

Segments et générations éphémères

Étant donné que les objets des générations 0 et 1 ont une courte durée de vie, ces générations sont appelées générations éphémères.

Les générations éphémères doivent être allouées dans le segment de mémoire appelé segment éphémère. Chaque nouveau segment acquis par le garbage collector devient le nouveau segment éphémère et contient les objets qui ont survécu à un garbage collection de génération 0. L'ancien segment éphémère devient le nouveau segment de génération 2.

La taille du segment éphémère varie selon que le système est de 32 ou 64 bits. Elle peut également varier en fonction du type de récupérateur de mémoire exécuté (GC pour station de travail ou serveur). Le tableau suivant montre les tailles par défaut du segment éphémère :

GC pour station de travail/serveur 32 bits 64 bits
Garbage collector pour station de travail 16 Mo 256 octets
Garbage collector pour serveur 64 Mo 4 Go
Garbage collector pour serveur avec > 4 processeurs logiques 32 Mo 2 Go
Garbage collector pour serveur avec > 8 processeurs logiques 16 Mo 1 Go

Le segment éphémère peut inclure des objets de la génération 2. Les objets de génération 2 peuvent utiliser plusieurs segments, autant que votre processus en requiert et que la mémoire en autorise.

La quantité de mémoire libérée à partir d'un garbage collection éphémère est limitée à la taille du segment éphémère. La quantité de mémoire libérée est proportionnelle à l'espace occupé par les objets morts.

Déroulement d’une opération garbage collection

Une opération garbage collection présente les phases suivantes :

  • Une phase de marquage qui recherche et crée une liste de tous les objets actifs.

  • Une phase de déplacement qui met à jour les références aux objets qui seront compactés.

  • Une phase de compactage qui libère l'espace occupé par les objets morts et compacte les objets survivants. La phase de compactage déplace les objets qui ont survécu à un nettoyage de la mémoire vers l'extrémité la plus ancienne du segment.

    Étant donné que les collections de génération 2 peuvent occuper plusieurs segments, les objets promus dans la génération 2 peuvent être déplacés dans un segment plus ancien. Les survivants des générations 1 et 2 peuvent être déplacés vers un autre segment, car ils sont promus à la génération 2.

    Normalement, le tas d'objets volumineux (LOH) n'est pas compacté, car la copie d'objets volumineux implique une diminution du niveau de performance. Toutefois, dans .NET Core et dans .NET Framework 4.5.1 et versions ultérieures, vous pouvez utiliser la propriété GCSettings.LargeObjectHeapCompactionMode pour compacter le tas d’objets volumineux à la demande. En outre, le LOH est automatiquement compacté lorsqu’une limite matérielle est définie en spécifiant :

Le garbage collector utilise les informations suivantes pour déterminer si les objets sont vivants :

  • Racines de la pile : variables de la pile fournies par le compilateur juste-à-temps (JIT) et l'explorateur de pile. Les optimisations JIT peuvent allonger ou raccourcir les régions de code dans lesquelles les variables de pile sont signalées au récupérateur de mémoire.

  • Descripteurs du nettoyage de la mémoire : descripteurs qui pointent vers les objets managés qui peuvent être alloués par le code utilisateur ou par la Prise en charge du Common Language Runtime.

  • Données statiques : objets statiques des domaines d'application qui pourraient référencer d'autres objets. Chaque domaine d'application effectue le suivi de ses objets statiques.

Avant qu'une opération garbage collection ne démarre, tous les threads managés sont suspendus à l'exception du thread qui a déclenché l'opération.

L'illustration suivante montre un thread qui déclenche un nettoyage de la mémoire et entraîne l'interruption des autres threads :

Screenshot of how a thread triggers a Garbage Collection.

Ressources non managées

Pour la plupart des objets créés par votre application, vous pouvez laisser au nettoyage de la mémoire le soin de réaliser automatiquement les tâches de gestion de mémoire requises. Cependant, les ressources non managées requièrent un nettoyage explicite. Le type le plus répandu de ressource non managée est un objet qui enveloppe une ressource de système d'exploitation telle qu'un handle de fichier ou de fenêtre ou une connexion réseau. Bien que le récupérateur de mémoire soit en mesure de suivre la durée de vie d'un objet managé qui encapsule une ressource non managée, il ne possède pas de connaissances spécifiques sur la façon de nettoyer une ressource.

Lors de la définition d’un objet qui encapsule une ressource non managée, il est recommandé de fournir le code nécessaire pour nettoyer la ressource non managée dans une méthode Dispose publique. En fournissant une méthode Dispose, vous donnez la possibilité aux utilisateurs de votre objet d’en mettre explicitement la mémoire en production lorsqu’ils ont fini de s’en servir. Lorsque vous utilisez un objet qui encapsule une ressource non managée, vous devez vous assurer d’appeler Dispose si nécessaire.

Vous devez également fournir un moyen permettant à vos ressources non managées d’être mises en production si un contrôle serveur consommateur de votre type oublie d'appeler Dispose. Vous pouvez utiliser un descripteur sécurisé pour inclure la ressource non managée dans un wrapper ou remplacer la méthode Object.Finalize().

Pour plus d’informations sur le nettoyage des ressources non managées, consultez Nettoyer les ressources non managées.

Voir aussi