Déboguer une fuite de mémoire dans .NET
Cet article s’applique à : ✔️ SDK .NET Core 3.1 et versions ultérieures
Il peut y avoir des fuites de mémoire lorsque votre application fait référence à des objets dont elle n’a plus besoin pour effectuer la tâche souhaitée. Le référencement de ces objets empêche le récupérateur de mémoire de récupérer la mémoire utilisée. Cela peut entraîner une dégradation des performances et une exception OutOfMemoryException levée.
Ce tutoriel montre les outils permettant d’analyser une fuite de mémoire dans une application .NET à l’aide des outils CLI de diagnostic .NET. Si vous utilisez Windows, vous pourrez peut-être utiliser les outils de diagnostic de la mémoire de Visual Studio pour déboguer la fuite de mémoire.
Ce tutoriel utilise un exemple d’application qui fuite intentionnellement de la mémoire, en tant qu’exercice. Vous pouvez également analyser des applications qui fuient involontairement la mémoire.
Ce didacticiel présente les procédures suivantes :
- Examinez l’utilisation de la mémoire managée avec dotnet-counters.
- Créer un fichier image mémoire.
- Analysez l’utilisation de la mémoire à l’aide du fichier image mémoire.
Prérequis
Le didacticiel utilise :
- SDK .NET Core 3.1 ou versions ultérieures.
- dotnet-counters pour vérifier l’utilisation de la mémoire managée.
- dotnet-dump pour collecter et analyser un fichier d’image mémoire (inclut l’extension de débogage SOS).
- Exemple d’échantillon d’application cible de débogage à diagnostiquer.
Le didacticiel part du principe que les échantillons d’applications et les outils sont installés et prêts à être utilisés.
Examiner l’utilisation de la mémoire managée
Avant de commencer à collecter des données de diagnostic pour aider à la cause racine de ce scénario, assurez-vous que vous voyez effectivement une fuite de mémoire (augmentation de l’utilisation de la mémoire). Vous pouvez utiliser l’outil dotnet-counters pour confirmer cela.
Ouvrez une fenêtre de console et accédez au répertoire dans lequel vous avez téléchargé et décompressé l’échantillon de cible de débogage. Exécutez la cible :
dotnet run
À partir d’une console distincte, recherchez l’ID de processus :
dotnet-counters ps
La sortie doit ressembler à ce qui suit :
4807 DiagnosticScena /home/user/git/samples/core/diagnostics/DiagnosticScenarios/bin/Debug/netcoreapp3.0/DiagnosticScenarios
Vérifiez maintenant l’utilisation de la mémoire managée avec l’outil dotnet-counters. Le --refresh-interval
spécifie le nombre de secondes entre les actualisations :
dotnet-counters monitor --refresh-interval 1 -p 4807
La sortie en direct doit être similaire à :
Press p to pause, r to resume, q to quit.
Status: Running
[System.Runtime]
# of Assemblies Loaded 118
% Time in GC (since last GC) 0
Allocation Rate (Bytes / sec) 37,896
CPU Usage (%) 0
Exceptions / sec 0
GC Heap Size (MB) 4
Gen 0 GC / sec 0
Gen 0 Size (B) 0
Gen 1 GC / sec 0
Gen 1 Size (B) 0
Gen 2 GC / sec 0
Gen 2 Size (B) 0
LOH Size (B) 0
Monitor Lock Contention Count / sec 0
Number of Active Timers 1
ThreadPool Completed Work Items / sec 10
ThreadPool Queue Length 0
ThreadPool Threads Count 1
Working Set (MB) 83
Concentrez-vous sur cette ligne :
GC Heap Size (MB) 4
Vous pouvez voir que la mémoire du tas managée est de 4 Mo juste après le démarrage.
Maintenant, accédez à l’URL https://localhost:5001/api/diagscenario/memleak/20000
.
Observez que l’utilisation de la mémoire est passée à 30 Mo.
GC Heap Size (MB) 30
En observant l'utilisation de la mémoire, vous pouvez facilement déterminer si la mémoire augmente ou fuit. L’étape suivante consiste à collecter les données appropriées pour l’analyse de la mémoire.
Générer une image mémoire
Lors de l’analyse des fuites de mémoire possibles, vous devez accéder au tas de mémoire de l’application pour analyser le contenu de la mémoire. En examinant les relations entre les objets, vous créez des théories expliquant pourquoi la mémoire n'est pas libérée. Une source de données de diagnostic courante est une image mémoire sur Windows ou l’image mémoire de base équivalente sur Linux. Pour générer une image mémoire d’une application .NET, vous pouvez utiliser l’outil dotnet-dump.
À l’aide de l’échantillon de cible de débogage précédemment démarré, exécutez la commande suivante pour générer une image mémoire principale Linux :
dotnet-dump collect -p 4807
Le résultat est une image mémoire principale située dans le même dossier.
Writing minidump with heap to ./core_20190430_185145
Complete
Notes
Pour une comparaison dans le temps, laissez le processus d’origine continuer à s’exécuter après la collecte de la première image mémoire et collectez une deuxième image mémoire de la même façon. Vous aurez alors deux images mémoire sur une période de temps que vous pouvez comparer pour voir où l’utilisation de la mémoire augmente.
Redémarrer le processus ayant échoué
Une fois l’image mémoire collectée, vous devez disposer d’informations suffisantes pour diagnostiquer l’échec du processus. Si le processus ayant échoué s’exécute sur un serveur de production, c’est maintenant le moment idéal pour la correction à court terme en redémarrant le processus.
Dans ce didacticiel, vous avez maintenant terminé avec l’échantillon de cible de débogage et vous pouvez le fermer. Accédez au terminal qui a démarré le serveur, puis appuyez sur Ctrl+C.
Analyser l’image mémoire principale
Maintenant qu’une image mémoire principale est générée, utilisez l’outil dotnet-dump pour analyser l’image mémoire :
dotnet-dump analyze core_20190430_185145
Où core_20190430_185145
est le nom de l’image mémoire principale que vous souhaitez analyser.
Notes
Si vous constatez une erreur indiquant que libdl.so est introuvable, vous devrez peut-être installer le package libc6-dev. Pour plus d’informations, consultez Configuration requise pour .NET sur Linux.
Une invite vous permet d’entrer des commandes SOS. En règle générale, la première chose que vous souhaitez examiner est l’état global du tas managé :
> dumpheap -stat
Statistics:
MT Count TotalSize Class Name
...
00007f6c1eeefba8 576 59904 System.Reflection.RuntimeMethodInfo
00007f6c1dc021c8 1749 95696 System.SByte[]
00000000008c9db0 3847 116080 Free
00007f6c1e784a18 175 128640 System.Char[]
00007f6c1dbf5510 217 133504 System.Object[]
00007f6c1dc014c0 467 416464 System.Byte[]
00007f6c21625038 6 4063376 testwebapi.Controllers.Customer[]
00007f6c20a67498 200000 4800000 testwebapi.Controllers.Customer
00007f6c1dc00f90 206770 19494060 System.String
Total 428516 objects
Ici, vous pouvez voir que la plupart des objets sont des objets String
ou Customer
.
Vous pouvez utiliser à nouveau la commande dumpheap
avec le tableau de méthodes (MT) pour obtenir la liste de toutes les instances String
:
> dumpheap -mt 00007f6c1dc00f90
Address MT Size
...
00007f6ad09421f8 00007faddaa50f90 94
...
00007f6ad0965b20 00007f6c1dc00f90 80
00007f6ad0965c10 00007f6c1dc00f90 80
00007f6ad0965d00 00007f6c1dc00f90 80
00007f6ad0965df0 00007f6c1dc00f90 80
00007f6ad0965ee0 00007f6c1dc00f90 80
Statistics:
MT Count TotalSize Class Name
00007f6c1dc00f90 206770 19494060 System.String
Total 206770 objects
Vous pouvez maintenant utiliser la commande gcroot
sur une instance de System.String
pour voir comment et pourquoi l’objet est rooté :
> gcroot 00007f6ad09421f8
Thread 3f68:
00007F6795BB58A0 00007F6C1D7D0745 System.Diagnostics.Tracing.CounterGroup.PollForValues() [/_/src/System.Private.CoreLib/shared/System/Diagnostics/Tracing/CounterGroup.cs @ 260]
rbx: (interior)
-> 00007F6BDFFFF038 System.Object[]
-> 00007F69D0033570 testwebapi.Controllers.Processor
-> 00007F69D0033588 testwebapi.Controllers.CustomerCache
-> 00007F69D00335A0 System.Collections.Generic.List`1[[testwebapi.Controllers.Customer, DiagnosticScenarios]]
-> 00007F6C000148A0 testwebapi.Controllers.Customer[]
-> 00007F6AD0942258 testwebapi.Controllers.Customer
-> 00007F6AD09421F8 System.String
HandleTable:
00007F6C98BB15F8 (pinned handle)
-> 00007F6BDFFFF038 System.Object[]
-> 00007F69D0033570 testwebapi.Controllers.Processor
-> 00007F69D0033588 testwebapi.Controllers.CustomerCache
-> 00007F69D00335A0 System.Collections.Generic.List`1[[testwebapi.Controllers.Customer, DiagnosticScenarios]]
-> 00007F6C000148A0 testwebapi.Controllers.Customer[]
-> 00007F6AD0942258 testwebapi.Controllers.Customer
-> 00007F6AD09421F8 System.String
Found 2 roots.
Vous pouvez voir que la String
est directement détenue par l’objet Customer
et indirectement détenue par un objet CustomerCache
.
Vous pouvez continuer à vider les objets pour voir que la plupart des objets String
suivent un modèle similaire. À ce stade, l’enquête a fourni suffisamment d’informations pour identifier la cause racine dans votre code.
Cette procédure générale vous permet d’identifier la source des fuites de mémoire majeures.
Nettoyer les ressources
Dans ce didacticiel, vous avez démarré un exemple de serveur Web. Ce serveur aurait dû être arrêté comme expliqué dans la section Redémarrer le processus ayant échoué.
Vous pouvez également supprimer le fichier image mémoire créé.
Voir aussi
- dotnet-trace pour répertorier les processus
- dotnet-counters pour vérifier l’utilisation de la mémoire managée
- dotnet-dump pour collecter et analyser un fichier image mémoire
- dotnet/diagnostics
- Utiliser Visual Studio pour déboguer les fuites de mémoire