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
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)
-
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
- Defrag Tools: #33 - CLR GC - Part 1, Channel 9.
- Documentation de référence sur windbg.
- Bibliothèque C++ du GC sur GitHub.
Badre BSAILA, Ingénieur d'étude et développement .NET sénior