Remarque
L’accès à cette page requiert une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page requiert une autorisation. Vous pouvez essayer de modifier des répertoires.
Dans le Common Language Runtime (CLR), le ramasse-miettes (GC) sert de gestionnaire automatique de la mémoire. Le récupérateur de mémoire gère l’allocation et la libération 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 ramasse-miettes offre les avantages suivants :
Il libère les développeurs de la nécessité de libérer manuellement 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, si celui-ci est présent.
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 ramasse-miettes alloue et libère la mémoire virtuelle pour vous sur le tas géré.
Si vous écrivez du code natif, vous utilisez des fonctions Windows pour utiliser l'espace d'adressage virtuel. Ces fonctions allouent et libèrent pour vous la mémoire virtuelle sur des tas de mémoire natifs.
La mémoire virtuelle peut être dans trois états :
État Descriptif Gratuit Il n'existe aucune référence au bloc de mémoire et celui-ci est disponible pour être alloué. 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é. Engagé 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.
Le fichier d'échange est utilisé même lorsque la pression exercée par la mémoire physique, c'est-à-dire la demande pour celle-ci, est faible. La première fois que la pression sur la mémoire physique est élevée, le système d'exploitation doit libérer de l'espace dans la mémoire physique pour stocker des données, et transfère une partie des données situées dans la mémoire physique vers le fichier d'échange. Les données ne sont pas paginées tant qu'elles ne sont pas nécessaires, donc il est possible de rencontrer de la pagination dans des situations où la pression sur la 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 réservé est appelé le tas mémoire géré. Le tas géré maintient un pointeur vers l'adresse où le prochain objet du tas sera alloué. À l’origine, ce pointeur indique l’adresse de base du tas managé. Tous les types de référence sont alloués sur le tas géré. Lorsqu’une application crée le premier type de référence, la mémoire est allouée pour le type à l’adresse de base du tas géré. Lorsque l'application crée l'objet suivant, le 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, le Common Language Runtime continue d'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, c'est presque 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.
Libération de mémoire
Le moteur d'optimisation du « garbage collector » détermine le meilleur moment pour effectuer une collecte en fonction des allocations de mémoire effectuées. Lorsque le ramasse-miettes effectue une collecte de déchets, il libère la mémoire des objets qui ne sont plus utilisés 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 du processeur, des descripteurs de 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 collecteur de déchets peut demander ces racines au reste du Common Language Runtime. Le collecteur de déchets utilise cette liste pour créer un graphe contenant tous les objets accessibles depuis les racines.
Les objets non compris dans le graphique ne sont pas accessibles à partir des racines de l'application. Le ramasse-miettes considère les objets inaccessibles comme inutiles et libère la mémoire qui leur a été allouée. Au cours d'une collecte de déchets, le garbage collector examine le tas managé pour y chercher 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, l'environnement d'exécution alloue de la mémoire pour les grands objets dans un tas séparé. Le ramasse-miettes libère automatiquement la mémoire des grands objets. 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
La collecte de déchets se produit lorsque l'une des conditions suivantes est remplie :
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 fonctionne en continu. Cette méthode est principalement utilisée pour les situations uniques et les tests.
Le tas managé
Une fois le ramasse-miettes 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 géré, 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 libère des segments dans le système d’exploitation (après les avoir débarrassés de tous les objets) en appelant la fonction VirtualFree de Windows.
Importante
La taille des segments alloués par le collecteur de déchets est spécifique à l'implémentation et est susceptible de changer à tout moment, notamment lors des 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 il y a de travail pour le ramasse-miettes. 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'une collecte des ordures est déclenchée, le ramasse-miettes restaure 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 collections est le résultat du volume des allocations et de la quantité de mémoire survivante 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 ;
La collecte des ordures se produit principalement avec la récupération d'objets à courte durée. Pour optimiser la performance du ramasse-miettes, le tas géré est divisé en trois générations, 0, 1 et 2, afin de pouvoir gérer séparément des objets à vie longue et à vie courte. Le collecteur de déchets 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 ramasse-miettes de libérer la mémoire spécifique à une génération plutôt que de libérer la mémoire de l'intégralité du tas managé à chaque collecte.
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. La collecte de ramasse-miettes se produit le plus fréquemment 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 récupérés pour la collecte des ordures 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 ramasse-miettes exécute une collection pour libérer de l’espace d'adressage pour l’objet. Le ramasse-miettes commence par examiner les objets de la génération 0 plutôt que d'examiner tous les objets du tas géré. 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 réalisé une collecte 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'est pas obligé de 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 de la génération 1 qui survivent aux collectes sont promus à la génération 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 collectes de mémoire ont lieu dans des générations spécifiques lorsque 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. La collecte des déchets de génération 2 est également appelée collecte complète des déchets, car elle libère les objets de toutes les générations (autrement dit, tous les objets du tas géré).
Survie et promotions
Les objets qui ne sont pas récupérés lors d'une collecte de déchets sont appelés survivants et sont promus à la génération suivante :
- Les objets qui survivent au ramasse-miettes de génération 0 sont promus à la génération 1.
- Les objets qui survivent à une collecte des déchets de génération 1 sont promus à la génération 2.
- Les objets qui survivent à une collecte de déchets de génération 2 restent dans la génération 2.
Lorsque le ramasse-miettes 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 prochaine collection bénéficie d'une quantité importante de mémoire récupérée. Le CLR gère en continu deux priorités : ne pas laisser le jeu de travail d'une application devenir trop grand en retardant la collection de déchets et ne pas permettre à la collection de déchets de s'exécuter trop souvent.
Générations éphémères et segments
É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 sont allouées dans le segment de mémoire appelé segment éphémère. Chaque nouveau segment acquis par le ramasse-miettes devient le nouveau segment éphémère et contient les objets qui ont survécu à un ramassage des miettes 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 et du type de collecteur de déchets qu’il exécute (station de travail ou serveur GC). Le tableau suivant montre les tailles par défaut du segment éphémère :
| GC pour station de travail/serveur | 32 bits | 64 bits |
|---|---|---|
| Workstation GC | 16 Mo | 256 Mo |
| Collecteur de mémoire pour serveur | 64 Mo | 4 Go |
| Ramasse-miettes serveur avec > 4 processeurs logiques | 32 Mo | 2 Go |
| GC 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.
Que se passe-t-il lors d'une opération de collecte de déchets
Une ramasse-miettes présente les phases suivantes :
Une phase de marquage qui recherche et crée une liste de tous les objets actifs.
Phase de déplacement qui met à jour les références aux objets 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 à une collecte de déchets 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 :
- Limite de mémoire sur un conteneur.
- Options de configuration du runtime GCHeapHardLimit ou GCHeapHardLimitPercent.
Le ramasse-miettes utilise les informations suivantes pour déterminer si les objets sont actifs :
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 collecteur de mémoire.
Poignées du ramasse-miettes : poignées qui pointent vers des objets managés pouvant être alloués par le code utilisateur ou par le 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 ramasse-miettes et entraîne la suspension des autres threads.
Ressources non managées
Pour la plupart des objets que votre application crée, vous pouvez vous appuyer sur le ramasse-miettes pour gérer automatiquement la mémoire requise. 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 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 manière de libérer cette 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 permettez aux utilisateurs de votre objet de libérer explicitement la ressource lorsqu’ils ont fini de l’utiliser. 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 libérées si un consommateur de votre classe 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.