Glossaire GC .NET
Termes que l'on rencontre dans les traces GC de .NET (fichiers .nettrace EventPipe). Chaque entrée dit ce que le terme signifie, ce qu'il implique pour les performances et quel réglage agit dessus.
Documentation officielle
- Fundamentals of garbage collection (Microsoft Learn)
- Workstation and server garbage collection (Microsoft Learn)
- Garbage collector configuration settings (Microsoft Learn)
- Latency modes (Microsoft Learn)
Budget de gen 0
La quantité de nouvelles allocations après laquelle une collecte gen 0 démarre. Le runtime l'ajuste dynamiquement.
Le budget dépend de la taille du cache du processeur, du taux de survie des collectes précédentes et du mode de GC (le GC serveur utilise des budgets plus grands). Un taux de survie élevé augmente le budget pour que moins de collectes, plus grosses, aient lieu ; un taux bas le diminue.
Le budget explique pourquoi les collectes gen 0 ont lieu à volume d'allocation régulier, et pourquoi le débit d'allocation est la première chose à regarder quand les collectes sont trop fréquentes. Les événements allocation tick d'une trace (un environ tous les 100 Kio alloués) alimentent les graphiques d'allocation de GC Doctor.
Compactage vs balayage
Après le marquage, le GC soit compacte les survivants, soit note seulement les trous libres (balayage). Il choisit à chaque collecte.
Le compactage rassemble les objets vivants, ce qui supprime la fragmentation et fait de l'allocation un simple avancement de pointeur, mais coûte du temps de copie et la mise à jour des références. Le balayage coûte moins cher mais laisse des trous, mis dans une liste libre et réutilisés.
Le GC compacte normalement les générations éphémères et décide pour gen 2 d'après la fragmentation ; le LOH est balayé par défaut. Une gen 2 qui ne compacte pas alors que le tas continue de grossir suggère une fragmentation. GCSettings.LargeObjectHeapCompactionMode permet de demander un compactage du LOH à la prochaine gen 2 bloquante.
Documentation officielle : Fundamentals of garbage collection (Microsoft Learn)
Épinglage
Fixer un objet à son adresse pour que le GC ne puisse pas le déplacer, comme l'exigent le code natif ou une instruction fixed.
Les objets épinglés (instructions fixed de C#, GCHandle de type Pinned, tampons utilisés par des E/S asynchrones) bloquent le compactage. Le GC doit les contourner, ce qui laisse des trous entre eux (fragmentation) et peut forcer gen 0 ou gen 1 à grandir ou à être promue.
Beaucoup d'épinglages de courte durée, comme dans le code réseau, coûtent peu. Quelques épinglages durables en gen 0 sont le vrai problème. Allouez une fois les tampons épinglés durables, dans le tas des objets épinglés (GC.AllocateArray avec pinned à true) ou dans le LOH, et réutilisez-les.
Finalisation
Un finaliseur est une méthode que le runtime appelle avant de récupérer un objet. Les objets à finaliseur survivent à au moins une collecte de plus.
Quand un objet à finaliseur devient inaccessible, le GC ne peut pas le libérer tout de suite : il le déplace dans la file de finalisation, un thread dédié exécute le finaliseur, et seule une collecte ultérieure récupère la mémoire. Ces objets sont donc promus vers des générations plus anciennes, et un finaliseur lent ou une longue file garde de la mémoire en vie.
Préférez IDisposable avec SafeHandle, appelez Dispose (instructions using) et GC.SuppressFinalize pour sortir l'objet de la file. Une file de finalisation qui grossit, visible dans les compteurs de diagnostic .NET, explique souvent la croissance de gen 2.
Documentation officielle : Cleaning up unmanaged resources (Microsoft Learn)
GC en arrière-plan
Collecte concurrente de la génération 2, qui laisse les collectes gen 0 et gen 1 s'exécuter entre-temps.
Une collecte gen 2 bloquante arrête tous les threads managés pendant longtemps. Avec le GC en arrière-plan (activé par défaut, paramètre System.GC.Concurrent), un thread dédié marque gen 2 en concurrence avec l'application. Si gen 0 se remplit entre-temps, une collecte de premier plan des générations éphémères suspend brièvement les threads, s'exécute, puis laisse la collecte en arrière-plan continuer.
Seule une collecte gen 2 qui ne peut pas être en arrière-plan, par exemple quand elle est provoquée comme bloquante ou quand la mémoire est très basse, est une longue pause. Le GC en arrière-plan réduit les temps de pause au prix d'un peu de CPU et de mémoire. GC Doctor étiquette les collectes .NET en arrière-plan ou bloquantes, ce qui explique pourquoi une collecte gen 2 a ou non causé une longue pause.
Documentation officielle : Background garbage collection (Microsoft Learn)
GC workstation vs serveur
Deux modes de GC : workstation, où la collecte s'exécute sur le thread qui alloue, et serveur, avec un tas et un thread GC dédié par cœur.
Le GC workstation convient aux applications clientes et aux machines partagées entre de nombreux processus ; il peut être concurrent. Le GC serveur (paramètre System.GC.Server) crée par défaut un tas et un thread GC de haute priorité par processeur logique et collecte les tas en parallèle : meilleur débit et tas plus grands, mais plus de mémoire et de CPU, et des pauses qui mobilisent tous les cœurs. Certains hôtes choisissent leur propre valeur par défaut, et les versions récentes peuvent adapter le nombre de tas au comportement d'allocation (DATAS).
En conteneur, le nombre de tas suit la limite de CPU sauf si System.GC.HeapCount le fixe. Un GC serveur dans un processus limité à quelques CPU avec une petite limite mémoire peut gaspiller de la mémoire : vérifiez le nombre de tas dans la trace.
Documentation officielle : Workstation and server garbage collection (Microsoft Learn), Garbage collector configuration settings (Microsoft Learn)
Générations (gen 0, 1, 2)
Le tas managé de .NET est divisé par âge : gen 0 pour les nouveaux objets, gen 1 comme tampon, gen 2 pour les objets de longue durée.
Une collecte de la génération N collecte aussi toutes les générations plus jeunes : une collecte gen 1 inclut gen 0, et une collecte gen 2, dite complète ou bloquante, couvre tout le tas y compris le tas des gros objets. Les survivants de gen 0 sont promus en gen 1, ceux de gen 1 en gen 2. Gen 0 et gen 1 sont les générations éphémères ; elles sont petites et rapides à collecter.
Une application saine montre beaucoup de collectes gen 0, moins de gen 1 et peu de gen 2. Une part élevée de collectes gen 2, ou des collectes gen 1 qui promeuvent beaucoup, signifie que des objets survivent plus longtemps qu'ils ne devraient : caches, rafales d'allocation, objets maintenus en vie par des finaliseurs. Les événements GC d'un .nettrace donnent la génération et la raison de chaque collecte ; GC Doctor les trace par génération.
Documentation officielle : Fundamentals of garbage collection (Microsoft Learn)
Suspension du GC
Avant la plupart des collectes, le runtime doit suspendre tous les threads managés. Le temps que cela prend est le temps de suspension.
Les threads managés sont arrêtés à des points sûrs de leur code. Les threads en code natif sont marqués pour ne pas pouvoir revenir en code managé pendant la collecte. Un thread qui n'atteint pas vite un point sûr (une longue boucle serrée, une longue opération) retarde toute la collecte pendant que les autres attendent.
Dans les traces .NET, le temps de suspension est rapporté séparément du temps de collecte. GC Doctor le trace pour qu'on puisse attribuer une longue pause au collecteur ou à l'attente qui la précède. Les longues suspensions viennent souvent de boucles chaudes, d'une famine du pool de threads ou d'une machine surchargée.
Tas des gros objets (LOH)
Zone séparée pour les objets de 85 000 octets ou plus. Elle n'est collectée qu'avec la génération 2.
Les gros objets ne sont pas alloués en gen 0, car les copier coûterait cher. Le LOH fait logiquement partie de gen 2 : il n'est collecté que lorsqu'une collecte gen 2 a lieu, et il est normalement balayé, pas compacté (les blocs libres sont réutilisés et les voisins fusionnés). Le compactage peut être demandé avec GCSettings.LargeObjectHeapCompactionMode. Les grands tableaux temporaires (tampons, grandes chaînes, listes qui grossissent) remplissent donc vite le LOH, déclenchent des collectes gen 2 coûteuses et peuvent le fragmenter.
Remèdes : réutiliser les tampons (ArrayPool), éviter les collections qui grossissent par doublement, traiter les données en flux au lieu de les charger entières, et ajuster le seuil avec le paramètre GCLOHThreshold si besoin. C'est l'équivalent .NET des objets humongous de G1.
Documentation officielle : The large object heap on Windows (Microsoft Learn)
Tas des objets épinglés (POH)
Tas introduit en .NET 5 pour les objets qui ne doivent jamais bouger, alloués avec GC.AllocateArray et l'option pinned.
Les objets passés à du code natif ou utilisés pour des E/S doivent garder une adresse fixe tant qu'il les utilise. Épingler un objet dans le tas ordinaire empêche le compacteur de le déplacer et peut fragmenter gen 0. Le POH est une zone séparée où ces objets vivent toute leur vie, à l'écart du tas normal.
Seuls des tableaux de types sans références peuvent y être alloués (GC.AllocateArray et GC.AllocateUninitializedArray avec pinned à true). Comme le LOH, le POH est collecté avec gen 2 et n'est pas compacté.