Compartilhar via


Depurar um vazamento de memória no .NET

Este artigo se aplica a: ✔️ SDK do .NET Core 3.1 e versões posteriores

A memória pode vazar quando seu aplicativo faz referência a objetos que ele não precisa mais executar a tarefa desejada. Fazer referência a esses objetos impede que o coletor de lixo recupere a memória usada. Isso pode resultar na degradação do desempenho e na geração de uma exceção OutOfMemoryException .

Este tutorial demonstra as ferramentas para analisar um vazamento de memória em um aplicativo .NET usando as ferramentas da CLI de diagnóstico do .NET. Se você estiver no Windows, poderá usar as ferramentas de Diagnóstico de Memória do Visual Studio para depurar o vazamento de memória.

Este tutorial usa um aplicativo de exemplo que intencionalmente vaza memória como um exercício. Você também pode analisar aplicativos que vazam memória sem querer.

Neste tutorial, você irá:

  • Examine o uso de memória gerenciada com dotnet-counters.
  • Gerar um arquivo de despejo.
  • Analise o uso da memória usando o arquivo de despejo.

Pré-requisitos

O tutorial usa:

O tutorial pressupõe que os aplicativos e as ferramentas de exemplo estão instalados e prontos para uso.

Se o aplicativo estiver executando uma versão do .NET mais antiga que o .NET 9, a interface do usuário de saída de contadores dotnet terá uma aparência ligeiramente diferente; consulte dotnet-counters para obter detalhes.

Examinar o uso de memória gerenciada

Antes de começar a coletar dados de diagnóstico para ajudar a identificar a causa desse cenário, verifique se você está realmente observando um vazamento de memória (aumento no uso da memória). Você pode usar a ferramenta dotnet-counters para confirmar isso.

Abra uma janela do console e navegue até o diretório em que baixou e descompactou o destino de depuração de exemplo. Execute o destino:

dotnet run

Em um console separado, localize a ID do processo:

dotnet-counters ps

A saída deverá ser semelhante a:

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

Observação

Se o comando anterior não funcionar ou não for encontrado, você provavelmente precisará instalar a dotnet-counters ferramenta primeiro. Use o seguinte comando:

dotnet tool install --global dotnet-counters

Agora, verifique o uso da memória gerenciada com a ferramenta dotnet-counters . Especifica --refresh-interval o número de segundos entre as atualizações:

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

A saída ao vivo deve ser semelhante a:

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

Focando nessa linha:

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

Você pode ver que a memória de heap gerenciada é de 4 MB logo após a inicialização.

Agora, vá para a URL https://localhost:5001/api/diagscenario/memleak/20000.

Observe que o uso de memória cresceu para mais de 20 MB.

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

Observando o uso da memória, você pode dizer com segurança que a memória está crescendo ou vazando. A próxima etapa é coletar os dados certos para análise de memória.

Gerar despejo de memória

Ao analisar possíveis vazamentos de memória, você precisa de acesso ao heap de memória do aplicativo para analisar o conteúdo da memória. Examinando as relações entre objetos, você cria teorias sobre por que a memória não está sendo liberada. Uma fonte comum de dados de diagnóstico é um despejo de memória no Windows ou um core dump equivalente no Linux. Para gerar um despejo de um aplicativo .NET, você pode usar a ferramenta dotnet-dump .

Usando o destino de depuração de exemplo já iniciado, execute o seguinte comando para gerar um despejo de núcleo do Linux:

dotnet-dump collect -p 4807

O resultado é um despejo de núcleo localizado na mesma pasta.

Writing minidump with heap to ./core_20190430_185145
Complete

Observação

Para uma comparação ao longo do tempo, permita que o processo original continue em execução depois de coletar o primeiro despejo e coletar um segundo despejo da mesma maneira. Assim você terá dois despejos ao longo de um tempo que poderá comparar para ver o local em que o uso de memória está aumentando.

Reiniciar o processo com falha

Depois que o despejo for coletado, haverá informações suficientes para diagnosticar o processo com falha. Se o processo com falha estiver em execução em um servidor de produção, agora é o momento ideal para correção de curto prazo reiniciando o processo.

Neste tutorial, o uso do destino de depuração de exemplo já terminou e você pode fechá-lo. Navegue até o terminal que iniciou o servidor e pressione Ctrl+C.

Analisar o despejo principal

Agora que um despejo de núcleo foi gerado, use a ferramenta dotnet-dump para analisá-lo:

dotnet-dump analyze core_20190430_185145

Em que core_20190430_185145 é o nome do despejo de núcleo que você quer analisar.

Observação

Se você vir um erro reclamando que libdl.so não pode ser encontrado, talvez seja necessário instalar o pacote libc6-dev . Para obter mais informações, consulte Pré-requisitos para .NET no Linux.

Você receberá um prompt em que poderá inserir comandos SOS . Normalmente, a primeira coisa a ser examinada é o estado geral do heap gerenciado:

> 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

Aqui você pode ver que a maioria dos objetos são String ou Customer.

Você pode usar o dumpheap comando novamente com a tabela de métodos (MT) para obter uma lista de todas as String instâncias:

> 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

Agora você pode usar o gcroot comando em uma System.String instância para ver como e por que o objeto está enraizado:

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

Você pode ver que o String objeto é mantido diretamente pelo Customer objeto e indiretamente mantido por um CustomerCache objeto.

Você pode continuar despejando objetos para ver que a maioria dos String objetos segue um padrão semelhante. Neste ponto, a investigação forneceu informações suficientes para identificar a causa raiz em seu código.

Este procedimento geral permite identificar a origem de grandes vazamentos de memória.

Limpar os recursos

Neste tutorial, você iniciou um servidor Web de exemplo. Esse servidor deveria ter sido desligado, conforme explicado na seção Reiniciar o processo com falha .

Você também pode excluir o arquivo de despejo que foi criado.

Consulte também

Próximas etapas