Gestion de la mémoire et Garbage Collector en C#
Apprenez la gestion de la mémoire et le garbage collector en C# pour comprendre le cycle de vie des objets.
Dans les applications .NET, la gestion de la mémoire (Memory Management) est effectuée automatiquement par le Garbage Collector (GC). Le développeur n’a pas besoin de gérer manuellement le cycle de vie des objets. Le GC détecte les objets inutilisés et libère la mémoire correspondante. Cependant, comprendre le fonctionnement de la gestion de la mémoire est essentiel pour optimiser les performances.
Qu’est-ce que la Gestion de la Mémoire ?
La gestion de la mémoire consiste à suivre le cycle de vie des objets créés par une application dans la RAM. Dans l’environnement .NET, il existe deux zones de mémoire principales :
- Stack : utilisée pour les types valeur (
int,bool,struct, etc.). Rapide et automatiquement libérée. - Heap : utilisée pour les types référence (
class,string,array, etc.). Gérée par le GC.
Différences entre la Pile (Stack) et le Tas (Heap)
| Caractéristique | Stack | Heap |
|---|---|---|
| Espace Mémoire | Petit et rapide | Grand, espace géré |
| Types Stockés | Types valeur | Types référence |
| Gestion | Automatique (basée sur la portée) | Gérée par le Garbage Collector |
| Cycle de Vie | Libéré à la fin de la méthode | Conservé jusqu’à ce que le GC le détecte |
| Vitesse d’Accès | Très rapide | Plus lente |
Qu’est-ce que le Garbage Collector (GC) ?
Le Garbage Collector est un composant qui libère automatiquement les objets inutilisés du tas. Cela réduit le risque de fuite de mémoire (memory leak). Le GC s’exécute périodiquement pendant l’exécution du programme et libère les objets non référencés.
using System;
class Program
{
static void Main()
{
for (int i = 0; i < 1000; i++)
{
var data = new byte[1024 * 1024]; // 1 Mo
}
Console.WriteLine("Avant le nettoyage de la mémoire...");
GC.Collect(); // Appel manuel du GC
Console.WriteLine("Après le nettoyage de la mémoire...");
}
}
Appeler manuellement le GC est généralement déconseillé et ne doit être fait que dans des cas particuliers (par ex. tests ou opérations très consommatrices de mémoire).
Comment fonctionne le GC ?
Le Garbage Collector fonctionne selon un modèle basé sur les générations. Cela signifie que les objets à courte durée de vie et ceux à longue durée de vie sont stockés dans des zones différentes.
- Gen 0 (Génération 0) : objets nouvellement créés. Zone la plus fréquemment nettoyée.
- Gen 1 : objets ayant survécu à la génération 0. Objets de durée de vie moyenne.
- Gen 2 : objets à longue durée de vie (ex. singletons, caches, données statiques).
Le GC parcourt l’ensemble du tas et libère les objets qui ne sont plus accessibles depuis les références racines.
Différence entre Dispose et Finalize
Le GC ne suffit pas à lui seul pour une bonne gestion de la mémoire.
Les ressources non managées (ex. fichiers, connexions réseau, objets GDI) ne sont pas automatiquement nettoyées par le GC.
Pour ces ressources, on utilise l’interface IDisposable et la méthode Finalize (destructeur).
class RessourceFichier : IDisposable
{
private bool disposed = false;
public void Ecrire(string data)
{
if (disposed)
throw new ObjectDisposedException(nameof(RessourceFichier));
Console.WriteLine($"Écriture des données : {data}");
}
public void Dispose()
{
if (!disposed)
{
Console.WriteLine("Ressource libérée (Dispose).");
disposed = true;
GC.SuppressFinalize(this);
}
}
~RessourceFichier()
{
Console.WriteLine("Finalize appelé.");
}
}
// Utilisation :
using (var fichier = new RessourceFichier())
{
fichier.Ecrire("Test");
}
La méthode Dispose() est utilisée pour le nettoyage manuel,
tandis que Finalize (le destructeur) est appelé par le GC.
SuppressFinalize() empêche le GC d’appeler le destructeur une seconde fois.
Qu’est-ce qu’une Fuite de Mémoire ?
Bien que le GC soit automatique, des fuites de mémoire peuvent survenir si les ressources sont mal gérées. Les gestionnaires d’événements ou références statiques non libérés peuvent empêcher la collecte des objets.
// Exemple classique de fuite de mémoire :
class Operation
{
public event EventHandler DonneesPretes;
}
class Program
{
static Operation op = new Operation();
static void Main()
{
op.DonneesPretes += (s, e) => Console.WriteLine("L'événement reste abonné !");
op = null; // L’événement maintient une référence, le GC ne peut pas nettoyer !
}
}
Dans ce cas, les événements doivent être désinscrits manuellement avec -=.
Modes et Paramètres de Performance du GC
Le GC .NET dispose de deux principaux modes de fonctionnement :
- Workstation GC : optimisé pour les applications de bureau (utilisateur unique, priorité à l’interface graphique).
- Server GC : optimisé pour la collecte parallèle sur les serveurs multi-cœurs.
// Exemple dans app.config ou runtimeconfig.json :
<configuration>
<runtime>
<gcServer enabled="true"/>
</runtime>
</configuration>
La méthode GC.TryStartNoGCRegion() peut empêcher le GC de s’exécuter pendant une certaine période,
garantissant un fonctionnement sans interruption dans les applications temps réel.
Surveiller les Événements du GC
Vous pouvez utiliser GCNotification et GC.GetTotalMemory() pour surveiller quand le GC s’exécute ou combien de mémoire a été libérée.
using System;
class Program
{
static void Main()
{
long avant = GC.GetTotalMemory(false);
var data = new byte[10_000_000];
long apres = GC.GetTotalMemory(false);
Console.WriteLine($"Différence : {(apres - avant) / 1024 / 1024} Mo");
GC.RegisterForFullGCNotification(10, 10);
GC.Collect();
Console.WriteLine("GC déclenché !");
}
}
Exemple : Traitement de Grandes Données
Dans les applications manipulant de gros volumes de données, créer inutilement des objets augmente la charge du GC.
L’exemple suivant montre la réutilisation des objets avec ArrayPool<T> pour optimiser la mémoire.
using System;
using System.Buffers;
class Program
{
static void Main()
{
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024 * 1024); // Louer un buffer de 1 Mo
for (int i = 0; i < buffer.Length; i++)
buffer[i] = 255;
Console.WriteLine("Données traitées.");
pool.Return(buffer); // Pas de charge supplémentaire pour le GC
}
}
ArrayPool permet de réutiliser les objets fréquemment utilisés et réduit la pression sur le GC.
Performances et Bonnes Pratiques
- Évitez d’appeler manuellement le GC ; .NET le gère automatiquement.
- Nettoyez toujours les ressources non managées avec
Disposeouusing. - Évitez les allocations d’objets volumineux (
>85 Ko) ; le Large Object Heap est nettoyé plus lentement. - N’oubliez pas de désinscrire les événements (
-=) ; sinon le GC ne pourra pas les collecter. - Utilisez des pools d’objets (
ArrayPool,ObjectPool) au lieu de créer de nombreux petits objets.
TL;DR
- Le Garbage Collector nettoie automatiquement les objets inutilisés sur le tas.
- La pile (stack) contient des types valeur rapides et temporaires ; le tas (heap) stocke les types référence.
- Le GC fonctionne selon un modèle à générations (0, 1, 2).
- IDisposable fournit un nettoyage manuel pour les ressources non managées.
- Les fuites de mémoire proviennent souvent d’événements ou de références statiques.
- Utilisez des pools d’objets (
ArrayPool,ObjectPool) pour de meilleures performances.
Articles connexes
Code non sécurisé et pointeurs en C#
Apprenez le code non sécurisé et les pointeurs en C# pour manipuler la mémoire et les opérations bas niveau.
IDisposable et le modèle using en C#
Apprenez IDisposable et le modèle using en C# pour gérer les ressources correctement et éviter les fuites mémoire.
Optimisation des performances avec Span<T> et Memory<T> en C#
Apprenez l’optimisation des performances en C# avec Span<T> et Memory<T> pour une gestion mémoire efficace.