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
- 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.
- O 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.
- 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.
- 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.
- WinDbg com SOS revela o que ferramentas de APM escondem: Comandos como
!dumpheap -type Freee!eeheap -gcmostram 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 |
|---|
![]() |
| Alocação de 400KB: SUCESSO (bloco contíguo de 450KB disponível) |
| HEAP COM FRAGMENTAÇÃO |
|---|
![]() |
| 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.
O Managed Heap
O runtime do .NET mantém um managed heap onde todos os objetos de referência são alocados. Este heap é dividido em:

O 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.
|
1 2 3 4 5 |
// Este array vai para o SOH (Small Object Heap) var small = new byte[84_999]; // 84.999 bytes < 85.000 // Este array vai para o LOH (Large Object Heap) var large = new byte[85_000]; // 85.000 bytes >= 85.000 |
Compactação é a defesa contra fragmentação
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<em>// CÓDIGO QUE CAUSA FRAGMENTAÇÃO NO LOH</em> public void ProcessarArquivos(string[] caminhos) { foreach (var caminho in caminhos) { <em>// Cada iteração aloca um array grande no LOH</em> byte[] buffer = File.ReadAllBytes(caminho); <em>// Pode ser > 85KB</em> ProcessarBuffer(buffer); <em>// buffer sai de escopo, mas o "buraco" no LOH permanece</em> <em>// até ser reutilizado por uma alocação de tamanho similar</em> } } |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<em>// Exemplo de pinning que causa fragmentação</em> public unsafe void ProcessarComPinning() { byte[] buffer1 = new byte[1024]; byte[] buffer2 = new byte[1024]; byte[] buffer3 = new byte[1024]; <em>// Fixa buffer2 no meio</em> GCHandle handle = GCHandle.Alloc(buffer2, GCHandleType.Pinned); try { IntPtr ptr = handle.AddrOfPinnedObject(); <em>// Usar ptr com código nativo...</em> <em>// Se buffer1 e buffer3 morrerem, o espaço não pode ser consolidado</em> <em>// porque buffer2 está "preso" no meio</em> } finally { handle.Free(); } } |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<em>// Padrão que causa fragmentação na Gen2</em> public class CacheProblematico { private readonly Dictionary<string, byte[]> _cache = new(); public void Processar(string chave, byte[] dados) { <em>// Objetos ficam no cache por tempo indeterminado</em> _cache[chave] = dados; <em>// Eventualmente são removidos</em> if (_cache.Count > 1000) { var chavesAntigas = _cache.Keys.Take(100).ToList(); foreach (var k in chavesAntigas) { _cache.Remove(k); <em>// Cria buracos na Gen2</em> } } } } |
Diagnóstico com WinDbg e SOS
O 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:
|
1 2 3 4 5 6 7 8 |
<em># Usando dotnet-dump (recomendado para .NET Core/.NET 5+)</em> dotnet-dump collect -p <PID> <em># Usando procdump (Windows)</em> procdump -ma <PID> dump.dmp <em># Usando Task Manager (Windows)</em> <em># Botão direito no processo → Create dump file</em> |
2. Abra o dump no WinDbg:
|
1 |
File → Open Crash Dump → selecione o arquivo .dmp |
3. Carregue a extensão SOS:
|
1 2 3 4 5 6 7 8 |
# Para .NET Framework .loadby sos clr # Para .NET Core/.NET 5+ .loadby sos coreclr # Alternativa: carregar automaticamente .load C:\path\to\sos.dll |
Comando: !eeheap -gc
Este comando mostra a estrutura completa do heap gerenciado, incluindo cada segmento e geração.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
0:000> !eeheap -gc Number of GC Heaps: 1 generation 0 starts at 0x00007ff9a0c01030 generation 1 starts at 0x00007ff9a0c01018 generation 2 starts at 0x00007ff9a0c01000 segment begin allocated committed allocated size committed size generation 0: 00007ff9a0c00000 00007ff9a0c01000 00007ff9a1400fe8 00007ff9a1401000 0x7ffefe8(134213608) 0x800000(8388608) generation 1: generation 2: Large object heap: 00007ff9b0c00000 00007ff9b0c01000 00007ff9b4d89e90 00007ff9b4d8a000 0x4188e90(68587152) 0x4189000(68640768) Pinned object heap: 00007ff9c0c00000 00007ff9c0c01000 00007ff9c0c85a20 00007ff9c0c86000 0x84a20(543264) 0x85000(544768) GC Allocated Heap Size: Size: 0xc417098 (205843608) bytes. GC Committed Heap Size: Size: 0xcc0e000 (213901312) bytes. |
Interpretação:
| Campo | Significado |
|---|---|
segment | Endereço base do segmento de memória |
begin | Início da área utilizável |
allocated | Até onde objetos foram alocados |
committed | Memória committed no sistema operacional |
allocated size | Tamanho efetivamente usado por objetos |
committed size | Tamanho total reservado |
Cálculo de fragmentação inicial:
|
1 2 |
Committed - Allocated = Overhead/Fragmentação potencial 213.901.312 - 205.843.608 = 8.057.704 bytes (3,7%) |
Comando: !dumpheap -type Free -stat
Este é o comando mais importante para medir fragmentação. Ele lista todos os blocos livres no heap.
|
1 2 3 4 5 6 |
0:000> !dumpheap -type Free -stat Statistics: MT Count TotalSize Class Name 00007ff9e0c05540 12847 23567840 Free Total 12847 objects, 23567840 bytes |
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:
|
1 2 3 |
Fragmentação % = (Free TotalSize / GC Allocated Heap Size) × 100 Fragmentação % = (23.567.840 / 205.843.608) × 100 Fragmentação % = 11,4% |
Comando: !dumpheap -type Free (detalhado)
Sem o -stat, você vê cada bloco livre individualmente:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
0:000> !dumpheap -type Free Address MT Size 00007ff9a0c15a80 00007ff9e0c05540 24 00007ff9a0c16b90 00007ff9e0c05540 120 00007ff9a0c18c40 00007ff9e0c05540 4096 00007ff9a0c19c40 00007ff9e0c05540 32768 00007ff9b0c89a00 00007ff9e0c05540 524288 ← Bloco grande no LOH ... Statistics: MT Count TotalSize Class Name 00007ff9e0c05540 12847 23567840 Free Total 12847 objects, 23567840 bytes |
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:
|
1 2 3 4 5 6 7 8 9 10 |
0:000> !heapstat Heap Gen0 Gen1 Gen2 LOH POH Heap0 2097152 4194304 127821848 68587152 543264 Free space: Percentage Heap0 12032 20480 8234528 15234816 32000 SOH: 8267040 bytes (4.4%) LOH: 15234816 bytes (22.2%) Total Allocated: 205.843.608 bytes |
Interpretação crítica:
| Geração | Espaço Livre | Percentual |
|---|---|---|
| SOH (Gen0+Gen1+Gen2) | 8.267.040 bytes | 4,4% |
| LOH | 15.234.816 bytes | 22,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:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
0:000> !gchandles Handle Type Object Size Data Type 00007ff9d0001000 Strong 00007ff9a0c21000 120 System.Object[] 00007ff9d0001008 Pinned 00007ff9b0c45000 85000 System.Byte[] 00007ff9d0001010 Pinned 00007ff9b0c9a000 131072 System.Byte[] 00007ff9d0001018 Pinned 00007ff9b0d1b000 65536 System.Byte[] ... Statistics: Strong Handles: 1542 Pinned Handles: 847 Weak Long Handles: 234 Weak Short Handles: 89 |
Alerta: 847 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):
|
1 2 3 4 5 6 7 8 9 10 11 |
0:000> !gcroot 00007ff9b0c45000 Thread 1: RSP:00007ff9f0e8f900 → 00007ff9a0c21000 (System.Object[]) → 00007ff9b0c45000 (System.Byte[]) HandleTable: 00007ff9d0001008 (pinned handle) → 00007ff9b0c45000 (System.Byte[]) Found 2 unique roots. |
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:
|
1 2 3 4 5 |
0:000> !eeheap -gc ... Large object heap: 00007ff9b0c00000 00007ff9b0c01000 00007ff9b4d89e90 00007ff9b4d8a000 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
0:000> !dumpheap -type Free 00007ff9b0c01000 00007ff9b4d89e90 Address MT Size 00007ff9b0c89a00 00007ff9e0c05540 524288 00007ff9b0d45000 00007ff9e0c05540 262144 00007ff9b0e21800 00007ff9e0c05540 131072 00007ff9b1234000 00007ff9e0c05540 1048576 ← 1MB livre no meio do LOH! 00007ff9b2345600 00007ff9e0c05540 524288 ... Statistics: MT Count TotalSize Class Name 00007ff9e0c05540 127 15234816 Free Total 127 objects, 15234816 bytes |
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):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
0:000> !address -summary --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal Free 1847 7f9`8a2c0000 ( 127.598 TB) 99.69% <unknown> 2134 0`23580000 ( 565.500 MB) 72.82% 0.00% Image 1256 0`0a8c4000 ( 168.766 MB) 21.72% 0.00% Heap 234 0`02340000 ( 35.250 MB) 4.54% 0.00% Stack 24 0`00700000 ( 7.000 MB) 0.90% 0.00% ... --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal MEM_FREE 1847 7f9`8a2c0000 ( 127.598 TB) 99.69% MEM_RESERVE 312 0`12480000 ( 292.500 MB) 37.66% 0.00% MEM_COMMIT 3089 0`1d200000 ( 466.000 MB) 60.00% 0.00% |
Interpretação:
| Estado | Significado |
|---|---|
MEM_FREE | Memória virtual não reservada (disponível para uso) |
MEM_RESERVE | Memória reservada mas não committed |
MEM_COMMIT | Memó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)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
dotnet-counters monitor -p <PID> --counters System.Runtime Press p to pause, r to resume, q to quit. Status: Running [System.Runtime] GC Heap Size (MB) 205 GC Fragmentation (%) 11.4 Gen 0 GC Count 1234 Gen 1 GC Count 456 Gen 2 GC Count 78 LOH Size (MB) 68 POH Size (MB) 0.5 |
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 Collector, Thread 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()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public static void RelatarFragmentacao() { GCMemoryInfo info = GC.GetGCMemoryInfo(); Console.WriteLine($"=== Relatório de Fragmentação ==="); Console.WriteLine($"Heap Total: {info.HeapSizeBytes / 1024 / 1024} MB"); Console.WriteLine($"Fragmentação Total: {info.FragmentedBytes / 1024 / 1024} MB"); Console.WriteLine($"Percentual: {(double)info.FragmentedBytes / info.HeapSizeBytes * 100:F2}%"); Console.WriteLine(); <em>// Detalhes por geração (.NET 5+)</em> for (int i = 0; i < info.GenerationInfo.Length; i++) { var gen = info.GenerationInfo[i]; string nome = i switch { 0 => "Gen 0", 1 => "Gen 1", 2 => "Gen 2", 3 => "LOH", 4 => "POH", _ => $"Gen {i}" }; Console.WriteLine($"{nome}:"); Console.WriteLine($" Tamanho: {gen.SizeAfterBytes / 1024 / 1024} MB"); Console.WriteLine($" Fragmentação: {gen.FragmentationAfterBytes / 1024} KB"); } } |
Saída exemplo:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
=== Relatório de Fragmentação === Heap Total: 205 MB Fragmentação Total: 23 MB Percentual: 11.22% Gen 0: Tamanho: 2 MB Fragmentação: 12 KB Gen 1: Tamanho: 4 MB Fragmentação: 20 KB Gen 2: Tamanho: 127 MB Fragmentação: 8234 KB LOH: Tamanho: 68 MB Fragmentação: 15234 KB POH: Tamanho: 0 MB Fragmentação: 32 KB |
PerfView (Análise de Pinning)
O PerfView oferece a visão “Pinning At GC Time Stacks” que mostra exatamente quais objetos estão pinned e qual código os fixou:
- Baixe o PerfView: https://github.com/microsoft/perfview/releases
- Execute:
perfview /GCCollectOnly /AcceptEULA /nogui collect - Reproduza o cenário problemático
- Pare a coleta
- Abra o arquivo
.etl.zipno PerfView - 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:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<em>// RUIM: Aloca novo array no LOH a cada chamada</em> public void ProcessarArquivoRuim(string caminho) { byte[] buffer = new byte[1_000_000]; <em>// 1MB → LOH</em> using var stream = File.OpenRead(caminho); stream.Read(buffer, 0, buffer.Length); ProcessarBuffer(buffer); <em>// buffer será coletado, deixando buraco no LOH</em> } <em>// BOM: Reutiliza buffers do pool</em> public void ProcessarArquivoBom(string caminho) { byte[] buffer = ArrayPool<byte>.Shared.Rent(1_000_000); try { using var stream = File.OpenRead(caminho); int bytesLidos = stream.Read(buffer, 0, buffer.Length); ProcessarBuffer(buffer.AsSpan(0, bytesLidos)); } finally { ArrayPool<byte>.Shared.Return(buffer); } } |
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:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<em>// RUIM: Pinning no SOH causa fragmentação</em> public void EnviarDadosRuim() { byte[] buffer = new byte[4096]; GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); try { EnviarParaCodigoNativo(handle.AddrOfPinnedObject()); } finally { handle.Free(); } } <em>// BOM: Aloca diretamente no POH</em> public void EnviarDadosBom() { <em>// Alocação no Pinned Object Heap - não fragmenta o SOH</em> byte[] buffer = GC.AllocateArray<byte>(4096, pinned: true); unsafe { fixed (byte* ptr = buffer) { EnviarParaCodigoNativo((IntPtr)ptr); } } <em>// buffer pode permanecer no POH sem causar fragmentação no SOH</em> } |
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:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public void ForcarCompactacaoLOH() { Console.WriteLine("Antes da compactação:"); RelatarFragmentacao(); <em>// Configura para compactar o LOH na próxima coleta completa</em> GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; <em>// Força uma coleta completa (Gen2 + LOH)</em> GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true); Console.WriteLine("\nApós a compactação:"); RelatarFragmentacao(); <em>// O modo volta automaticamente para Default após a compactação</em> } |
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:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<em>// RUIM: String gigante vai para o LOH</em> public string ConcatenarTextos(IEnumerable<string> textos) { string resultado = ""; foreach (var texto in textos) { resultado += texto; <em>// Pode criar strings > 85KB no LOH</em> } return resultado; } <em>// BOM: StringBuilder gerencia memória de forma mais eficiente</em> public string ConcatenarTextosMelhor(IEnumerable<string> textos) { var sb = new StringBuilder(); foreach (var texto in textos) { sb.Append(texto); } return sb.ToString(); } <em>// MELHOR: Processar em chunks sem materializar tudo</em> public IEnumerable<string> ProcessarEmChunks(IEnumerable<string> textos, int tamanhoChunk) { var chunk = new List<string>(tamanhoChunk); foreach (var texto in textos) { chunk.Add(texto); if (chunk.Count >= tamanhoChunk) { yield return string.Join("", chunk); chunk.Clear(); } } if (chunk.Count > 0) { yield return string.Join("", chunk); } } |
5. Monitore e alerte
Configure alertas baseados nas métricas de fragmentação:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class MonitorFragmentacao : BackgroundService { private readonly ILogger<MonitorFragmentacao> _logger; private const double LimiteAlertaPercentual = 20.0; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var info = GC.GetGCMemoryInfo(); double fragmentacaoPercentual = (double)info.FragmentedBytes / info.HeapSizeBytes * 100; if (fragmentacaoPercentual > LimiteAlertaPercentual) { _logger.LogWarning( "Fragmentação de memória alta: {Percentual:F2}% ({FragmentadoMB}MB de {TotalMB}MB)", fragmentacaoPercentual, info.FragmentedBytes / 1024 / 1024, info.HeapSizeBytes / 1024 / 1024); } await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); } } } |
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
|
1 |
dotnet-dump collect -p 12345 -o memorydump.dmp |
Passo 2: Abra no WinDbg e carregue SOS
|
1 |
.loadby sos coreclr |
Passo 3: Verifique o tamanho geral do heap
|
1 2 3 |
0:000> !eeheap -gc ... GC Allocated Heap Size: 2147483648 bytes (2 GB) |
Passo 4: Analise a fragmentação
|
1 2 3 4 5 |
0:000> !dumpheap -type Free -stat Statistics: MT Count TotalSize Class Name 00007ff9e0c05540 45234 734003200 Free |
734MB livres em 45.234 blocos = fragmentação severa!
Passo 5: Calcule o percentual
|
1 |
Fragmentação = 734.003.200 / 2.147.483.648 × 100 = 34,2% |
34,2% de fragmentação – muito acima do limite aceitável de 20%.
Passo 6: Identifique onde está a fragmentação
|
1 2 3 4 5 6 7 8 |
0:000> !heapstat Heap Gen0 Gen1 Gen2 LOH POH Heap0 4194304 8388608 1600000000 500000000 4000000 Free space: Percentage Heap0 24576 32768 34003200 700000000 0 SOH: 34 MB (2.1%) LOH: 700 MB (140%) |
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
|
1 2 3 4 5 6 7 |
0:000> !dumpheap -type Free 00007ff9b0c01000 00007ff9c0c00000 Address MT Size ... 00007ff9b8234000 00007ff9e0c05540 16777216 ← 16MB livre 00007ff9b9456000 00007ff9e0c05540 8388608 ← 8MB livre ... |
Maior bloco livre: 16MB. Qualquer alocação > 16MB falhará.
Passo 8: Identifique o que está causando
|
1 2 3 4 5 6 7 |
0:000> !dumpheap -stat 00007ff9b0c01000 00007ff9c0c00000 Statistics: MT Count TotalSize Class Name 00007ff9abc12340 1234 412000000 System.Byte[] 00007ff9def56780 567 88000000 System.String 00007ff9e0c05540 45234 734003200 Free |
System.Byte[]: 1.234 objetos ocupando 412MB no LOH. Este é o candidato.
Passo 9: Encontre quem está alocando
|
1 2 3 4 5 6 7 8 9 10 11 12 |
0:000> !dumpheap -mt 00007ff9abc12340 00007ff9b0c01000 00007ff9c0c00000 -short 00007ff9b1234000 00007ff9b2345000 00007ff9b3456000 ... 0:000> !gcroot 00007ff9b1234000 Thread 15: 00007ff9e0001234 MyApp.Services.ImageProcessor.ResizeImage(System.IO.Stream) ... |
Diagnóstico final: ImageProcessor.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
| Comando | Propósito |
|---|---|
!eeheap -gc | Estrutura completa do heap gerenciado |
!dumpheap -stat | Estatísticas de objetos por tipo |
!dumpheap -type Free -stat | Total de fragmentação |
!dumpheap -type Free | Lista de todos os blocos livres |
!heapstat | Estatísticas por geração |
!gchandles | Lista de GCHandles (pinned, strong, etc.) |
!gcroot <addr> | Encontra referências mantendo objeto vivo |
!objsize <addr> | Tamanho total de um objeto (incluindo filhos) |
!verifyheap | Verifica corrupção no heap |
!address -summary | Uso 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 pinned. O 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:
- Fragmentação medida > 30% usando
!dumpheap -type Free -stat - Maior bloco livre < tamanho necessário para próximas alocações
- A aplicação está em período de baixa carga (a compactação é bloqueante)
- 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ário | Ferramenta Recomendada |
|---|---|
| Monitoramento contínuo em produção | dotnet-counters ou métricas via OpenTelemetry |
| Diagnóstico profundo de dump | WinDbg + SOS |
| Análise visual interativa | dotMemory (JetBrains) |
| Investigação de pinning | PerfView |
| Desenvolvimento/debugging local | Visual Studio Diagnostic Tools |
O WinDbg é a ferramenta definitiva para diagnóstico profundo porque oferece acesso direto à memória do processo, sem abstrações.

