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.
// CÓDIGO QUE CAUSA FRAGMENTAÇÃO NO LOH
public void ProcessarArquivos(string[] caminhos)
{
foreach (var caminho in caminhos)
{
// Cada iteração aloca um array grande no LOH
byte[] buffer = File.ReadAllBytes(caminho); // Pode ser > 85KB
ProcessarBuffer(buffer);
// buffer sai de escopo, mas o "buraco" no LOH permanece
// até ser reutilizado por uma alocação de tamanho similar
}
}
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.
// Exemplo de pinning que causa fragmentação
public unsafe void ProcessarComPinning()
{
byte[] buffer1 = new byte[1024];
byte[] buffer2 = new byte[1024];
byte[] buffer3 = new byte[1024];
// Fixa buffer2 no meio
GCHandle handle = GCHandle.Alloc(buffer2, GCHandleType.Pinned);
try
{
IntPtr ptr = handle.AddrOfPinnedObject();
// Usar ptr com código nativo...
// Se buffer1 e buffer3 morrerem, o espaço não pode ser consolidado
// porque buffer2 está "preso" no meio
}
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.
// Padrão que causa fragmentação na Gen2
public class CacheProblematico
{
private readonly Dictionary<string, byte[]> _cache = new();
public void Processar(string chave, byte[] dados)
{
// Objetos ficam no cache por tempo indeterminado
_cache[chave] = dados;
// Eventualmente são removidos
if (_cache.Count > 1000)
{
var chavesAntigas = _cache.Keys.Take(100).ToList();
foreach (var k in chavesAntigas)
{
_cache.Remove(k); // Cria buracos na Gen2
}
}
}
}
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:
# Usando dotnet-dump (recomendado para .NET Core/.NET 5+)
dotnet-dump collect -p <PID>
# Usando procdump (Windows)
procdump -ma <PID> dump.dmp
# Usando Task Manager (Windows)
# Botão direito no processo → Create dump file
2. Abra o dump no WinDbg:
File → Open Crash Dump → selecione o arquivo .dmp
3. Carregue a extensão SOS:
# 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.
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:
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.
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:
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:
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:
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:
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):
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:
0:000> !eeheap -gc
...
Large object heap:
00007ff9b0c00000 00007ff9b0c01000 00007ff9b4d89e90 00007ff9b4d8a000
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):
0:000> !address -summary
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free 1847 7f98a2c0000 ( 127.598 TB) 99.69%
<unknown> 2134 023580000 ( 565.500 MB) 72.82% 0.00%
Image 1256 00a8c4000 ( 168.766 MB) 21.72% 0.00%
Heap 234 002340000 ( 35.250 MB) 4.54% 0.00%
Stack 24 000700000 ( 7.000 MB) 0.90% 0.00%
...
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE 1847 7f98a2c0000 ( 127.598 TB) 99.69%
MEM_RESERVE 312 012480000 ( 292.500 MB) 37.66% 0.00%
MEM_COMMIT 3089 01d200000 ( 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)
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()
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();
// Detalhes por geração (.NET 5+)
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:
=== 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:
// RUIM: Aloca novo array no LOH a cada chamada
public void ProcessarArquivoRuim(string caminho)
{
byte[] buffer = new byte[1_000_000]; // 1MB → LOH
using var stream = File.OpenRead(caminho);
stream.Read(buffer, 0, buffer.Length);
ProcessarBuffer(buffer);
// buffer será coletado, deixando buraco no LOH
}
// BOM: Reutiliza buffers do pool
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:
// RUIM: Pinning no SOH causa fragmentação
public void EnviarDadosRuim()
{
byte[] buffer = new byte[4096];
GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
EnviarParaCodigoNativo(handle.AddrOfPinnedObject());
}
finally
{
handle.Free();
}
}
// BOM: Aloca diretamente no POH
public void EnviarDadosBom()
{
// Alocação no Pinned Object Heap - não fragmenta o SOH
byte[] buffer = GC.AllocateArray<byte>(4096, pinned: true);
unsafe
{
fixed (byte* ptr = buffer)
{
EnviarParaCodigoNativo((IntPtr)ptr);
}
}
// buffer pode permanecer no POH sem causar fragmentação no SOH
}
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:
public void ForcarCompactacaoLOH()
{
Console.WriteLine("Antes da compactação:");
RelatarFragmentacao();
// Configura para compactar o LOH na próxima coleta completa
GCSettings.LargeObjectHeapCompactionMode =
GCLargeObjectHeapCompactionMode.CompactOnce;
// Força uma coleta completa (Gen2 + LOH)
GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true);
Console.WriteLine("\nApós a compactação:");
RelatarFragmentacao();
// O modo volta automaticamente para Default após a compactação
}
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:
// RUIM: String gigante vai para o LOH
public string ConcatenarTextos(IEnumerable<string> textos)
{
string resultado = "";
foreach (var texto in textos)
{
resultado += texto; // Pode criar strings > 85KB no LOH
}
return resultado;
}
// BOM: StringBuilder gerencia memória de forma mais eficiente
public string ConcatenarTextosMelhor(IEnumerable<string> textos)
{
var sb = new StringBuilder();
foreach (var texto in textos)
{
sb.Append(texto);
}
return sb.ToString();
}
// MELHOR: Processar em chunks sem materializar tudo
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:
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
dotnet-dump collect -p 12345 -o memorydump.dmp
Passo 2: Abra no WinDbg e carregue SOS
.loadby sos coreclr
Passo 3: Verifique o tamanho geral do heap
0:000> !eeheap -gc
...
GC Allocated Heap Size: 2147483648 bytes (2 GB)
Passo 4: Analise a fragmentação
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
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
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
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
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
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.

