Votre code C# n'est pas compris directement par le processeur. Il est d'abord traduit dans une langue intermédiaire (l'IL), puis traduit à nouveau en instructions machine au moment de l'exécution. Pendant que l'appli tourne, un « ramasse-miettes » libère régulièrement la mémoire des objets dont plus personne ne se sert, comme un service de nettoyage qui ne jette que ce qui n'est plus rangé nulle part.
Du code source au code machine Junior
C# et VB.NET compilent tous deux, via Roslyn, vers le même IL (Intermediate Language) stocké dans une assembly avec ses métadonnées. C'est pour cela qu'une DLL VB est consommable depuis C# sans friction. L'IL est ensuite traduit en code natif, soit à l'exécution (JIT), soit à la publication (AOT).
| Mode | Démarrage | Débit en régime | Contraintes | Cas d'usage |
|---|---|---|---|---|
| JIT (tiered + PGO dynamique) | Moyen | Le meilleur | Aucune | API longue durée, services |
| ReadyToRun | Rapide | Très bon (re-JIT des méthodes chaudes) | Binaire plus gros, spécifique à l'OS/arch | Cloud Run, clients lourds |
| Native AOT | Très rapide | Bon | Code trim-safe, pas de Reflection.Emit, sérialisation par source generators | Fonctions, CLI, sidecars, scale-to-zero |
Pile, tas, types valeur et référence Junior
Un type valeur (struct, int, DateTime) est copié à l'affectation et vit là où il est déclaré : sur la pile pour une variable locale, inline dans l'objet pour un champ. Un type référence (class, string, tableau) vit sur le tas managé ; la variable ne contient qu'un pointeur. Le boxing copie un type valeur dans un objet sur le tas, par exemple en le passant à un paramètre object ou à une interface non générique.
Span<T> et Memory<T>
Vue sans copie sur un tableau, une chaîne ou de la mémoire native. Span<T> est un ref struct : il reste sur la pile, donc interdit dans un champ de classe ou à travers un await ; utilisez Memory<T> dans ces cas.
ArrayPool et objets poolés
ArrayPool<byte>.Shared.Rent() évite d'allouer de gros buffers à chaque requête. Toujours Return() dans un finally, et ne jamais réutiliser le buffer après l'avoir rendu.
Le ramasse-miettes (GC) générationnel Confirmé
Hypothèse de base : la plupart des objets meurent jeunes. Le tas est donc découpé en générations. Une collecte gen0 est rapide car elle ne parcourt que les objets récents ; les survivants sont promus. Les objets de 85 000 octets ou plus vont directement dans le LOH (Large Object Heap), collecté avec la gen2 et non compacté par défaut.
GEN 0 · 0
GEN 1 · 0
GEN 2 · 0
| Réglage | Effet | Quand |
|---|---|---|
ServerGarbageCollector=true | Un tas et un thread GC par cœur, débit maximal | Défaut ASP.NET Core |
| Workstation GC | Un tas, pauses plus courtes, moins de mémoire | Clients lourds, petits conteneurs |
| DATAS (actif par défaut depuis .NET 9 avec Server GC) | Adapte dynamiquement le nombre de tas à la charge | Conteneurs à mémoire contrainte |
ConcurrentGarbageCollection | GC d'arrière-plan pour la gen2 | Laisser activé |
DOTNET_GCHeapHardLimit / GCHeapHardLimitPercent | Plafond explicite du tas | Quand la limite cgroup ne suffit pas |
IDisposable : ressources non managées
Le GC gère la mémoire, pas les sockets, fichiers ou connexions SQL. IDisposable libère ces ressources de façon déterministe. Un finaliseur (~Classe()) n'est utile que si la classe détient directement un handle natif ; sinon, n'en écrivez pas, car il retarde la collecte d'une génération.
await using var conn = new SqlConnection(cs); // DisposeAsync en fin de portée
await conn.OpenAsync(ct);
// Pour un handle natif, préférer SafeHandle à un finaliseur écrit à la main
sealed class NativeBuffer : IDisposable
{
private readonly SafeFileHandle _handle;
public NativeBuffer(SafeFileHandle h) => _handle = h;
public void Dispose() => _handle.Dispose();
}
Versions et support Junior
| Version | Type | C# | Fin de support | Statut au 15/10/2026 |
|---|---|---|---|---|
| .NET Framework 4.8.1 | Composant Windows | 7.3 | Cycle de vie de l'OS | Maintenance, plus d'évolutions |
| .NET 8 | LTS | 12 | 10 nov. 2026 | Migrer maintenant |
| .NET 9 | STS | 13 | 10 nov. 2026 | Migrer maintenant |
| .NET 10 | LTS | 14 | nov. 2028 | Cible recommandée |
| .NET 11 | STS | 15 | ~2 ans après sa sortie | Sortie attendue nov. 2026 |
Versions paires = LTS (3 ans), impaires = STS (2 ans depuis .NET 9). Figez le SDK avec un global.json ("rollForward": "latestFeature") pour que poste de dev et CI compilent avec la même version.
- Fuite par événement : s'abonner à l'événement d'un objet à longue durée de vie (singleton,
static) garde l'abonné vivant. Se désabonner, ou utiliser un weak event. - Caches statiques sans borne : un
static Dictionaryqui grossit est la fuite la plus fréquente. UtiliserIMemoryCacheavecSizeLimitet expiration. - Fragmentation du LOH : allouer en boucle des
byte[]ou desListde plus de 85 Ko. Passer parArrayPoolou du streaming. GC.Collect()en production : presque toujours un symptôme, jamais un correctif.
À retenir
- C# et VB partagent IL, CLR, GC et BCL : seule la syntaxe diffère.
- JIT pour le débit, ReadyToRun ou AOT pour le démarrage ; l'AOT impose du code sans réflexion dynamique.
- Allouer peu sur les chemins chauds :
Span<T>, pooling, éviter le boxing et les LINQ dans les boucles critiques. - Viser .NET 10 LTS ; .NET 8 et 9 ne sont plus supportés après le 10/11/2026.