Notions de base du garbage collection

Dans le Common Language Runtime (CLR), le garbage collector (GC) sert de gestionnaire de mémoire automatique. Le garbage collector gère l’allocation et la libération de mémoire pour une application. Par conséquent, les développeurs qui travaillent avec 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 peut éliminer les problèmes courants tels que l’oubli de libérer un objet et provoquer une fuite de mémoire ou tenter d’accéder à la mémoire libérée pour un objet déjà libéré.

Cet article décrit les concepts fondamentaux du garbage collection.

Avantages

Le garbage collector offre les avantages suivants :

  • Libère la mémoire manuellement pour les développeurs.

  • 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 du contenu propre pour commencer, de sorte que leurs constructeurs n’ont pas besoin d’initialiser chaque champ de données.

  • 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 pour un autre objet.

Notions de base de la mémoire

La liste suivante récapitule 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 de page, s’il en existe un.

  • 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 travailler avec 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’elles ne sont pas validées.
    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 trous dans l’espace d’adressage. Lorsqu’une allocation de mémoire virtuelle est demandée, le gestionnaire de mémoire virtuelle doit trouver un seul bloc libre suffisamment grand pour satisfaire la demande d’allocation. Même si vous avez 2 Go d’espace libre, une allocation nécessitant 2 Go échoue, sauf si tout cet espace libre se trouve dans un seul bloc d’adresses.

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

    Le fichier de page est utilisé même si la sollicitation de la mémoire physique (demande de mémoire physique) est faible. La première fois que la sollicitation de la mémoire physique est élevée, le système d’exploitation doit faire de la place en mémoire physique pour stocker des données et sauvegarder certaines des données qui sont en mémoire physique dans le fichier de page. Les données ne sont pas paginées tant qu’elles ne sont pas nécessaires. Il est donc possible de rencontrer la pagination dans les situations où la sollicitation de la mémoire physique est 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, le « garbage collector » 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, le « garbage collector » 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 le runtime alloue de la mémoire pour un objet en ajoutant une valeur à un pointeur, il est presque aussi rapide que d’allouer de la mémoire à partir de la pile. De plus, étant donné que les nouveaux objets alloués consécutivement sont stockés contigus dans le tas managé, une application peut accéder 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. Il détermine quels objets 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 handles GC 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 au reste du runtime pour ces racines. Le garbage collector utilise cette liste pour créer un graphique qui contient tous les objets accessibles à partir des racines.

Les objets qui ne figurent pas dans le graphique ne sont pas inaccessibles à partir des racines de l’application. Le récupérateur de mémoire considère les objets inaccessibles garbage et libère la mémoire allouée pour eux. 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 important d’objets inaccessibles. Si tous les objets du tas managé survivent à une collection, il n’est pas nécessaire de compacter 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. Toutefois, pour éviter de déplacer des objets volumineux en mémoire, cette mémoire n’est généralement 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 par la notification de mémoire insuffisante du système d’exploitation ou par la mémoire faible, comme indiqué par 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 besoin d’appeler cette méthode, car le garbage collector s’exécute en continu. Cette méthode est principalement utilisée pour les situations uniques et les tests.

Tas managé

Une fois le CLR initialisé le garbage collector, 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 garbage collector appelle la fonction Windows VirtualAlloc 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 les segments selon les besoins et libère les segments vers le système d’exploitation (après les avoir nettoyés de tous les objets) en appelant la fonction Windows VirtualFree .

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, telles que l’allocation d’un tableau de 32 octets lorsque vous n’avez besoin que de 15 octets.

Lorsqu’un garbage collection est déclenché, le garbage collector récupère la mémoire occupée par les objets morts. Le processus de récupération compacte les objets vivants afin qu’ils soient déplacés ensemble, et l’espace mort est supprimé, ce qui rend le tas plus petit. Ce processus garantit que les objets alloués ensemble restent ensemble sur le tas managé pour préserver leur localité.

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 trop volumineux.

Conseil

Vous pouvez configurer la taille de seuil pour les objets à utiliser 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 l’ensemble du tas managé.
  • Les objets plus récents ont des durées de vie plus courtes, et les objets plus anciens ont des durées de vie plus longues.
  • Les objets plus récents ont tendance à être liés les uns aux autres et accessibles par l’application en même temps.

Le garbage collection se produit principalement avec la récupération d’objets de courte durée. Pour optimiser les performances du garbage collector, le tas managé est divisé en trois générations, 0, 1 et 2, afin qu’il puisse gérer des objets de longue durée et de courte durée séparément. Le garbage collector stocke de 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. Étant donné qu’il est plus rapide de compacter une partie du tas managé que l’ensemble du tas, ce schéma permet au garbage collector de libérer la mémoire dans une génération spécifique plutôt que de libérer la mémoire pour l’ensemble du tas managé chaque fois qu’il effectue une collection.

  • Génération 0 : cette génération est la plus jeune et contient des objets de courte durée. 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’ils sont des 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 collectée logiquement dans le cadre de la génération 2.

    La plupart des objets sont récupérés pour le garbage collection en génération 0 et ne survivent pas à la prochaine génération.

    Si une application tente de créer un objet lorsque la génération 0 est complète, le garbage collector effectue un regroupement 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 seule récupère souvent 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 et sert de mémoire tampon entre les objets de courte durée et les objets de longue durée.

    Une fois le garbage collector effectué une collection de génération 0, il compacte la mémoire pour les 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 garbage collector 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 récupère pas suffisamment de mémoire pour que l’application crée un objet, le garbage collector peut effectuer 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 de longue durée. Un exemple d’objet de longue durée est un objet dans une application serveur qui contient des données statiques qui sont actives pendant la durée du processus.

    Les objets de la génération 2 qui survivent à une collection restent de génération 2 jusqu’à ce qu’ils soient déterminés à être inaccessibles dans une collection future.

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

Les collectes de mémoire se produisent dans des générations spécifiques, car les conditions le justifient. 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. Un garbage collection de génération 2 est également appelé garbage collection complète, car il récupère des objets dans toutes les générations (autrement dit, tous les objets dans le tas managé).

Survie et promotions

Les objets qui ne sont pas récupérés dans un garbage collection sont appelés survivants et sont promus vers la prochaine génération :

  • Les objets qui survivent à un garbage collection de génération 0 sont promus vers la génération 1.
  • Les objets qui survivent à un garbage collection de génération 1 sont promus vers la génération 2.
  • Les objets qui survivent à un garbage collection de génération 2 restent de génération 2.

Lorsque le garbage collector détecte que le taux de survie est élevé dans une génération, il augmente le seuil d’allocations pour cette génération. La collection suivante obtient une taille substantielle de mémoire récupérée. Le CLR équilibre continuellement deux priorités : ne pas laisser l’ensemble de travail d’une application être trop volumineux en retardant le garbage collection et en ne laissant pas le garbage collection s’exécuter trop fréquemment.

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

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

Les générations éphémères sont 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 qu’un système est 32 bits ou 64 bits et sur le type de garbage collector qu’il exécute (station de travail ou GC serveur). Le tableau suivant montre les tailles par défaut du segment éphémère :

Gc de 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
Serveur GC avec > 4 processeurs logiques 32 Mo 2 Go
Serveur GC 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 nécessite et la mémoire permet.

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 garbage collection vers l’ancien bout 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 de la génération 1 et de la génération 2 peuvent être déplacés vers un autre segment, car ils sont promus vers la génération 2.

    En règle générale, le tas d’objets volumineux (LOH) n’est pas compacté, car la copie d’objets volumineux impose une pénalité de performance. Toutefois, dans .NET Core et dans .NET Framework 4.5.1 et versions ultérieures, vous pouvez utiliser la propriété pour compacter le GCSettings.LargeObjectHeapCompactionMode tas d’objets volumineux à la demande. En outre, le LOH est automatiquement compacté lorsqu’une limite dure est définie en spécifiant les éléments suivants :

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

  • Racines de pile : variables de pile fournies par le compilateur juste-à-temps (JIT) et le marcheur de pile. Les optimisations JIT peuvent allonger ou raccourcir les régions de code dans lesquelles les variables de pile sont signalées au garbage collector.

  • Handles garbage collection : gère qui pointe vers des objets managés et qui peuvent être alloués par le code utilisateur ou le common language runtime.

  • Données statiques : objets statiques dans les domaines d’application qui peuvent faire référence à 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 garbage collection et entraîne la suspension des autres threads :

Capture d’écran de la façon dont un thread déclenche un garbage collection.

Ressources non managées

Pour la plupart des objets créés par votre application, vous pouvez vous appuyer sur le garbage collection pour effectuer automatiquement les tâches de gestion de la mémoire nécessaires. 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 garbage collector puisse suivre la durée de vie d’un objet managé qui encapsule une ressource non managée, il n’a pas de connaissances spécifiques sur la façon de nettoyer la ressource.

Lorsque vous définissez 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 publique Dispose . En fournissant une Dispose méthode, vous allez permettre aux utilisateurs de votre objet de libérer explicitement la ressource lorsqu’ils sont terminés avec l’objet. Lorsque vous utilisez un objet qui encapsule une ressource non managée, veillez à appeler Dispose si nécessaire.

Vous devez également fournir un moyen de libérer vos ressources non managées si un consommateur de votre type oublie d’appeler Dispose. Vous pouvez utiliser un handle sécurisé pour encapsuler la ressource non managée ou remplacer la Object.Finalize() méthode.

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

Voir aussi