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.

// 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

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:

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:

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çã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:

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

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):

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:

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)

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 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()

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)

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:

// 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 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?