Depurar uma perda de memória no .NET
Este artigo se aplica a: ✔️ SDK do .NET Core 3.1 e versões posteriores
Pode haver perda de memória quando o aplicativo faz referência a objetos de que não precisa mais para executar a tarefa desejada. Ao fazer referência a esses objetos, impede-se 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 uma perda 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 a perda 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 de maneira não intencional.
Neste tutorial, você irá:
- Examinar o uso de memória gerenciada com dotnet-counters.
- Gerar um arquivo de despejo.
- Analisar o uso de memória usando o arquivo de despejo.
Pré-requisitos
O tutorial usa:
- O SDK do .NET Core 3.1 ou uma versão posterior.
- O dotnet-counters para verificar o uso de memória gerenciada.
- dotnet-dump para coletar e analisar um arquivo de despejo (inclui a extensão de depuração de SOS).
- Um aplicativo de destino de depuração de exemplo a ser diagnosticado.
O tutorial pressupõe que os aplicativos de exemplo e as ferramentas estejam instalados e prontos para uso.
Examinar o uso de memória gerenciada
Para começar a coletar dados de diagnóstico e ajudar a determinar a causa raiz desse cenário, é necessário que realmente esteja ocorrendo uma perda 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
Agora, verifique o uso de memória gerenciada com a ferramenta dotnet-counters. O --refresh-interval
especifica 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 esta:
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
Concentrando-se nesta linha:
GC Heap Size (MB) 4
Veja que a memória de heap gerenciada é de 4 MB logo após a inicialização.
Agora, acesse a URL https://localhost:5001/api/diagscenario/memleak/20000
.
Observe que o uso de memória cresceu para 30 MB.
GC Heap Size (MB) 30
Observando o uso de memória, você pode dizer com segurança se há crescimento ou perda de memória. A próxima etapa é coletar os dados corretos para análise de memória.
Gerar despejo de memória
Ao analisar possíveis perdas de memória, você precisa de acesso ao heap de memória do aplicativo para determinar o conteúdo da memória. Examinando as relações entre os objetos, é possível criar teorias do porquê a memória não está sendo liberada. Uma fonte de dados de diagnóstico comum é um despejo de memória no Windows ou o despejo de núcleo 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 fazer uma comparação ao longo do tempo, deixe o processo original continuar em execução depois de coletar o primeiro despejo e colete 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 será 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. Acesse o terminal que iniciou o servidor e pressione Ctrl+C.
Analisar o despejo de núcleo
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 aparecer um erro informando que libdl.so não pode ser encontrado, talvez seja necessário instalar o pacote libc6-dev. Para obter mais informações, confira Pré-requisitos para o .NET no Linux.
Será exibido um prompt no qual você 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 comando dumpheap
novamente com a MT (tabela de métodos) para obter uma lista de todas as instâncias de 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
Agora você pode usar o comando gcroot
em uma instância de System.String
para ver como e por que o objeto está com 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.
Veja que o objeto String
é mantido diretamente pelo objeto Customer
e indiretamente por um objeto CustomerCache
.
Você pode continuar o despejo de objetos para ver que a maioria dos objetos String
segue um padrão semelhante. Neste ponto, a investigação forneceu informações suficientes para identificar a causa raiz no código.
Esse procedimento geral permite identificar a origem das principais perdas de memória.
Limpar os recursos
Neste tutorial, você iniciou um servidor Web de exemplo. Esse servidor deve ter sido desligado conforme a explicação na seção Reiniciar o processo com falha.
Você também pode excluir o arquivo de despejo que foi criado.
Confira também
- dotnet-trace para listar processos
- dotnet-counters para verificar o uso de memória gerenciada
- dotnet-dump para coletar e analisar um arquivo de despejo
- dotnet/diagnostics
- Usar o Visual Studio para depurar perdas de memória