Condividi tramite


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ò verificarsi un leak quando l'app fa riferimento a oggetti di cui non ha più bisogno per eseguire 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 viene generata un'eccezione OutOfMemoryException.

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 utilizza un'app di esempio che perde intenzionalmente memoria, come parte dell'esercizio. È anche possibile analizzare le app che causano involontariamente perdite di memoria.

In questa esercitazione si eseguiranno le seguenti attività:

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

Prerequisiti

Il tutorial utilizza:

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

Se l'app esegue una versione di .NET precedente a .NET 9, l'interfaccia utente di output dei contatori dotnet sarà leggermente diversa; per informazioni dettagliate, vedere dotnet-counters .

Esaminare l'utilizzo della memoria gestita

Prima di iniziare a raccogliere i dati di diagnostica per individuare la causa principale di questo scenario, assicurarsi di aver effettivamente individuato un leak di memoria (ovvero un aumento dell'uso della memoria). È possibile usare lo strumento dotnet-counters per confermarlo.

Aprire una finestra della console e passare alla directory in cui è stata scaricata e decompressa la destinazione di debug di esempio. Esegui il target

dotnet run

Da una console separata, trova l'ID del processo.

dotnet-counters ps

L'output dovrebbe essere simile a:

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

Annotazioni

Se il comando precedente non funziona o non viene trovato, è probabile che sia necessario installare prima lo dotnet-counters strumento. Utilizza il seguente comando:

dotnet tool install --global dotnet-counters

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 dovrebbe essere simile al seguente:

Press p to pause, r to resume, q to quit.
Status: Running

Name                                                            Current Value
[System.Runtime]
    dotnet.assembly.count ({assembly})                              111
    dotnet.gc.collections ({collection})
    gc.heap.generation
    ------------------
    gen0                                                              1
    gen1                                                              0
    gen2                                                              0
    dotnet.gc.heap.total_allocated (By)                       4,431,712
    dotnet.gc.last_collection.heap.fragmentation.size (By)
    gc.heap.generation
    ------------------
    gen0                                                        803,576
    gen1                                                         15,456
    gen2                                                              0
    loh                                                               0
    poh                                                               0
    dotnet.gc.last_collection.heap.size (By)
    gc.heap.generation
    ------------------
    gen0                                                        811,960
    gen1                                                      1,214,720
    gen2                                                              0
    loh                                                               0
    poh                                                          24,528
    dotnet.gc.last_collection.memory.committed_size (By)      4,296,704
    dotnet.gc.pause.time (s)                                          0.003
    dotnet.jit.compilation.time (s)                                   0.329
    dotnet.jit.compiled_il.size (By)                            120,212
    dotnet.jit.compiled_methods ({method})                            1,202
    dotnet.monitor.lock_contentions ({contention})                    2
    dotnet.process.cpu.count ({cpu})                                 22
    dotnet.process.cpu.time (s)
    cpu.mode
    --------
    system                                                            0.344
    user                                                              0.344
    dotnet.process.memory.working_set (By)                   64,331,776
    dotnet.thread_pool.queue.length ({work_item})                     0
    dotnet.thread_pool.thread.count ({thread})                        0
    dotnet.thread_pool.work_item.count ({work_item})                  7
    dotnet.timer.count ({timer})                                      0

Concentrarsi su questa riga:

    dotnet.gc.last_collection.memory.committed_size (By)   4,296,704

È 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 fino a oltre 20 MB.

    dotnet.gc.last_collection.memory.committed_size (By)   21,020,672

Osservando l'utilizzo della memoria, è possibile dire in modo sicuro che la memoria è in 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 su Windows o un core dump equivalente su 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 core dump 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

Annotazioni

Per un confronto nel tempo, lasciare che il processo originale continui a funzionare dopo la raccolta del 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 è stato completato l'esempio di destinazione di debug, ed è possibile chiuderla. Passare al terminale che ha avviato il server e premere CTRL+C.

Analizzare il core dump

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.

Annotazioni

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 i 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 dumpheap comando con la tabella del metodo (MT) per ottenere un elenco di tutte le String istanze:

> 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 comando gcroot su un'istanza System.String per vedere come e perché l'oggetto è radicato.

> 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 l'oggetto String è tenuto direttamente dall'oggetto Customer e indirettamente mantenuto da un CustomerCache oggetto .

È possibile continuare a eseguire il dump degli oggetti per verificare che la maggior parte String degli oggetti segua un modello simile. A questo punto, l'indagine ha fornito informazioni sufficienti per identificare la causa principale nel tuo 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 sarebbe dovuto essere stato arrestato come spiegato nella sezione Riavviare il processo non riuscito.

È anche possibile eliminare il file di dump creato.

Vedere anche

Passaggi successivi