La magie que vous devez comprendre
\n\nQuiconque a écrit en C# ne serait-ce que quelques semaines le sait : la mémoire est gérée par l'environnement d'exécution (CLR). Vous créez simplement des objets avec new, et quand ils ne sont plus nécessaires — le ramasse-miettes (GC) arrive et nettoie tout. Cela ressemble à de la magie, n'est-ce pas ? Mais derrière cette magie se cache une ingénierie rigoureuse, et si vous ne la comprenez pas — votre application commencera à « ralentir », à « consommer » des gigaoctets de RAM ou même plantera avec OutOfMemoryException.
Dans cet article, nous allons regarder sous le capot du CLR et comprendre comment fonctionne exactement la gestion de la mémoire en C#. Vous apprendrez en quoi la pile diffère du tas, ce que sont les générations d'objets, pourquoi IDisposable est votre meilleur ami, et comment éviter les pièges avec le Large Object Heap. C'est parti !
Pile et tas : les fondements dont on ne parle pas dans les manuels
\n\nLorsque votre programme s'exécute, le CLR divise la mémoire en deux zones principales : la pile (Stack) et le tas (Heap). Comprendre la différence entre elles est la base, sans laquelle tout le reste perd son sens.
\n\nLa pile — rapide, mais petite
\nLa pile est une zone mémoire où sont stockés les types valeur (int, bool, struct, enum) et les références vers les objets. Elle fonctionne selon le principe LIFO (Last In, First Out). Lorsque vous appelez une méthode, un cadre lui est alloué dans la pile. Dès que la méthode se termine — le cadre est détruit instantanément. Aucun ramasse-miettes n'est nécessaire ici. La vitesse d'accès à la pile est maximale, mais sa taille est limitée (généralement 1-4 Mo par thread).
\n\nvoid Example()\n{\n int x = 42; // x se trouve dans la pile\n string s = "Bonjour"; // la référence s dans la pile, l'objet lui-même — dans le tas\n}\n\n\nLe tas — plus lent, mais plus spacieux
\nLe tas (Managed Heap) est la mémoire « durable ». C'est ici que vivent tous les types référence (classes, chaînes, tableaux, délégués). L'allocation mémoire dans le tas se produit à chaque new. Et c'est là que le ramasse-miettes entre en jeu. C'est lui qui décide quand la mémoire peut être libérée, et quand il faut attendre.
Important : La pile n'est pas « mauvaise », et le tas n'est pas « bon ». Chacun a son rôle. Les types valeur sont rapides, mais on ne peut pas les passer par référence sans le mot-clé ref. Les types référence sont flexibles, mais nécessitent le GC.
Comment fonctionne le ramasse-miettes en C# : générations et algorithme
\n\nLe ramasse-miettes dans .NET n'est pas simplement « supprimer et oublier ». C'est un mécanisme intelligent qui utilise les générations (Generations) pour minimiser les pauses dans le fonctionnement de l'application.
\n\nTrois générations d'objets
\n- \n
- Gen 0 — les objets nouveau-nés. Ils vivent peu de temps. Le nettoyage de Gen 0 est fréquent et très rapide. \n
- Gen 1 — les « survivants » après un nettoyage. Zone tampon entre les objets jeunes et anciens. \n
- Gen 2 — les objets de longue durée. Le nettoyage de Gen 2 est le plus coûteux et le plus rare. Si un objet survit jusqu'à Gen 2, il y restera probablement longtemps. \n
L'algorithme de fonctionnement du GC, simplifié, ressemble à ceci :
\n- \n
- L'application demande de la mémoire. S'il n'y a pas de place dans le tas — un nettoyage est déclenché. \n
- Le GC construit un graphe d'accessibilité des objets, en partant des racines (champs statiques globaux, variables locales dans les piles des threads, registres CPU). \n
- Tous les objets inaccessibles sont marqués comme « déchets ». \n
- Un compactage (compaction) a lieu — les objets vivants sont déplacés pour éviter la fragmentation. \n
- La mémoire libérée est retournée au pool. \n
Voici un exemple concret de l'impact du GC sur les performances :
\n\nclass HeavyObject\n{\n public byte[] Data = new byte[1024 * 1024]; // 1 Mo\n}\n\nvoid TestGC()\n{\n for (int i = 0; i < 1000; i++)\n {\n var obj = new HeavyObject(); // On alloue 1 Mo dans le tas à chaque fois\n // obj sort de la portée — devient un déchet\n }\n // Après la boucle, le GC peut nettoyer Gen 0 rapidement