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 :

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

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

Étapes suivantes