Garbage Collector en .NET - Partie 2

2020-08-23 par Badre BSAILA

Dans le précédent article, J’ai exposé quelques notions sur la gestion mémoire .NET notamment : le tas managé, LOH, GC, la collection, les générations et les ségments. Aujourd’hui, je ferai une démonstration de ces dernières. Les prérequis pour pouvoir la suivre sont :

  • .NET Core SDK >= 2.1, lien.
  • dotnet-sos >= 3.0.47001, lien, installation.
  • windbg inclus avec Debugging Tools for Windows, lien.

L’article est la 2ème partie d’une série où je vais couvrir les bases de la gestion mémoire en .NET :

  • Partie 1 - Tas managé, LOH, GC, Collection, Générations et Ségments.
  • Partie 2 - Démonstration avec windbg (article courant).
  • Partie 3 - Graphe des objets et références.
  • Partie 4 - Workstation/Server GC et arrière-plan GC.
  • Partie 5 - Compteurs de performance et événements ETW

Debugging Tools for Windows

Ce pack contient un ensemble de débogueurs et outillage pour déboguer les programmes nativement sur Windows. Il est possible de l’utiliser sur des programmes s’exécutant sur le même hôte ou sur un serveur distant, et avec 2 modes :

  • Le mode utilisateur : permet de déboguer les applications utilisateur qui s’exécutent sur le système d’exploitation Windows.

  • Le mode noyau : avec ce mode on débogue des applications utilisateur et des composants Windows comme les pilotes des périphériques ou le noyau (utile pour analyser les crash dump qui résultent du fameux écran bleu Windows).

Pour le besoin de la démonstration, je vais ajouter le chemin de windbg.exe dans le Path de Windows.

dotnet-sos

c’est un outil .NET qui installe l’extension de débogage SOS.dll. Cette dll expose des informations sur l’environnement d’exécution .NET (CLR) comme l’état du tas, les threads, l’espace virtuel des addresses, … etc.

Avant de commencer

Cherchez/Créez une application dotnet pour analyser sa mémoire avec les outils précédents. La mienne est une application console s’appelant PingMyVM.

  • Démarrons l’application PingMyVM avec windbg:

windbg E:\Workspace\PingMyVM\PingMyVM\bin\Debug\netcoreapp3.1\PingMyVM.exe

alt démarrer windbg

Avec ça, les applications windbg et PingMyVM sont chargées et on mentionne sur windbg: le message “(348c.10c8): Break instruction exception - code 80000003 (first chance)” ce qui veut dire que PingMyVM est pausé. En bas de la fenêtre de windbg, il existe un shell.

  • Chargeons la bibliothèque de CLR, ses symboles de débogage, et son contexte d’exception sur le shell de windbg:

sxeld:coreclr

  • Chargeons aussi la bibliothèque SOS:

.load C:\Users\myname\.dotnet\sos\sos.dll

Et hop nous sommes prêts.

Inspecter la mémoire

  • Je commence par continuer l’exécution de PingMyVM avec:

g (x2)

alt continuer l'exécution

  • Après un certain temps, je la pause avec la combinaison ctrl + break.

  • Je joue la commande:

!sos.eeheap -gc

En résultat, j’obtiens l’emplacement du tas et LOH dans la mémoire plus les ségments et leurs tailles:

Number of GC Heaps: 1
generation 0 starts at 0x000001FE13855110
generation 1 starts at 0x000001FE137655B0
generation 2 starts at 0x000001FE136E1000
ephemeral segment allocation context: none
         segment             begin         allocated              size
000001FE136E0000  000001FE136E1000  000001FE138EEAF8  0x20daf8(2153208)
Large object heap starts at 0x000001FE236E1000
         segment             begin         allocated              size
000001FE236E0000  000001FE236E1000  000001FE236F4CD8  0x13cd8(81112)
Total Size:              Size: 0x2217d0 (2234320) bytes.
  • Je peux récupérer des statistiques sur les objets avec:

!dumpheap -stat

Statistics:
              MT    Count    TotalSize Class Name
00007ffc77abbf08        3          168 Microsoft.Extensions.Configuration.IConfigurationProvider[]
00007ffc77a63138        3          168 System.Globalization.CompareInfo
00007ffc77a0dda0        1          168 System.Net.NetEventSource
00007ffc77a07260        7          168 System.DateTime
00007ffc77a017b8        1          168 System.Diagnostics.Tracing.NativeRuntimeEventSource
00007ffc77fcf3f0        1          176 Newtonsoft.Json.Utilities.TypeInformation[]
00007ffc77e836c8        1          176 System.Threading.Tasks.TplEventSource
00007ffc77bb3b80        1          176 Microsoft.Extensions.DependencyInjection.DependencyInjectionEventSource
00007ffc77f29d28        3          184 System.Threading.ThreadPoolWorkQueue+WorkStealingQueue[]
00007ffc77c8bab8        5          184 Microsoft.Extensions.Logging.ScopeLogger[]
00007ffc77ba3a10        1          184 Microsoft.Extensions.Logging.EventSource.LoggingEventSource
00007ffc78183568        3          192 System.Action`2[[System.Object, System.Private.CoreLib],[System.Object, System.Private.CoreLib]]
00007ffc780a8f58        8          192 System.Reflection.Emit.DynamicScope
00007ffc7807e690        3          192 System.Func`5[[System.Linq.Expressions.Expression, System.Linq.Expressions],[System.String, System.Private.CoreLib],[System.Boolean, System.Private.CoreLib],[System.Collections.ObjectModel.ReadOnlyCollection`1[[System.Linq.Expressions.ParameterExpression, System.Linq.Expressions]], System.Private.CoreLib],[System.Linq.Expressions.LambdaExpression, System.Linq.Expressions]]
00007ffc77e52bb0        3          192 System.Func`1[[System.Net.Http.Headers.MediaTypeHeaderValue, System.Net.Http]]
00007ffc77da49a0        1          192 System.Collections.Generic.Dictionary`2+Entry[[System.String, System.Private.CoreLib],[Microsoft.Extensions.Logging.Logger, Microsoft.Extensions.Logging]][]
00007ffc77ccdd60        3          192 System.Xml.Schema.XmlSchemaInfo
00007ffc77ca92e0        6          192 log4net.Repository.Hierarchy.ProvisionNode

Le tableau recense chaque classe dans le tas, sa taille mémoire, ses nombres d’instances et son méta-tableau (un objet contenant des méta-informations sur la classe).

  • Pour inspecter le méta-tableau d’une classe avec son MT:

!sos.dumpheap -mt 00007ffc77ca92e0

         Address               MT     Size
000001fe13707f68 00007ffc77ca92e0       32     
000001fe13708080 00007ffc77ca92e0       32     
000001fe13708138 00007ffc77ca92e0       32     
000001fe137081d8 00007ffc77ca92e0       32     
000001fe13716cd0 00007ffc77ca92e0       32     
000001fe13717f48 00007ffc77ca92e0       32     

Pour la classe log4net.Repository.Hierarchy.ProvisionNode, j’ai 6 objets.

  • J’inspecte un objet particulier à l’aide de son Address:

!sos.do 000001fe13717f48

Sortie:

Name:        log4net.Repository.Hierarchy.ProvisionNode
MethodTable: 00007ffc77ca92e0
EEClass:     00007ffc77cb0830
Size:        32(0x20) bytes
File:        E:\Workspace\PingMyVM\PingMyVM\bin\Debug\netcoreapp3.1\log4net.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffc77906610  4001b8a        8      System.Object[]  0 instance 000001fe13717f68 _items
00007ffc7790b1e8  4001b8b       10         System.Int32  1 instance                1 _size
00007ffc7790b1e8  4001b8c       14         System.Int32  1 instance                1 _version
  • Pour savoir la distribution de la mémoire du tas par génération et l’espace libre dans chacune :

!sos.heapstat

Sortie:

Heap             Gen0         Gen1         Gen2          LOH          POH
Heap0          629224       981856       542128        81112            0

Free space:                                                               Percentage
Heap0           21152         8328         1008          280            0 SOH:  1% LOH:  0% POH:  0%

Vérifier la mémoire avant et après collection

Les méthodes du GC qui entrent en jeu durant la collection:

  • coreclr!WKS::GCHeap::GarbageCollectGeneration : fait la collection à proprement dire.
  • coreclr!ThreadSuspend::SuspendEE : arrête les threads avant de compacter le tas.
  • coreclr!ThreadSuspend::RestartEE : redémarre les threads après.

Pour détecter une collection, je poserai deux points d’arrêt sur le shell de windbg :

  • bp coreclr!WKS::GCHeap::GarbageCollectGeneration pour l’appel à la collection.
  • bp coreclr!ThreadSuspend::RestartEE pour le redémarrage des threads.

Et je procéderai comme suit:

  • Quand le 1er point d’arrêt est atteint je mesurerai la taille du tas avec !sos.eeheap -gc et !sos.heapstat
  • Je poursuivrai l’exécution (g) après.
  • Quand le 2ème point d’arrêt est atteint je re-mesurerai.
  • Comparaison.

Résultat:

  • Sur le 1er point d’arrêt, j’avais pour les commandes:

    • !sos.eeheap -gc
    Number of GC Heaps: 1
    generation 0 starts at 0x000001FE13855110
    generation 1 starts at 0x000001FE137655B0
    generation 2 starts at 0x000001FE136E1000
    ephemeral segment allocation context: none
             segment             begin         allocated              size
    000001FE136E0000  000001FE136E1000  000001FE13B51FE8  0x470fe8(4657128)
    Large object heap starts at 0x000001FE236E1000
             segment             begin         allocated              size
    000001FE236E0000  000001FE236E1000  000001FE236F4CD8  0x13cd8(81112)
    Total Size:              Size: 0x484cc0 (4738240) bytes.
    ------------------------------
    GC Heap Size:    Size: 0x484cc0 (4738240) bytes.
    
    • !sos.heapstat
    Heap             Gen0         Gen1         Gen2          LOH          POH
    Heap0         3133144       981856       542128        81112            0
      
    Free space:                                                               Percentage
    Heap0          129344         8328         1008          280            0 SOH:  2% LOH:  0% POH:  0%
    
  • Sur le 2ème, j’obtiens:

    • !sos.eeheap -gc
    Number of GC Heaps: 1
    generation 0 starts at 0x000001FE137FAD58
    generation 1 starts at 0x000001FE13775B70
    generation 2 starts at 0x000001FE136E1000
    ephemeral segment allocation context: none
            segment             begin         allocated              size
    000001FE136E0000  000001FE136E1000  000001FE137FAD70  0x119d70(1154416)
    Large object heap starts at 0x000001FE236E1000
             segment             begin         allocated              size
    000001FE236E0000  000001FE236E1000  000001FE236F4CD8  0x13cd8(81112)
    Total Size:              Size: 0x12da48 (1235528) bytes.
    ------------------------------
    GC Heap Size:    Size: 0x12da48 (1235528) bytes.
    
    • !sos.heapstat
    Heap             Gen0         Gen1         Gen2          LOH          POH
    Heap0              24       545256       609136        81112            0
      
    Free space:                                                               Percentage
    Heap0               0         3504         1008          280            0 SOH:  0% LOH:  0% POH:  0%
    

On remarque que la taille de mon tas a été divisé par quasiment 4.

That’s all folks

J’espère que cette démonstration a pu éclaircir la partie 1 qui était abusivement abstraîte 😁. A la prochaine et stay safe 😊.

Références


Badre BSAILA, Ingénieur d'étude et développement .NET sénior