Eseguire il debug di una perdita di memoria in .NET

Questo articolo si applica a: ✔️ .NET Core 3.1 SDK e versioni successive

La memoria può subire una perdita quando l'applicazione fa riferimento a oggetti di cui non ha più bisogno per svolgere l'attività desiderata. Facendo riferimento a questi oggetti si impedisce al Garbage Collector di recuperare la memoria usata. Ciò può comportare una riduzione delle prestazioni e las generazione di un'eccezione OutOfMemoryException generata.

Questa esercitazione illustra gli strumenti per analizzare una perdita di memoria in un'app .NET usando gli strumenti dell'interfaccia della riga di comando di diagnostica .NET. Se si usa Windows, è possibile usare gli strumenti di diagnostica della memoria di Visual Studio per eseguire il debug della perdita di memoria.

Questa esercitazione usa un'app di esempio che subisce intenzionalmente una perdita di memoria come esercizio. È anche possibile analizzare le app che causano involontariamente perdite di memoria.

Questa esercitazione illustra come:

  • Esaminare l'utilizzo della memoria gestita con dotnet-counters.
  • Generare un file dump.
  • Analizzare l'utilizzo della memoria usando il file dump.

Prerequisiti

L'esercitazione usa:

L'esercitazione presuppone che le app e gli strumenti di esempio siano installati e pronti per l'uso.

Esaminare l'utilizzo della memoria gestita

Prima di iniziare a raccogliere i dati di diagnostica per contribuire alla causa radice di questo scenario, assicurarsi di visualizzare effettivamente una perdita di memoria (aumento dell'utilizzo della memoria). È possibile usare lo strumento dotnet-counters per confermarlo.

Aprire una finestra della console e passare alla directory in cui è stato scaricato e decompresso la destinazione di debug di esempio. Eseguire la destinazione:

dotnet run

Da una console separata trovare l'ID processo:

dotnet-counters ps

L'output sarà simile al seguente:

4807 DiagnosticScena /home/user/git/samples/core/diagnostics/DiagnosticScenarios/bin/Debug/netcoreapp3.0/DiagnosticScenarios

Controllare ora l'utilizzo della memoria gestita con lo strumento dotnet-counters. --refresh-interval specifica il numero di secondi tra gli aggiornamenti:

dotnet-counters monitor --refresh-interval 1 -p 4807

L'output live sarà simile al seguente:

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

Concentrarsi su questa riga:

    GC Heap Size (MB)                                  4

È possibile notare che la memoria dell'heap gestita è di 4 MB subito dopo l'avvio.

Passare ora all'URL https://localhost:5001/api/diagscenario/memleak/20000.

Osservare che l'utilizzo della memoria è cresciuto a 30 MB.

    GC Heap Size (MB)                                 30

Osservando l'utilizzo della memoria, è possibile dire in modo sicuro che la perdita di memoria è in fase di crescita o perdita. Il passaggio successivo consiste nel raccogliere i dati corretti per l'analisi della memoria.

Generare il dump della memoria

Quando si analizzano possibili perdite di memoria, è necessario accedere all'heap di memoria dell'app per analizzare il contenuto della memoria. Esaminando le relazioni tra oggetti, si creano teorie sul motivo per cui la memoria non viene liberata. Un'origine dati di diagnostica comune è un dump della memoria in Windows o il dump core equivalente in Linux. Per generare un dump di un'applicazione .NET, è possibile usare lo strumento dotnet-dump.

Usando la destinazione di debug di esempio avviata in precedenza, eseguire il comando seguente per generare un dump di base di Linux:

dotnet-dump collect -p 4807

Il risultato è un dump principale che si trova nella stessa cartella.

Writing minidump with heap to ./core_20190430_185145
Complete

Nota

Per un confronto nel tempo, lasciare che il processo originale continui a essere in esecuzione dopo aver raccolto il primo dump e raccogliere un secondo dump nello stesso modo. Si avranno quindi due dump in un periodo di tempo che è possibile confrontare per vedere dove cresce l'utilizzo della memoria.

Riavviare il processo non riuscito

Dopo aver raccolto il dump, è necessario disporre di informazioni sufficienti per diagnosticare il processo non riuscito. Se il processo non riuscito è in esecuzione in un server di produzione, è il momento ideale per la correzione a breve termine riavviando il processo.

In questa esercitazione è stata completata la destinazione di debug di esempio ed è possibile chiuderla. Passare al terminale che ha avviato il server e premere CTRL+C.

Analizzare il dump principale

Ora che è stato generato un dump principale, usare lo strumento dotnet-dump per analizzare il dump:

dotnet-dump analyze core_20190430_185145

Dove core_20190430_185145 è il nome del dump principale da analizzare.

Nota

Se viene visualizzato un errore che segnala che non è possibile trovare libdl.so, potrebbe essere necessario installare il pacchetto libc6-dev. Per altre informazioni, vedere Prerequisiti per .NET in Linux.

Verrà visualizzato un prompt in cui è possibile immettere comandi SOS. In genere, la prima cosa da esaminare è lo stato complessivo dell'heap gestito:

> 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

Qui è possibile vedere che la maggior parte degli oggetti è String o Customer oggetti.

È possibile usare di nuovo il comando dumpheap con la tabella del metodo (MT) per ottenere un elenco di tutte le istanze di 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

È ora possibile usare il gcroot comando in un'istanzaSystem.String per vedere come e perché l'oggetto è rooted:

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

È possibile notare che String è tenuto direttamente dall'oggetto Customer e indirettamente mantenuto da un oggettoCustomerCache.

È possibile continuare a eseguire il dump degli oggetti per verificare che la maggior parte degli oggetti String segua un modello simile. A questo punto, l'indagine ha fornito informazioni sufficienti per identificare la causa radice nel codice.

Questa procedura generale consente di identificare l'origine di perdite di memoria principali.

Pulire le risorse

In questa esercitazione è stato avviato un server Web di esempio. Questo server dovrebbe essere stato arrestato come illustrato nella sezione Riavviare il processo non riuscito.

È anche possibile eliminare il file dump creato.

Vedi anche

Passaggi successivi