La gestion mémoire automatique de .NET est un atout majeur pour les développeurs. Mais dans certaines applications critiques (UI, temps réel, traitements lourds), une mauvaise compréhension du garbage collector (GC) peut entraîner des ralentissements notables.
Avec .NET 9, de nouvelles options et des réglages plus fins permettent d’adapter le comportement du GC à vos besoins. Voici comment tirer parti de ces mécanismes pour améliorer les performances de vos applications.
🧠 Comment fonctionne le GC en .NET ?
- Le GC .NET est générationnel : objets jeunes (Gen0) sont collectés plus souvent que les anciens (Gen2).
- Il est concurrent : collecte pendant que l'application tourne, avec des pauses minimales.
- Le mode Server optimise les performances multithreadées.
⚙️ Choisir une stratégie de GC adaptée
.NET propose plusieurs modes que vous pouvez activer via runtimeconfig.json ou programme :
- Workstation GC (par défaut)
- Optimisé pour les applications UI
- Réduit les pauses
- Mono-threadé dans ses collectes
- Server GC
- Optimisé pour les serveurs, APIs, backends
- Multithread, collecte parallèle
- Utilise plus de mémoire pour moins de pauses
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
}
}
D'ailleurs, concernant cette configuration je vais être plus précis afin que vous sachiez exactement comment et où la changer ;
Voici les exemples complets pour configurer le Garbage Collector en mode Server ou Workstation selon les deux approches principales :
✅ 1. Configuration dans le .csproj
Ajoutez la propriété <ServerGarbageCollection> dans le fichier projet :
➤ Exemple pour activer le GC Server :
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
</Project>
➤ Exemple pour forcer le GC Workstation :
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ServerGarbageCollection>false</ServerGarbageCollection>
</PropertyGroup>
</Project>
📝 Si vous n’indiquez rien, .NET choisira automatiquement le mode le plus adapté (souvent Workstation pour les apps desktop, Server pour ASP.NET Core).
✅ 2. Configuration via runtimeconfig.json ou runtimeconfig.template.json
➤ Exemple Server GC dans un runtimeconfig.template.json :
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
}
}
➤ Exemple Workstation GC :
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": false
}
}
}
📌 Où placer le fichier JSON ?
-
Pour une application publiée : MonApp.runtimeconfig.json, placé à côté de l'exécutable
-
Pour le build (SDK style) : placez un runtimeconfig.template.json dans le projet, il sera fusionné automatiquement
📎 À savoir
-
Le mode Server est incompatible avec certaines contraintes de threading UI (d’où son exclusion par défaut sur WPF/WinForms/MAUI).
-
Vous pouvez combiner cette option avec d’autres, comme :
Mais revenons au sujet principal de ce billet :
- SustainedLowLatency
- Pour les apps sensibles aux temps de pause (trading, temps réel)
- Empêche le passage en Gen2 sauf si mémoire critique
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
🧪 Exemple : forcer une pause douce du GC
using System;
using System.Runtime;
class Program
{
static void Main()
{
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
for (int i = 0; i < 1000; i++)
{
var arr = new byte[1024 * 100]; // Allocation temporaire
// Traitement ici
}
GC.Collect(); // déclenchement manuel si besoin
}
}
Ce mode limite la pression sur les Gen2, donc les pauses longues.
🧰 .NET 9 : nouveautés utiles pour le monitoring
Avec dotnet-counters, vous pouvez surveiller :
- Temps passé dans la collecte
- Nombre de collectes Gen0/1/2
- Taux d’allocations par seconde
dotnet-counters monitor -p <pid> System.Runtime
Extrait de sortie :
Gen 0 GC Count 1200
Gen 1 GC Count 300
Gen 2 GC Count 5
Allocated Bytes/sec 10 MB
⚠️ Erreurs classiques à éviter
- Appeler GC.Collect() trop souvent : contre-productif, sauf cas extrême.
- Allouer en boucle des gros objets (>85kB) : vont sur le Large Object Heap (LOH), plus lent à gérer.
- Mélanger des objets courts et longs vivant ensemble : augmente la fragmentation.
✅ Bonnes pratiques
- Utiliser ArrayPool<T> pour les buffers réutilisables
- Éviter les allocations dans les boucles critiques (voir Span<T>)
- Surveiller avec dotnet-trace ou dotnet-counters
- Préférer using + Dispose() pour la durée de vie explicite
🔚 Conclusion
Le garbage collector de .NET 9 est puissant, adaptable, mais il n'est pas magique. Un bon développeur saura choisir la stratégie appropriée, surveiller les métriques et ajuster son code pour éviter les pièges classiques.
Maîtriser la mémoire, c’est aussi maîtriser la performance globale de son application. J'aborderai dans un prochain article comment utiliser MemoryPool<T> et ArrayPool<T> pour encore plus de contrôle. Quand ? Ce sera une surprise. Heureux seront ceux qui sont viennent ici tous les vendredi à partir de midi !
Stay Tuned !