Conteúdo

Profiling de memória é a arte de descobrir por que sua aplicação .NET está consumindo mais memória do que deveria. Diferente de fragmentação (memória livre mas inutilizável), um memory leak ocorre quando objetos permanecem vivos indefinidamente porque ainda existem referências para eles — mesmo que logicamente não sejam mais necessários. Este artigo apresenta um guia completo e prático para diagnosticar leaks usando as ferramentas oficiais da Microsoft: dotnet-dump para análise profunda e dotnet-gcdump para inspeções leves em produção.

Você aprenderá a capturar dumps de memória em diferentes cenários (Windows, Linux, containers), analisar o heap com comandos SOS como dumpheap, gcroot, dumpobj e objsize, identificar padrões comuns de leak (eventos não desinscritos, closures capturando referências, caches sem expiração, CancellationTokenSource não liberados), e implementar soluções definitivas. Todos os exemplos são 100% funcionais e podem ser copiados e testados localmente. Ao final, você terá domínio completo sobre diagnóstico de memória no .NET 10.

Insights

  1. Memory leak não é memória “perdida”, é memória “esquecida”: Objetos que deveriam morrer continuam vivos porque algo ainda os referencia. O GC funciona perfeitamente — ele só não coleta o que você acidentalmente mantém vivo.
  1. dotnet-gcdump é seu aliado leve em produção: Captura apenas o grafo de objetos do heap gerenciado usando EventPipe, sem pausar significativamente a aplicação. Ideal para comparações antes/depois e análise rápida de tendências.
  1. dotnet-dump é o bisturi cirúrgico: Captura um dump completo do processo (memória, threads, handles) permitindo análise profunda com comandos SOS. Mais pesado, mas revela absolutamente tudo.
  1. O comando gcroot é seu melhor amigo: Mostra a cadeia completa de referências que mantém um objeto vivo. Sem ele, você sabe o que está vazando mas não por que.
  1. *Dois dumps são melhor que um: Capture um dump logo após o startup* e outro quando a memória está alta. Compare os dois para identificar o que cresceu — isso elimina falsos positivos de objetos legítimos de longa vida.

O Que é um Memory Leak no .NET?

Um memory leak em .NET ocorre quando objetos permanecem no heap gerenciado indefinidamente porque ainda existem referências ativas para eles, mesmo que logicamente não sejam mais necessários para a aplicação.

Importante: o Garbage Collector (GC) do .NET funciona corretamente. Ele coleta todo objeto que não possui referências. O problema está no seu código — você está mantendo referências que não deveria.

flowchart TB
    subgraph SEM["Cenário sem leak"]
        R1["GC Root (stack, static)"] -->|referência| A1["Objeto A"]
        A1 -->|sai de escopo| X1["Referência removida"]
        X1 --> C1["GC coleta A — memória livre"]
    end
    subgraph COM["Cenário com leak"]
        R2["GC Root (evento static)"] -->|referência esquecida| A2["Objeto A"]
        A2 -->|deveria sair de escopo| X2["Referência persiste"]
        X2 --> C2["GC não coleta — memória cresce"]
    end

Diferença Entre Leak e Fragmentação

CaracterísticaMemory LeakFragmentação
CausaReferências mantidas indevidamenteBuracos entre objetos vivos
Heap cresce?Sim, continuamentePode não crescer
OOM ocorre porEsgotamento totalFalta de bloco contíguo
GC consegue ajudar?Não (objetos estão “vivos”)Parcialmente (compactação)
Ferramenta principalgcroot (quem referencia?)dumpheap -type Free

> Artigo anterior: Se você ainda não leu sobre fragmentação de memória, recomendo o artigo Fragmentação de Memória no .NET: Diagnóstico Profundo com WinDbg que complementa este material.

Ferramentas de Diagnóstico — Visão Geral

O ecossistema .NET oferece várias ferramentas para diagnóstico de memória. Este artigo foca nas duas principais ferramentas de linha de comando:

flowchart TB
    C["dotnet-counters — tempo real (detectar SE há problema)"]
    G["dotnet-gcdump — grafo de objetos via EventPipe (QUAIS tipos crescem)"]
    D["dotnet-dump — dump completo + SOS (POR QUE estão vivos, gcroot)"]
    C -->|problema detectado| G
    G -->|precisa de mais detalhes| D

Comparação — dotnet-dump vs dotnet-gcdump

Aspectodotnet-dumpdotnet-gcdump
Tamanho do arquivoGrande (centenas de MB a GB)Pequeno (alguns MB)
Overhead na capturaMédio-alto (pausa breve)Baixo
Informações capturadasTudo (memória, threads, handles)Apenas objetos do heap
Análise offlineSim, com analyzeSim, com Visual Studio/PerfView
Comando gcrootSimNão
Cross-platformSimSim
Ideal paraDiagnóstico profundoComparação rápida, produção

Instalação das Ferramentas

As ferramentas são distribuídas como global tools do .NET:

Atualização (se já instalado):

Aplicação de Exemplo — Simulando Memory Leaks

Vamos criar uma aplicação completa que simula os tipos mais comuns de memory leaks. Este código é 100% funcional — copie e execute para praticar o diagnóstico.

Estrutura do Projeto

Código Completo da Aplicação

Substitua o conteúdo de Program.cs:

Executando a Aplicação

Etapa 1 — Detectando o Problema com dotnet-counters

Antes de capturar dumps, confirme que há um problema de memória usando dotnet-counters.

Encontrar o PID do Processo

Saída esperada:

Monitorar Métricas em Tempo Real

Saída inicial (aplicação saudável):

Simular o Leak

Em outro terminal, cause o leak:

Saída do dotnet-counters APÓS os leaks:

Análise:

  • heap.size subiu de 15MB para 384MB
  • GC rodou várias vezes (gen2 foi de 1 para 12)
  • Memória NÃO foi liberada mesmo após GC forçado

Conclusão: há um memory leak. Hora de investigar com dumps.

Etapa 2 — Captura Rápida com dotnet-gcdump

O dotnet-gcdump é ideal para uma primeira análise porque é leve e rápido.

Capturar o GC Dump

Saída:

Gerar Relatório de Heap

Saída (ordenada por tamanho total):

Análise:

TipoContagemTamanhoProblema
EventSubscriber5.000250MBCada um tem 50KB de Payload
Customer10.000100MBCache sem expiração
System.Byte[]15.00060MBArrays dentro dos objetos acima

O dotnet-gcdump respondeu O QUE está vazando. Mas não mostra POR QUE. Para isso, precisamos do dotnet-dump.

Visualização no Visual Studio (Windows)

No Windows, você pode abrir o arquivo .gcdump diretamente no Visual Studio:

  1. File → Open → File
  2. Selecione o arquivo .gcdump
  3. Visual Studio mostra o Memory Analysis Report

O relatório visual inclui:

  • Gráfico de tipos por tamanho
  • Árvore de retenção (quem mantém quem)
  • Comparação entre dumps (diff)

Etapa 3 — Análise Profunda com dotnet-dump

O dotnet-dump captura um dump completo do processo, permitindo análise detalhada com comandos SOS.

Capturar o Dump Completo

Saída:

Iniciar a Análise Interativa

Prompt interativo:

Comandos SOS Essenciais para Diagnóstico de Memory Leak

O comando dumpheap -stat

Mostra estatísticas de todos os objetos no heap, agrupados por tipo.

Saída:

Interpretação da saída:

ColunaSignificado
MTMethod Table – identificador único do tipo
CountQuantidade de objetos desse tipo
TotalSizeTamanho total ocupado por todos os objetos desse tipo
Class NameNome completo do tipo

O comando dumpheap -mt <MethodTable>

Lista todos os objetos de um tipo específico.

Saída:

O comando dumpheap -type <TypeName>

Filtra por substring do nome do tipo (mais conveniente que -mt).

Saída:

Parâmetros Úteis do dumpheap

ParâmetroDescriçãoExemplo
-statApenas estatísticas, não lista objetosdumpheap -stat
-mt <addr>Filtra por Method Tabledumpheap -mt 00007f8a1c005940
-type <name>Filtra por nome do tipo (substring)dumpheap -type Customer
-min <bytes>Objetos maiores que N bytesdumpheap -min 85000
-max <bytes>Objetos menores que N bytesdumpheap -max 1000
-gen <0/1/2>Objetos em geração específicadumpheap -gen 2 -stat
-shortApenas endereços (para scripts)dumpheap -type Customer -short

O comando dumpobj <address>

Examina um objeto específico em detalhes.

Saída:

Interpretação:

CampoSignificado
NameNome do tipo
SizeTamanho em bytes
FieldsLista de campos com seus valores/endereços
ValuePara tipos referência, é o endereço do objeto referenciado

Para ver o valor do campo Name:

O comando gcroot – O Mais Importante

O gcroot mostra a cadeia completa de referências que mantém um objeto vivo. Este é o comando mais importante para diagnóstico de memory leak.

Saída:

Interpretação do resultado:

flowchart TB
    H["Strong Handle (GC Root)"] -->|referência| P["EventPublisher (Singleton)"]
    P -->|campo OnDataReceived| E["EventHandler"]
    E -->|invocationList| O["System.Object[]"]
    O -->|elemento| S["EventSubscriber — objeto vazando (50KB)"]

Conclusão do diagnóstico:

  1. EventPublisher é um Singleton (vive para sempre)
  2. O campo OnDataReceived (evento) contém um delegate
  3. O delegate referencia nosso EventSubscriber
  4. Solução: chamar Unsubscribe() quando o subscriber não for mais necessário

O comando objsize <address>

Calcula o tamanho total de um objeto incluindo todos os objetos que ele referencia (tamanho transitivo).

Saída:

Para ver a árvore completa:

O comando gchandles

Lista todos os GC Handles (referências que o GC não pode coletar).

Saída:

Tipos de handles:

TipoDescriçãoImpacto em GC
StrongReferência forte normalMantém objeto vivo
PinnedObjeto fixo na memóriaMantém vivo + impede movimento
WeakShortReferência fraca (curta)Não mantém vivo
WeakLongReferência fraca (longa)Não mantém vivo, sobrevive a finalização

O comando dumpasync

Lista todas as state machines de métodos async, útil para diagnosticar tasks pendentes.

Saída:

Estados comuns:

StateSignificado
-2Task criada mas não iniciada
-1Task executando
≥ 0Task em await no ponto N

O comando threadpool

Informações sobre o ThreadPool, útil para diagnosticar starvation.

Saída:

Análise: 500 Timers ativos indica um possível leak de System.Timers.Timer.

O comando timerinfo

Detalhes sobre todos os timers ativos.

Saída:

Análise: 500 timers com período de 3.600.000ms (1 hora) que nunca foram parados.

Diagnóstico Completo — Passo a Passo

Vamos fazer um diagnóstico completo do nosso aplicativo de exemplo.

Passo 1 — Identificar os Tipos Suspeitos

Passo 2 — Examinar Objetos de Cada Tipo

Passo 3 — Rastrear a Raiz de um Objeto

Diagnóstico: EventSubscriber está sendo referenciado pelo evento OnDataReceived do EventPublisher.

Passo 4 — Investigar o Publicador

Análise: _invocationCount = 5000 confirma que há 5.000 subscribers registrados no evento!

Passo 5 — Rastrear os Customers

Diagnóstico: Customer está no ConcurrentDictionary do LeakDemoService, que nunca remove itens.

Passo 6 — Verificar Timers

Diagnóstico: Os Timer estão vivos porque:

  1. TimerHolder referencia o Timer
  2. O Timer está na fila de timers do runtime (porque está rodando)

Soluções para Cada Tipo de Leak

Solução 1 — Event Handlers

Problema: subscriber adicionado a evento e nunca removido.

Alternativa: Weak Event Pattern

Solução 2 — Cache Sem Expiração

Problema: itens adicionados ao cache nunca expiram.

Alternativa: LRU Cache Manual

Solução 3 — Closures Capturando this

Problema: lambda captura this e Timer mantém referência.

Alternativa: Capturar apenas o necessário

Solução 4 — CancellationTokenSource

Problema: CTS criado mas nunca disposed.

Solução 5 — Timers

Problema: Timer criado e nunca parado/disposed.

Fluxo Completo de Diagnóstico

flowchart TB
    A["Suspeita de leak (memória crescendo)"] --> B["dotnet-counters — confirmar crescimento do heap"]
    B --> C{"Comportamento"}
    C -->|"Heap cresce, GC não ajuda"| LEAK["LEAK"]
    C -->|"Heap estável após GC"| NORMAL["Normal"]
    C -->|"Heap cresce + OOM com RAM livre"| FRAG["Fragmentação"]
    LEAK --> Q["dotnet-gcdump collect + report (QUAIS tipos)"]
    Q --> R["dotnet-dump analyze"]
    R --> R1["dumpheap -stat (O QUE vaza)"]
    R --> R2["gcroot address (POR QUE vivo)"]
    R --> R3["threadpool / timerinfo (timers ativos)"]
    R1 --> SOL["Implementar solução"]
    R2 --> SOL
    R3 --> SOL
    SOL --> FIX["handlers IDisposable+unsubscribe / caches expiração+limite / closures capturar valores / CTS using / timers Stop+Dispose"]

Comparação de Dumps — Técnica Diff

Uma técnica poderosa é comparar dois dumps capturados em momentos diferentes.

Capturar Dump “Antes” (Baseline)

Capturar Dump “Depois” (Com Leak)

Comparar no Visual Studio (Windows)

  1. Abra ambos os arquivos .gcdump no Visual Studio
  2. Na janela do segundo dump, clique em “Compare to baseline”
  3. Selecione o primeiro dump

O Visual Studio mostra:

  • Tipos que cresceram
  • Diferença de contagem e tamanho
  • Novos tipos que apareceram

Comparar via Script (Cross-Platform)

Diagnóstico em Containers e Kubernetes

Capturar Dump em Container Docker

Dockerfile com Ferramentas de Diagnóstico

Kubernetes — Capturar Dump de Pod

Sidecar para Diagnóstico Contínuo

Automação — Script de Diagnóstico

Script completo para automatizar a coleta e análise inicial:

Uso:

Tabela de Referência — Comandos SOS

ComandoDescriçãoUso Comum
dumpheap -statEstatísticas de heapIdentificar tipos que ocupam mais memória
dumpheap -mt <MT>Listar objetos por MethodTableVer instâncias de um tipo
dumpheap -type <name>Listar objetos por nomeFiltrar por substring do tipo
dumpheap -min <bytes>Objetos maiores que NEncontrar objetos grandes
dumpheap -gen <0/1/2>Objetos por geraçãoAnalisar objetos de longa vida (Gen2)
dumpobj <addr>Examinar objetoVer campos e valores
gcroot <addr>Cadeia de referênciasDescobrir por que objeto está vivo
objsize <addr>Tamanho transitivoTamanho total incluindo referências
gchandlesListar GC handlesEncontrar strong/pinned handles
threadpoolInfo do ThreadPoolVerificar timers ativos
timerinfoListar timersIdentificar timers não parados
dumpasyncState machines asyncAnalisar tasks pendentes
finalizequeueFila de finalizaçãoObjetos aguardando finalização
eeheap -gcEstrutura do heap GCSegmentos e gerações
clrstackStack da thread atualVer onde o código está
clrthreadsListar threadsVisão geral de threads
pe / printexceptionÚltima exceçãoDiagnóstico de crashes

FAQ – Perguntas Frequentes

1. Quando devo usar dotnet-dump vs dotnet-gcdump?

dotnet-gcdump é ideal para:

  • Diagnóstico inicial e rápido
  • Produção com baixo impacto
  • Comparação antes/depois
  • Identificar quais tipos estão crescendo

dotnet-dump é necessário quando:

  • Você precisa do comando gcroot para entender por que objetos estão vivos
  • Precisa analisar threads, stacks, handles
  • Diagnóstico profundo após identificar suspeitos com gcdump

Fluxo recomendado: comece com dotnet-gcdump para triagem, depois use dotnet-dump para investigação detalhada.

2. O dump captura o estado exato ou pode ter inconsistências?

O dotnet-dump captura um snapshot consistente do processo. Durante a captura:

  • O processo é brevemente pausado (fração de segundo)
  • Todas as threads são suspensas
  • O estado é capturado atomicamente

O dotnet-gcdump usa EventPipe e pode ter pequenas inconsistências porque não pausa completamente o processo, mas é suficientemente preciso para diagnóstico de leaks.

3. Como diferenciar um leak real de uso legítimo de memória?

Técnica do Diff: capture dois dumps em momentos diferentes e compare:

  • Se os mesmos tipos crescem proporcionalmente ao uso → provavelmente legítimo
  • Se tipos crescem mesmo sem uso correspondente → provavelmente leak

Análise de gcroot: se o objeto está referenciado por:

  • Caches, pools, ou estruturas de negócio → pode ser legítimo
  • Eventos estáticos, delegates órfãos, timers esquecidos → provavelmente leak

Forçar GC: execute GC.Collect() e observe se a memória diminui:

  • Diminuiu significativamente → não era leak, eram objetos elegíveis
  • Não diminuiu → leak confirmado

4. Posso analisar dumps de Linux no Windows (e vice-versa)?

dotnet-dump: Sim, dumps são portáveis entre plataformas desde que você use a mesma versão do .NET (ou compatível).

dotnet-gcdump: Arquivos .gcdump podem ser gerados em qualquer plataforma, mas a análise visual (Visual Studio, PerfView) está disponível apenas no Windows.

5. Como automatizar a detecção de leaks em CI/CD?

Compartilhe:

Tiago Tartari

Tiago Tartari

Eu ajudo e capacito pessoas e organizações a transformar problemas complexos em soluções práticas usando a tecnologia para atingir resultados extraordinários.

Qual é o desafio
que você tem hoje?