MVAESTECH
Guide .NET · 01 · Fondations

Publié le · par Mathieu Vaes

Runtime & mémoire

Comprendre ce que fait le CLR entre votre code source et le processeur, c'est savoir expliquer un démarrage lent, une fuite mémoire ou une pause GC.

Chapitres du guide
En clair

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).

Code source.cs · .vb Roslyncsc / vbc Assembly IL.dll + métadonnées JIT RyuJITtier 0 → tier 1 + PGO Native AOTà la publication Code natifen mémoire Exécutablesans JIT ReadyToRun (R2R) : pré-compile une partie en natif tout en gardant l'IL et le JIT en secours
Deux chemins après l'IL : JIT à l'exécution (défaut, optimisations dynamiques) ou Native AOT (démarrage instantané, mémoire réduite, mais pas de réflexion dynamique ni de chargement d'assembly à chaud).
ModeDémarrageDébit en régimeContraintesCas d'usage
JIT (tiered + PGO dynamique)MoyenLe meilleurAucuneAPI longue durée, services
ReadyToRunRapideTrès bon (re-JIT des méthodes chaudes)Binaire plus gros, spécifique à l'OS/archCloud Run, clients lourds
Native AOTTrès rapideBonCode trim-safe, pas de Reflection.Emit, sérialisation par source generatorsFonctions, 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.

RéglageEffetQuand
ServerGarbageCollector=trueUn tas et un thread GC par cœur, débit maximalDéfaut ASP.NET Core
Workstation GCUn tas, pauses plus courtes, moins de mémoireClients lourds, petits conteneurs
DATAS (actif par défaut depuis .NET 9 avec Server GC)Adapte dynamiquement le nombre de tas à la chargeConteneurs à mémoire contrainte
ConcurrentGarbageCollectionGC d'arrière-plan pour la gen2Laisser activé
DOTNET_GCHeapHardLimit / GCHeapHardLimitPercentPlafond explicite du tasQuand 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

VersionTypeC#Fin de supportStatut au 15/10/2026
.NET Framework 4.8.1Composant Windows7.3Cycle de vie de l'OSMaintenance, plus d'évolutions
.NET 8LTS1210 nov. 2026Migrer maintenant
.NET 9STS1310 nov. 2026Migrer maintenant
.NET 10LTS14nov. 2028Cible recommandée
.NET 11STS15~2 ans après sa sortieSortie 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.

Piège
  • 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 Dictionary qui grossit est la fuite la plus fréquente. Utiliser IMemoryCache avec SizeLimit et expiration.
  • Fragmentation du LOH : allouer en boucle des byte[] ou des List de plus de 85 Ko. Passer par ArrayPool ou 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.

Un projet .NET à cadrer ou à reprendre ?

API REST documentées et testées, applications ASP.NET MVC (y compris VB.NET), migration de services anciens vers une architecture claire : parlons de votre code.