Partager via


Garbage Collector Basics and Performance Hints

 

Rico Mariani
Microsoft Corporation

Avril 2003

Résumé: Le récupérateur de mémoire .NET fournit un service d’allocation à haut débit avec une bonne utilisation de la mémoire et aucun problème de fragmentation à long terme. Cet article explique comment fonctionnent les récupérateurs de mémoire, puis décrit certains des problèmes de performances qui peuvent être rencontrés dans un environnement de nettoyage de la mémoire. (10 pages imprimées)

S’applique à :
   Microsoft® .NET Framework

Contenu

Introduction
Modèle simplifié
Collecte de la mémoire
Performances
Finalisation
Conclusion

Introduction

Afin de comprendre comment utiliser correctement le récupérateur de mémoire et les problèmes de performances que vous pouvez rencontrez lors de l’exécution dans un environnement de nettoyage de la mémoire, il est important de comprendre les principes de base du fonctionnement des récupérateurs de mémoire et la façon dont ces fonctionnements internes affectent les programmes en cours d’exécution.

Cet article est divisé en deux parties : Tout d’abord, je vais aborder la nature du récupérateur de mémoire du Common Language Runtime (CLR) en termes généraux à l’aide d’un modèle simplifié, puis je vais aborder certaines implications de cette structure sur les performances.

Modèle simplifié

À des fins explicites, considérez le modèle simplifié suivant du tas managé. Notez que ce n’est pas ce qui est réellement implémenté.

Figure 1. Modèle simplifié du tas managé

Les règles de ce modèle simplifié sont les suivantes :

  • Tous les objets récupérables par le garbage sont alloués à partir d’une plage contiguë d’espace d’adressage.
  • Le tas est divisé en générations (plus à ce sujet plus tard) de sorte qu’il est possible d’éliminer la plupart des déchets en ne regardant qu’une petite fraction du tas.
  • Les objets d’une génération ont à peu près le même âge.
  • Les générations de plus grand nombre indiquent des zones du tas avec des objets plus anciens: ces objets sont beaucoup plus susceptibles d’être stables.
  • Les objets les plus anciens sont aux adresses les plus basses, tandis que les nouveaux objets sont créés à l’augmentation des adresses. (Les adresses diminuent dans la figure 1 ci-dessus.)
  • Le pointeur d’allocation pour les nouveaux objets marque la limite entre les zones de mémoire utilisées (allouées) et inutilisées (libres).
  • Régulièrement, le tas est compacté en supprimant les objets morts et en faisant glisser les objets vivants vers l’extrémité basse du tas. Cela développe la zone inutilisée au bas du diagramme dans lequel de nouveaux objets sont créés.
  • L’ordre des objets en mémoire reste l’ordre dans lequel ils ont été créés, pour une bonne localité.
  • Il n’y a jamais d’écart entre les objets dans le tas.
  • Seule une partie de l’espace libre est validée. Si nécessaire, ** plus de mémoire est acquise à partir du système d’exploitation dans la plage d’adresses réservée .

Collecte de la mémoire

Le type de collecte le plus simple à comprendre est le nettoyage de la mémoire entièrement compacté, donc je vais commencer par en discuter.

Collections complètes

Dans une collection complète, nous devons arrêter l’exécution du programme et trouver toutes les racines dans le tas GC. Ces racines se présentent sous diverses formes, mais il s’agit notamment de variables empilées et globales qui pointent vers le tas. À partir des racines, nous visitons chaque objet et suivons chaque pointeur d’objet contenu dans chaque objet visité marquant les objets au fur et à mesure. De cette façon, le collecteur aura trouvé chaque objet accessible ou actif . Les autres objets, les objets inaccessibles , sont maintenant condamnés.

Figure 2 : Racines dans le tas GC

Une fois que les objets inaccessibles ont été identifiés, nous voulons récupérer cet espace pour une utilisation ultérieure ; l’objectif du collecteur à ce stade est de faire glisser les objets en direct vers le haut et d’éliminer l’espace perdu. Une fois l’exécution arrêtée, le collecteur peut déplacer tous ces objets en toute sécurité et corriger tous les pointeurs afin que tout soit correctement lié dans son nouvel emplacement. Les objets survivants sont promus au numéro de génération suivant (c’est-à-dire que les limites des générations sont mises à jour) et l’exécution peut reprendre.

Collections partielles

Malheureusement, le garbage collection complet est tout simplement trop coûteux à faire à chaque fois. Il est donc maintenant approprié de discuter de la façon dont le fait d’avoir des générations dans la collection nous aide.

Considérons d’abord un cas imaginaire où nous sommes extraordinairement chanceux. Supposons qu’il y ait eu une collection récente complète et que le tas est bien compacté. L’exécution du programme reprend et certaines allocations se produisent. En fait, beaucoup et beaucoup d’allocations se produisent et après suffisamment d’allocations, le système de gestion de la mémoire décide qu’il est temps de collecter.

C’est là que nous avons de la chance. Supposons que dans tout le temps que nous exécutions depuis la dernière collection, nous n’avons pas du tout écrit sur les objets plus anciens, seuls les nouveaux objets de génération zéro (gen0) ont été écrits. Si cela devait se produire, nous serions dans une bonne situation, car nous pouvons simplifier massivement le processus de garbage collection.

Au lieu de notre collection complète habituelle, nous pouvons simplement supposer que tous les objets plus anciens (gen1,gen 2) sont toujours en vie , ou au moins assez d’entre eux sont vivants qu’il n’est pas utile de regarder ces objets. En outre, étant donné qu’aucun d’entre eux n’a été écrit (vous vous souvenez de la chance que nous avons ?), il n’y a pas de pointeur des objets plus anciens vers les objets plus récents. Donc, ce que nous pouvons faire est de regarder toutes les racines comme d’habitude, et si des racines pointent vers de vieux objets, il suffit d’ignorer celles-ci. Pour les autres racines (celles qui pointent vers la génération0), nous procédons comme d’habitude, en suivant tous les pointeurs. Chaque fois que nous trouvons un pointeur interne qui retourne dans les objets plus anciens, nous l’ignorons.

Une fois ce processus terminé, nous aurons visité tous les objets vivants de la génération0 sans avoir visité les objets des générations plus anciennes. Les objets dela génération 0 peuvent alors être condamnés comme d’habitude et nous glissons uniquement vers le haut de cette zone de mémoire, laissant les anciens objets intacts.

Maintenant, c’est vraiment une grande situation pour nous parce que nous savons que la plupart de l’espace mort est susceptible d’être dans des objets plus jeunes où il y a beaucoup d’attrition. De nombreuses classes créent des objets temporaires pour leurs valeurs de retour, des chaînes temporaires et d’autres classes utilitaires assorties, comme les énumérateurs et ce qui n’est pas le cas. En regardant juste la génération0 nous donne un moyen facile de récupérer la plupart de l’espace mort en ne regardant que très peu d’objets.

Malheureusement, nous n’avons jamais la chance d’utiliser cette approche, car au moins certains objets plus anciens sont susceptibles de changer pour qu’ils pointent vers de nouveaux objets. Si c’est le cas, il ne suffit pas de les ignorer.

Faire fonctionner des générations avec des barrières d’écriture

Pour que l’algorithme ci-dessus fonctionne réellement, nous devons savoir quels objets plus anciens ont été modifiés. Pour mémoriser l’emplacement des objets sale, nous utilisons une structure de données appelée table carte, et pour gérer cette structure de données, le compilateur de code managé génère des barrières d’écriture. Ces deux notions sont au cœur du succès du nettoyage de la mémoire basé sur la génération.

La table carte peut être implémentée de différentes façons, mais la façon la plus simple de la considérer est un tableau de bits. Chaque bit de la table carte représente une plage de mémoire sur le tas, par exemple 128 octets. Chaque fois qu’un programme écrit un objet dans une adresse, le code de barrière d’écriture doit calculer le bloc de 128 octets qui a été écrit, puis définir le bit correspondant dans la table carte.

Une fois ce mécanisme en place, nous pouvons maintenant revenir sur l’algorithme de collection. Si nous effectuons un garbage collection degénération 0, nous pouvons utiliser l’algorithme décrit ci-dessus, en ignorant les pointeurs vers les générations plus anciennes, mais une fois que nous l’avons fait, nous devons également trouver chaque pointeur d’objet dans chaque objet qui se trouve sur un bloc marqué comme modifié dans la table carte. Nous devons les traiter comme des racines. Si nous prenons également en compte ces pointeurs, nous collecterons correctement uniquement les objets degénération 0 .

Cette approche n’aiderait pas du tout si la table carte était toujours pleine, mais dans la pratique, relativement peu de pointeurs des générations plus anciennes sont réellement modifiés, de sorte qu’il y a une économie substantielle de cette approche.

Performances

Maintenant que nous disposons d’un modèle de base pour la façon dont les choses fonctionnent, considérons certaines choses qui pourraient mal tourner et ralentir. Cela nous donnera une bonne idée des sortes de choses que nous devrions essayer d’éviter pour obtenir les meilleures performances du collecteur.

Trop d’allocations

C’est vraiment la chose la plus simple qui peut mal tourner. L’allocation d’une nouvelle mémoire avec le récupérateur de mémoire est vraiment assez rapide. Comme vous pouvez le voir dans la figure 2 ci-dessus, il suffit généralement que le pointeur d’allocation soit déplacé pour créer de l’espace pour votre nouvel objet du côté « alloué », ce qui n’est pas beaucoup plus rapide que cela. Toutefois, tôt ou tard, un garbage collection doit se produire et, toutes choses égales par ailleurs, il est préférable que cela se produise plus tard que plus tôt. Vous voulez donc vous assurer que lorsque vous créez de nouveaux objets, il est vraiment nécessaire et approprié de le faire, même si la création d’un seul objet est rapide.

Cela peut sembler évident, mais en fait, il est remarquablement facile d’oublier qu’une petite ligne de code que vous écrivez peut déclencher beaucoup d’allocations. Par exemple, supposons que vous écriviez une fonction de comparaison d’une sorte et que vos objets ont un champ de mots clés et que vous souhaitez que votre comparaison ne respecte pas la casse sur les mots clés dans l’ordre indiqué. Dans ce cas, vous ne pouvez pas simplement comparer la chaîne de mots clés entière, car la première mot clé peut être très courte. Il serait tentant d’utiliser String.Split pour diviser la chaîne mot clé en morceaux, puis de comparer chaque élément dans l’ordre à l’aide de la comparaison normale qui ne respecte pas la casse. Ça sonne super non ?

Bien, comme il s’avère que le faire comme ça n’est pas une si bonne idée. Vous voyez, String.Split va créer un tableau de chaînes, ce qui signifie qu’un nouvel objet de chaîne pour chaque mot clé à l’origine dans votre chaîne de mots clés, plus un autre objet pour le tableau. Aïe! Si nous le faisons dans le contexte d’une sorte, il s’agit d’un grand nombre de comparaisons et votre fonction de comparaison à deux lignes crée maintenant un très grand nombre d’objets temporaires. Soudain, le garbage collector va travailler très dur en votre nom, et même avec le schéma de collecte le plus intelligent, il y a juste beaucoup de déchets à propre. Il est préférable d’écrire une fonction de comparaison qui ne nécessite pas du tout les allocations.

Allocations de Too-Large

Lorsqu’ils travaillent avec un allocateur traditionnel, tel que malloc(), les programmeurs écrivent souvent du code qui effectue le moins d’appels possible à malloc() parce qu’ils savent que le coût d’allocation est relativement élevé. Cela se traduit par la pratique de l’allocation en blocs, souvent en allocation spéculative des objets dont nous pourrions avoir besoin, afin que nous puissions faire moins d’allocations totales. Les objets pré-alloués sont ensuite gérés manuellement à partir d’un type de pool, créant ainsi une sorte d’allocateur personnalisé à grande vitesse.

Dans le monde géré, cette pratique est beaucoup moins attrayante pour plusieurs raisons :

Tout d’abord, le coût d’une allocation est extrêmement faible : il n’y a pas de recherche de blocs gratuits comme avec les allocateurs traditionnels ; tout ce qui doit se produire est la limite entre les zones libres et allouées doit se déplacer. Le faible coût de l’allocation signifie que la raison la plus convaincante de la mise en pool n’est tout simplement pas présente.

Deuxièmement, si vous choisissez de préallouer, vous effectuerez bien sûr plus d’allocations que ce qui est nécessaire pour vos besoins immédiats, ce qui pourrait à son tour forcer des collectes de déchets supplémentaires qui auraient pu être inutiles.

Enfin, le garbage collector ne pourra pas récupérer de l’espace pour les objets que vous recyclez manuellement, car d’un point de vue global, tous ces objets, y compris ceux qui ne sont pas utilisés actuellement, sont toujours en vie. Vous constaterez peut-être qu’une grande quantité de mémoire est gaspiller en gardant sous la main des objets prêts à l’emploi, mais pas en cours d’utilisation.

Cela ne veut pas dire que la pré-allocation est toujours une mauvaise idée. Vous souhaiterez peut-être le faire pour forcer l’allocation initiale de certains objets, pour instance, mais vous trouverez probablement qu’il est moins convaincant en tant que stratégie générale qu’il ne le serait dans du code non managé.

Trop de pointeurs

Si vous créez une structure de données qui est un grand maillage de pointeurs, vous rencontrez deux problèmes. Tout d’abord, il y aura beaucoup d’écritures d’objets (voir la figure 3 ci-dessous) et, deuxièmement, quand viendra le temps de collecter cette structure de données, vous allez faire en sorte que le garbage collector suive tous ces pointeurs et, si nécessaire, les modifier à mesure que les choses se déplacent. Si votre structure de données est de longue durée et ne change pas beaucoup, le collecteur n’aura besoin de visiter tous ces pointeurs que lorsque des collections complètes se produisent (au niveau gen2 ). Mais si vous créez une telle structure sur une base temporaire, par exemple dans le cadre du traitement des transactions, vous payerez le coût beaucoup plus souvent.

Figure 3. Structure de données lourde en pointeurs

Les structures de données qui sont lourdes en pointeurs peuvent également avoir d’autres problèmes, non liés au temps de garbage collection. Là encore, comme nous l’avons vu précédemment, lorsque des objets sont créés, ils sont alloués de manière contiguë dans l’ordre d’allocation. Cela est idéal si vous créez une structure de données volumineuse, éventuellement complexe, en restaurant des informations à partir d’un fichier, pour instance. Même si vous avez des types de données disparates, tous vos objets seront rapprochés en mémoire, ce qui à son tour aidera le processeur à avoir un accès rapide à ces objets. Toutefois, à mesure que le temps passe et que votre structure de données est modifiée, de nouveaux objets devront probablement être attachés aux anciens objets. Ces nouveaux objets auront été créés beaucoup plus tard et ne seront donc pas proches des objets d’origine en mémoire. Même lorsque le garbage collector compacte votre mémoire, vos objets ne sont pas mélangés en mémoire, ils se contentent de « glisser » ensemble pour supprimer l’espace gaspillé. Le désordre résultant peut devenir si mauvais au fil du temps que vous pourriez être enclin à faire une nouvelle copie de l’ensemble de votre structure de données, tout bien emballé, et laisser l’ancien désordonné être condamné par le collecteur en temps voulu.

Trop de racines

Le garbage collector doit bien sûr accorder un traitement spécial aux racines au moment de la collecte , ils doivent toujours être énumérés et dûment considérés à leur tour. La collection gen0 ne peut être rapide que dans la mesure où vous ne lui donnez pas un flot de racines à prendre en compte. Si vous deviez créer une fonction récursive profonde qui a de nombreux pointeurs d’objet parmi ses variables locales, le résultat peut en fait être assez coûteux. Ce coût est dû non seulement à la prise en compte de toutes ces racines, mais aussi au nombre très élevé d’objets gen0 que ces racines peuvent garder en vie pendant peu de temps (voir ci-dessous).

Trop d’écritures d’objet

Une fois de plus, en référence à notre discussion précédente, n’oubliez pas que chaque fois qu’un programme managé a modifié un pointeur d’objet, le code de barrière d’écriture est également déclenché. Cela peut être mauvais pour deux raisons :

Tout d’abord, le coût de la barrière d’écriture peut être comparable au coût de ce que vous essayiez de faire en premier lieu. Si vous effectuez, pour instance, des opérations simples dans un type quelconque de classe énumérateur, vous devrez peut-être déplacer certains de vos pointeurs clés de la collection main vers l’énumérateur à chaque étape. C’est en fait quelque chose que vous pouvez éviter, car vous doublez effectivement le coût de copie de ces pointeurs en raison de la barrière d’écriture et vous devrez peut-être le faire une ou plusieurs fois par boucle sur l’énumérateur.

Deuxièmement, déclencher des barrières d’écriture est doublement mauvais si vous écrivez sur des objets plus anciens. Lorsque vous modifiez vos anciens objets, vous créez efficacement des racines supplémentaires pour case activée (décrit ci-dessus) lors du garbage collection suivant. Si vous avez modifié suffisamment de vos anciens objets, vous annulez les améliorations de vitesse habituelles associées à la collecte uniquement de la génération la plus jeune.

Ces deux raisons sont bien sûr complétées par les raisons habituelles de ne pas faire trop d’écritures dans n’importe quel type de programme. Toutes choses étant égales par ailleurs, il est préférable de toucher moins de mémoire (en lecture ou en écriture, en fait) afin de faire une utilisation plus économique du cache du processeur.

Trop d’objets à vie presque longue

Enfin, le plus grand piège du garbage collector générationnel est peut-être la création de nombreux objets, qui ne sont ni exactement temporaires ni exactement de longue durée de vie. Ces objets peuvent causer beaucoup de problèmes, car ils ne seront pas nettoyés par une collection gen0 (le moins cher), car ils seront toujours nécessaires, et ils pourraient même survivre à une collection gen1 parce qu’ils sont toujours en cours d’utilisation, mais ils meurent bientôt après cela.

Le problème, c’est qu’une fois qu’un objet est arrivé au niveau gen2 , seule une collection complète s’en débarrassera, et les collectes complètes sont suffisamment coûteuses pour que le garbage collector les retarde aussi longtemps que possible. Ainsi, le résultat d’avoir de nombreux objets « à vie presque longue » est que votre gen2 aura tendance à croître, potentiellement à un rythme alarmant; il peut ne pas être nettoyé presque aussi vite que vous le souhaitez, et quand il est nettoyé, il sera certainement beaucoup plus coûteux de le faire que vous auriez pu le souhaiter.

Pour éviter ces types d’objets, vos meilleures lignes de défense vont comme suit :

  1. Allouez le moins d’objets possible, en montrant toute l’attention nécessaire à la quantité d’espace temporaire que vous utilisez.
  2. Limitez au minimum les tailles d’objet à durée de vie plus longue.
  3. Conservez autant de pointeurs d’objets que possible sur votre pile (il s’agit de racines).

Si vous effectuez ces opérations, vos collections gen0 sont plus susceptibles d’être très efficaces, et la génération1 ne se développera pas très rapidement. Par conséquent, les collections gen1 peuvent être effectuées moins fréquemment et, quand il devient prudent de faire une collection gen1 , vos objets de moyenne durée de vie seront déjà morts et peuvent être récupérés, à moindre coût, à ce moment-là.

Si les choses vont bien, pendant les opérations à l’état stable, votre taille gen2 n’augmentera pas du tout!

Finalisation

Maintenant que nous avons abordé quelques sujets avec le modèle d’allocation simplifié, j’aimerais compliquer un peu les choses afin que nous puissions discuter d’un autre phénomène important, à savoir le coût des finaliseurs et de la finalisation. Brièvement, un finaliseur peut être présent dans n’importe quelle classe. Il s’agit d’un membre facultatif que le garbage collector promet d’appeler sur des objets morts avant de récupérer la mémoire de cet objet. En C#, vous utilisez la syntaxe ~Class pour spécifier le finaliseur.

Impact de la finalisation sur la collection

Lorsque le garbage collector rencontre pour la première fois un objet qui est par ailleurs mort, mais qui doit encore être finalisé, il doit abandonner sa tentative de récupération de l’espace pour cet objet à ce moment-là. L’objet est ajouté à une liste d’objets nécessitant une finalisation et, en outre, le collecteur doit s’assurer que tous les pointeurs de l’objet restent valides jusqu’à ce que la finalisation soit terminée. Il s’agit essentiellement de la même chose que de dire que chaque objet ayant besoin d’être finalisé est comme un objet racine temporaire du point de vue du collecteur.

Une fois la collection terminée, le thread de finalisation bien nommé passe par la liste des objets nécessitant une finalisation et appelle les finaliseurs. Lorsque cela est fait, les objets redeviennent morts et seront naturellement collectés de la manière normale.

Finalisation et performances

Avec cette compréhension de base de la finalisation, nous pouvons déjà déduire certaines choses très importantes :

Tout d’abord, les objets qui doivent être finalisés vivent plus longtemps que les objets qui n’en ont pas. En fait, ils peuvent vivre beaucoup plus longtemps. Par instance, supposons qu’un objet qui se trouve dans lagénération 2 doit être finalisé. La finalisation sera planifiée, mais l’objet se trouve toujours dans la génération2. Il ne sera donc pas ré-collecté tant que la collection degénération 2 suivante n’aura pas lieu. Cela pourrait être très long en effet, et, en fait, si les choses se passent bien, ce sera long, parce que les collections gen2 sont coûteuses et donc nous voulons qu’elles se produisent très rarement. Les objets plus anciens nécessitant une finalisation peuvent devoir attendre des dizaines, voire des centaines de collections gen0 , avant que leur espace soit récupéré.

Deuxièmement, les objets qui doivent être finalisés provoquent des dommages collatéraux. Étant donné que les pointeurs d’objet internes doivent rester valides, non seulement les objets ayant besoin directement de finalisation s’attarderont dans la mémoire, mais tout ce à quoi l’objet fait référence, directement et indirectement, restera également en mémoire. Si un énorme arbre d’objets était ancré par un seul objet qui devait être finalisé, alors l’arbre entier resterait, potentiellement pendant une longue période, comme nous venons de le discuter. Il est donc important d’utiliser les finaliseurs avec parcimonie et de les placer sur des objets qui ont le moins de pointeurs d’objets internes possible. Dans l’exemple d’arborescence que je viens de donner, vous pouvez facilement éviter le problème en déplaçant les ressources qui ont besoin d’être finalisées vers un objet distinct et en conservant une référence à cet objet à la racine de l’arborescence. Avec ce modeste changement, seul le seul objet (espérons-le, un bel objet petit) s’attarderait et le coût de finalisation est réduit.

Enfin, les objets nécessitant une finalisation créent un travail pour le thread finaliseur. Si votre processus de finalisation est complexe, le seul et unique thread finaliseur passera beaucoup de temps à effectuer ces étapes, ce qui peut entraîner un backlog de travail et, par conséquent, entraîner l’attente de la finalisation d’un plus grand nombre d’objets. Par conséquent, il est essentiel que les finaliseurs fassent le moins de travail possible. Rappelez-vous également que bien que tous les pointeurs d’objet restent valides pendant la finalisation, il se peut que ces pointeurs conduisent à des objets qui ont déjà été finalisés et qui peuvent donc être moins utiles. Il est généralement plus sûr d’éviter de suivre les pointeurs d’objet dans le code de finalisation, même si les pointeurs sont valides. Un chemin de code de finalisation rapide et sécurisé est le meilleur.

IDisposable et Disposer

Dans de nombreux cas, il est possible que des objets qui auraient autrement toujours besoin d’être finalisés pour éviter ce coût en implémentant l’interface IDisposable . Cette interface fournit une autre méthode pour récupérer des ressources dont la durée de vie est bien connue du programmeur, et cela se produit en fait beaucoup. Bien sûr, il est préférable que vos objets n’utilisent que de la mémoire et ne nécessitent donc aucune finalisation ou élimination du tout; mais si la finalisation est nécessaire et qu’il existe de nombreux cas où la gestion explicite de vos objets est facile et pratique, l’implémentation de l’interface IDisposable est un excellent moyen d’éviter, ou du moins de réduire, les coûts de finalisation.

Dans le langage C#, ce modèle peut être très utile :

class X:  IDisposable
{
   public X(…)
   {
   … initialize resources … 
   }

   ~X()
   {
   … release resources … 
   }

   public void Dispose()
   {
// this is the same as calling ~X()
        Finalize(); 

// no need to finalize later
System.GC.SuppressFinalize(this); 
   }
};

Lorsqu’un appel manuel à Disposer évite au collecteur de conserver l’objet en vie et d’appeler le finaliseur.

Conclusion

Le garbage collector .NET fournit un service d’allocation à haut débit avec une bonne utilisation de la mémoire et aucun problème de fragmentation à long terme, mais il est possible de faire des choses qui vous donneront des performances beaucoup moins qu’optimales.

Pour tirer le meilleur parti de l’allocateur, vous devez envisager des pratiques telles que les suivantes :

  • Allouez toute la mémoire (ou autant que possible) à utiliser avec une structure de données donnée en même temps.
  • Supprimez les allocations temporaires qui peuvent être évitées avec peu de pénalités en complexité.
  • Réduisez le nombre de fois où les pointeurs d’objets sont écrits, en particulier les écritures effectuées sur des objets plus anciens.
  • Réduisez la densité des pointeurs dans vos structures de données.
  • Faites un usage limité des finaliseurs, puis uniquement sur les objets « feuilles », autant que possible. Cassez des objets si nécessaire pour vous aider.

Une pratique régulière consistant à examiner vos structures de données clés et à effectuer des profils d’utilisation de la mémoire avec des outils tels que Allocation Profiler contribuera grandement à maintenir l’efficacité de votre utilisation de la mémoire et à faire en sorte que le garbage collector fonctionne au mieux pour vous.