Conteúdo

Fragmentação de memória no .NET ocorre quando o heap gerenciado contém espaços livres intercalados com objetos vivos, impedindo alocações contíguas mesmo com memória total disponível. O problema é especialmente severo no Large Object Heap (LOH), que não passa por compactação automática, e em cenários com muitos objetos pinned que impedem o Garbage Collector de reorganizar a memória.

Insights

  1. Fragmentação não é vazamento de memória: Sua aplicação pode ter memória suficiente e ainda falhar com OutOfMemoryException porque não existe um bloco contíguo grande o suficiente para a alocação solicitada.
  2. Large Object Heap é o principal vilão: Objetos maiores que 85.000 bytes vão para o LOH, que não é compactado por padrão. Alocações e liberações frequentes de objetos grandes criam buracos de memória inutilizável.
  3. Objetos pinned impedem a compactação: Quando você fixa um objeto na memória (para interop com código nativo), o GC não pode movê-lo. Se muitos objetos ficam pinned em locais dispersos, a heap se fragmenta mesmo no Small Object Heap.
  4. A fragmentação da Geração 2 é a mais crítica: Fragmentação na Gen0 é inofensiva porque o GC pode alocar nos espaços livres. Na Gen2, os buracos tendem a persistir e crescer.
  5. WinDbg com SOS revela o que ferramentas de APM escondem: Comandos como !dumpheap -type Free e !eeheap -gc mostram exatamente onde está a fragmentação, quanto espaço está desperdiçado e qual objeto está causando o problema.

Este artigo explora em profundidade os mecanismos que causam fragmentação, como identificá-la usando WinDbg e a extensão SOS, e técnicas para resolver ou mitigar o problema. Você aprenderá a interpretar a saída de comandos como !dumpheap!eeheap!gchandles e !heapstat, calcular o percentual de fragmentação, e implementar soluções como pooling de objetos, uso do Pinned Object Heap (POH) do .NET 5+, e compactação manual do LOH.

O que é fragmentação de memória?

Fragmentação de memória é a condição em que o heap gerenciado contém espaços livres dispersos entre objetos vivos, resultando em incapacidade de alocar novos objetos mesmo quando a soma total de memória livre seria suficiente.

Considere o seguinte cenário visual:

HEAP SEM FRAGMENTAÇÃO
Fragmentação de Memória no .NET: Diagnóstico Profundo com WinDbg
Alocação de 400KB: SUCESSO (bloco contíguo de 450KB disponível)
HEAP COM FRAGMENTAÇÃO
Fragmentação de Memória no .NET: Diagnóstico Profundo com WinDbg
Total livre: 350KB
  Maior bloco contíguo: 150KB
  Alocação de 200KB: FALHA (OutOfMemoryException)

O segundo cenário ilustra o problema: **350KB livres, mas nenhum bloco individual maior que 150KB**. Uma alocação de 200KB falhará com OutOfMemoryException, mesmo com memória disponível.

Arquitetura de memória do .NET: Fundamentos

Para entender fragmentação, é essencial conhecer como o .NET organiza a memória.

Managed Heap

runtime do .NET mantém um managed heap onde todos os objetos de referência são alocados. Este heap é dividido em:

Fragmentação de Memória no .NET: Diagnóstico Profundo com WinDbg - Managed Heap do .NET

Threshold de 85.000 Bytes

Por que 85.000 bytes? Este valor foi determinado por performance tuning pela equipe do CLR da Microsoft. A lógica é simples: mover objetos grandes é caro. O custo de copiar um objeto de 1MB durante a compactação supera os benefícios de manter o heap organizado.

O número exato é 85.000 bytes, não 85KB (que seria 87.040 bytes). Este detalhe importa quando você está próximo do limite.

Compactação é a defesa contra fragmentação

Garbage Collector possui uma fase de compactação que move objetos vivos para ficarem contíguos, eliminando buracos:

O problema: o LOH não é compactado por padrão. No SOH, a compactação mantém a fragmentação sob controle. No LOH, os buracos persistem.

As três causas de fragmentação

1. Large Object Heap (LOH) sem Compactação

O LOH é a causa mais comum de fragmentação severa. Como não há compactação automática, o padrão de alocação/liberação cria buracos permanentes.

Cenário problemático: uma aplicação que processa arquivos grandes temporariamente.

Visualização do problema:

2. Objetos pinned dispersos

Quando você “fixa” (pin) um objeto na memória, o GC não pode movê-lo durante a compactação. Isso é necessário para interop com código nativo, I/O assíncrono, e outras operações que dependem de endereços fixos.

Visualização do problema de pinning:

3. Sobrevivência Excessiva para Geração 2

Objetos que sobrevivem múltiplas coletas são promovidos para Gen2. Como Gen2 é coletada com menos frequência, a fragmentação tende a se acumular.

Diagnóstico com WinDbg e SOS

WinDbg com a extensão SOS (Son of Strike) é a ferramenta definitiva para diagnóstico de fragmentação. Vamos explorar os comandos essenciais.

Configuração Inicial

1. Obtenha um dump de memória:

2. Abra o dump no WinDbg:

3. Carregue a extensão SOS:

Comando: !eeheap -gc

Este comando mostra a estrutura completa do heap gerenciado, incluindo cada segmento e geração.

Interpretação:

CampoSignificado
segmentEndereço base do segmento de memória
beginInício da área utilizável
allocatedAté onde objetos foram alocados
committedMemória committed no sistema operacional
allocated sizeTamanho efetivamente usado por objetos
committed sizeTamanho total reservado

Cálculo de fragmentação inicial:

Comando: !dumpheap -type Free -stat

Este é o comando mais importante para medir fragmentação. Ele lista todos os blocos livres no heap.

Interpretação:

  • Count: 12.847 = Existem 12.847 blocos livres (buracos) no heap
  • TotalSize: 23.567.840 = 23,5MB de memória fragmentada

Cálculo do percentual de fragmentação:

Comando: !dumpheap -type Free (detalhado)

Sem o -stat, você vê cada bloco livre individualmente:

Análise importante: observe o tamanho dos blocos livres. Muitos blocos pequenos indicam fragmentação severa. O endereço revela se está no SOH ou LOH (compare com a saída do !eeheap -gc).

Comando: !heapstat

Fornece estatísticas consolidadas por geração:

Interpretação crítica:

GeraçãoEspaço LivrePercentual
SOH (Gen0+Gen1+Gen2)8.267.040 bytes4,4%
LOH15.234.816 bytes22,2%

A regra dos 20%: fragmentação acima de 20% na Gen2 ou LOH indica problema sério que requer investigação e ação.

Comando: !gchandles

Lista todos os GCHandles, incluindo objetos pinned:

Alerta847 Pinned Handles é um número alto. Cada objeto pinned é um ponto potencial de fragmentação.

Comando: !gcroot <address>

Encontra o que está mantendo um objeto vivo (útil para entender por que um bloco livre está “preso” entre objetos):

Interpretação: o objeto está sendo mantido vivo por um handle pinned e por uma referência em uma thread. Investigar quem criou o handle é o próximo passo.


Análise Avançada: Correlacionando Segmentos

Para uma análise completa, correlacione os blocos livres com os segmentos identificados no !eeheap -gc:

Diagnóstico: 127 blocos livres totalizando 15MB no LOH. O maior bloco é de apenas 1MB. Qualquer alocação maior que 1MB falhará, mesmo com 15MB livres.


Comando: !address -summary

Para analisar fragmentação de memória virtual (além do heap gerenciado):

Interpretação:

EstadoSignificado
MEM_FREEMemória virtual não reservada (disponível para uso)
MEM_RESERVEMemória reservada mas não committed
MEM_COMMITMemória efetivamente alocada e em uso

Fragmentação de VM: se MEM_FREE está dispersa em muitos pequenos blocos, a aplicação pode ter problemas mesmo com memória física disponível.

Diagnóstico com outras ferramentas

dotnet-counters (Monitoramento em Tempo Real)

A métrica GC Fragmentation (%) indica diretamente o percentual de fragmentação atual.

Para um guia completo sobre métricas de runtime do .NET, incluindo Garbage CollectorThread Pool, exceções e contenção de locks, confira o artigo Como Analisar e Interpretar as Métricas de Runtime do .NET. Lá você encontra o contexto completo de todas as métricas expostas pelo runtime e como integrá-las com OpenTelemetry e ferramentas de APM.

GC.GetGCMemoryInfo()

Saída exemplo:

PerfView (Análise de Pinning)

PerfView oferece a visão “Pinning At GC Time Stacks” que mostra exatamente quais objetos estão pinned e qual código os fixou:

  1. Baixe o PerfView: https://github.com/microsoft/perfview/releases
  2. Execute:perfview /GCCollectOnly /AcceptEULA /nogui collect
  3. Reproduza o cenário problemático
  4. Pare a coleta
  5. Abra o arquivo .etl.zip no PerfView
  6. Navegue para: Advanced → Pinning At GC Time Stacks

Soluções e mitigações

1. Use ArrayPool para Objetos Grandes Temporários

Em vez de alocar arrays grandes diretamente, use o pool compartilhado:

Por que funciona: o ArrayPool reutiliza os mesmos buffers, evitando novas alocações no LOH.

2. Use o Pinned Object Heap

Para buffers que precisam ser pinned, use a alocação direta no POH:

Restrição: apenas arrays de tipos blittable (sem referências) podem ser alocados no POH.

3. Compactação manual do LOH

Em cenários específicos, você pode forçar a compactação do LOH:

Atenção: compactação do LOH é cara. A documentação da Microsoft adverte:

“LOH compaction can be an expensive operation and should only be used after significant performance analysis.”

Use apenas quando:

  • A fragmentação está causando OutOfMemoryException
  • A aplicação está em um período de baixa atividade
  • Você mediu e confirmou o benefício

4. Divida objetos grandes

Se possível, evite criar objetos maiores que 85.000 bytes:

5. Monitore e alerte

Configure alertas baseados nas métricas de fragmentação:

Diagnóstico passo a passo

Vamos simular um diagnóstico completo de fragmentação:

Sintoma: aplicação web lançando OutOfMemoryException esporádicos, mas o servidor mostra 8GB de RAM livre.

Passo 1: Capture um dump durante o problema

Passo 2: Abra no WinDbg e carregue SOS

Passo 3: Verifique o tamanho geral do heap

Passo 4: Analise a fragmentação

734MB livres em 45.234 blocos = fragmentação severa!

Passo 5: Calcule o percentual

34,2% de fragmentação – muito acima do limite aceitável de 20%.

Passo 6: Identifique onde está a fragmentação

LOH com 140% de fragmentação! (espaço livre maior que objetos vivos – indicador de fragmentação extrema com muitas alocações/liberações).

Passo 7: Veja os maiores blocos livres no LOH

Maior bloco livre: 16MB. Qualquer alocação > 16MB falhará.

Passo 8: Identifique o que está causando

System.Byte[]: 1.234 objetos ocupando 412MB no LOH. Este é o candidato.

Passo 9: Encontre quem está alocando

Diagnóstico finalImageProcessor.ResizeImage está alocando arrays grandes repetidamente sem usar pooling.

Solução: refatorar para usar ArrayPool<byte>.Shared.

Tabela de Referência Rápida: Comandos WinDbg

ComandoPropósito
!eeheap -gcEstrutura completa do heap gerenciado
!dumpheap -statEstatísticas de objetos por tipo
!dumpheap -type Free -statTotal de fragmentação
!dumpheap -type FreeLista de todos os blocos livres
!heapstatEstatísticas por geração
!gchandlesLista de GCHandles (pinnedstrong, etc.)
!gcroot <addr>Encontra referências mantendo objeto vivo
!objsize <addr>Tamanho total de um objeto (incluindo filhos)
!verifyheapVerifica corrupção no heap
!address -summaryUso de memória virtual

Diagrama para o fluxo de diagnóstico

FAQ: Perguntas Frequentes

1. Qual a diferença entre fragmentação de memória e vazamento de memória (memory leak)?

Vazamento de memória ocorre quando objetos permanecem vivos indefinidamente porque ainda têm referências, mesmo que não sejam mais necessários. O heap cresce continuamente.

Fragmentação de memória ocorre quando objetos morrem e são coletados, mas os espaços livres resultantes ficam dispersos, impedindo alocações contíguas maiores. O heap pode até diminuir, mas a memória útil diminui proporcionalmente mais.

Um vazamento eventualmente causa OutOfMemoryException por esgotamento total. A fragmentação causa OutOfMemoryException mesmo com memória livre disponível.

2. Por que o .NET não compacta o LOH automaticamente?

Custo de performance. Mover um objeto de 10MB requer copiar 10MB de dados e atualizar todas as referências que apontam para ele. Para objetos grandes, este custo supera os benefícios da compactação.

A decisão de não compactar o LOH foi baseada em performance tuning pela equipe do CLR. Na prática, a maioria das aplicações não aloca objetos grandes temporários com frequência suficiente para causar fragmentação severa.

3. O Pinned Object Heap (POH) resolve todos os problemas de pinning?

Não completamente. O POH resolve o problema de fragmentação no SOH causada por objetos pinnedO POH em si pode fragmentar, mas como é um heap separado, não afeta o SOH.

Além disso, o POH tem restrições:

  • Apenas arrays de tipos blittable
  • Disponível apenas no .NET 5+
  • Objetos no POH são coletados junto com Gen2 (menos frequente)

Para pinning de curta duração, ainda é melhor pin no SOH do que alocar no POH.

4. Como sei se devo usar compactação manual do LOH?

Use compactação manual do LOH quando:

  1. Fragmentação medida > 30% usando !dumpheap -type Free -stat
  2. Maior bloco livre < tamanho necessário para próximas alocações
  3. A aplicação está em período de baixa carga (a compactação é bloqueante)
  4. Você esgotou outras opções (pooling, divisão de objetos)

Evite usar regularmente em intervalos fixos – isso pode desestabilizar as heurísticas do GC.

5. Qual ferramenta usar: WinDbg, dotMemory, ou dotnet-counters?

CenárioFerramenta Recomendada
Monitoramento contínuo em produçãodotnet-counters ou métricas via OpenTelemetry
Diagnóstico profundo de dumpWinDbg + SOS
Análise visual interativadotMemory (JetBrains)
Investigação de pinningPerfView
Desenvolvimento/debugging localVisual Studio Diagnostic Tools

O WinDbg é a ferramenta definitiva para diagnóstico profundo porque oferece acesso direto à memória do processo, sem abstrações.

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?