Internals da Garbage Collection no .NET 8/9: Server GC vs. Workstation GC e Engenharia de Memória

terça-feira, 20 de outubro de 2026 | ReadingTime
Internals da Garbage Collection no .NET 8/9: Server GC vs. Workstation GC e Engenharia de Memória

A gestão eficiente de memória em sistemas corporativos de alta escala permanece como um requisito arquitetural indispensável para aplicações .NET de alto desempenho. Com o lançamento do .NET 8 e as refinadas atualizações do .NET 9, a Microsoft introduziu otimizações profundas no Garbage Collector (GC) do Common Language Runtime (CLR), com foco em Adaptação Dinâmica à Topologia (DAT), suporte a arquiteturas de hardware NUMA e otimização das áreas de alocação de heap.


Compreender a mecânica do runtime é essencial para engenheiros de sistemas e arquitetos de software que constroem microsserviços, plataformas financeiras e APIs de alta vazão. A escolha entre os modos Server GC e Workstation GC, aliada ao uso de primitivas de memória modernas como Span<T> e ArrayPool<T>, impacta diretamente a taxa de utilização de CPU, o tempo de pausa do aplicativo e os custos com infraestrutura em nuvem.


1. Funcionamento Interno do CLR GC: Gerações 0/1/2, Segmentos Efêmeros e Heaps LOH/POH


O Garbage Collector do .NET opera como um gerenciador automático de memória estruturado em gerações, desenhado para reduzir o impacto operacional na limpeza do heap. A memória gerenciada é dividida em gerações de acordo com a expectativa de vida dos objetos:


  • Geração 0 (Gen 0): Armazena objetos de vida curta, como variáveis locais, DTOs temporários e instâncias pequenas de strings. As alocações ocorrem no segmento efêmero de forma acelerada, e as coletas nesta fase são extremamente rápidas e de baixíssima latência.
  • Geração 1 (Gen 1): Funciona como uma zona de amortecimento. Objetos que sobrevivem a uma coleta na Gen 0 são promovidos para a Gen 1, atuando como um buffer para estabilizar coletas frequentes antes que os objetos sejam promovidos para a Gen 2.
  • Geração 2 (Gen 2): Contém objetos de vida longa, como singletons da aplicação, caches estáticos, trabalhadores em segundo plano e conexões com banco de dados. Coletas na Gen 2 (Full GCs) inspecionam todo o heap e consomem mais ciclos de CPU e tempo de pausa.
  • Large Object Heap (LOH): Destinado a alocações que excedem 85.000 bytes. Os objetos no LOH ignoram a promoção nas gerações 0 e 1, indo diretamente para a Gen 2. Para evitar degradação de desempenho, o LOH é varrido sem compactação automática contínua, embora a compactação possa ser configurada explicitamente.
  • Pinned Object Heap (POH): Introduzido no .NET 5 e aprimorado no .NET 8/9, o POH armazena objetos fixados (pinned) na memória para chamadas nativas (P/Invoke) ou operações de I/O. Isolar objetos fixados no POH evita a fragmentação das gerações 0, 1 e 2, garantindo compacções previsíveis no restante do heap.


2. Server GC vs. Workstation GC: Adaptação Dinâmica à Topologia (DAT), NUMA e FinOps na Nuvem


A configuração do modo de GC no arquivo runtimeconfig.json determina a forma como o CLR mapeia heaps gerenciados para os núcleos de processamento e arquitetura de hardware disponíveis.


JSON 

{ 

 "runtimeOptions": { 

   "configProperties": { 

     "System.GC.Server": true, 

     "System.GC.HeapCount": 4, 

     "System.GC.DynamicAdaptationToTopology": 1 

   } 

 } 

} 


Arquitetura Workstation GC


O Workstation GC é projetado para aplicações interativas (desktops, CLIs) onde a responsividade da interface gráfica é prioridade. Ele utiliza uma única thread dedicada para rotinas de coleta, rodando concorrentemente com as threads da aplicação em um único heap compartilhado.


Arquitetura Server GC


O Server GC aloca threads dedicadas de coleta e heaps independentes (Gen 0/1/2, LOH, POH) para cada núcleo de CPU alocado. As coletas ocorrem em paralelo em alta prioridade, maximizando o processamento de requisições concorrentes em servidores Web e APIs ASP.NET Core. Em hardwares de grande porte com arquitetura NUMA, o Server GC mapeia heaps diretamente para nós NUMA locais, evitando saturação do barramento de memória.


Dynamic Adaptation to Topology (DAT) no .NET 8/9


Historicamente, habilitar o Server GC em ambientes de contêineres restritos (como pods no Kubernetes com 2 vCPUs rodando em hosts de 64 núcleos) podia causar alocação excessiva de memória, pois o CLR criava heaps baseados na máquina física hospedeira.


No .NET 8 e aprimorado no .NET 9, a Adaptação Dinâmica à Topologia (DAT) ajusta dinamicamente a quantidade de heaps e threads de GC com base na memória e na cota de CPU alocadas ao contêiner em tempo real. Se os limites do contêiner mudarem, o DAT reorganiza a topologia do heap sem necessidade de reiniciar o processo, reduzindo os custos operacionais (FinOps) em clusters gerenciados.


Configuração dos Modos de Garbage Collection (GC) no .NET

  • Workstation GC
  • Quantidade de Heaps: 1 Heap Compartilhado
  • Coleta Paralela: Baixa (Thread única)
  • Suporte a NUMA: Não
  • Cenário Ideal de Uso: Aplicações desktop, ferramentas de linha de comando (CLI) e micro-contêineres com menos de 1 vCPU.
  • Server GC (Default)
  • Quantidade de Heaps: 1 Heap por Núcleo de CPU
  • Coleta Paralela: Alta (Multi-threaded)
  • Suporte a NUMA: Sim
  • Cenário Ideal de Uso: APIs Web, Microsserviços de alta demanda e Processadores de tarefas em lote (Batch).
  • Server GC + DAT (.NET 8/9)
  • Quantidade de Heaps: Escalonável Dinamicamente
  • Coleta Paralela: Adaptativa
  • Suporte a NUMA: Sim
  • Cenário Ideal de Uso: Pods implantados em Kubernetes e serviços Serverless com auto-escalamento.



3. Padrões de Alocação Zero: Otimização de Código com Span, Memory e ArrayPool


Configurar parâmetros no runtime não é suficiente para sistemas que exigem latência determinística. Desenvolver código de alto desempenho exige eliminar alocações desnecessárias no Heap em caminhos críticos de execução para evitar coletas na Gen 0 e promoção para a Gen 2.


Primitivas Modernas de Memória


  • Span<T> e ReadOnlySpan<T>: Estruturas baseadas em pilha (stack-only) que representam regiões contíguas de memória. Permitem fatiar e analisar strings e buffers sem criar novas alocações no Heap.
  • Memory<T> e ReadOnlyMemory<T>: Alternativas ao Span<T> alocáveis no Heap, utilizadas quando a referência de memória precisa cruzar métodos assíncronos (await).
  • ArrayPool<T>: Reutiliza buffers alocados previamente através do método ArrayPool<T>.Shared.Rent(), reduzindo drasticamente a alocação de arrays temporários no Large Object Heap (LOH).


Exemplo Prático: Refatoração para Alocação Zero


Código Legado (Alocações no Heap)


C# 

// Gera múltiplas alocações de memória temporária no Heap 

public class LegacyPayloadParser 

{ 

   public static (string Header, string Value) ParseHeader(string rawPayload) 

   { 

       // Alocação 1: Criação do array de strings pelo Split 

       string[] parts = rawPayload.Split(':'); 

        

       // Alocações 2 e 3: Instanciação de novas strings pelo Trim 

       string header = parts[0].Trim(); 

       string value = parts[1].Trim(); 

        

       return (header, value); 

   } 

} 


 


Código Moderno .NET 8/9 (Alocação Zero) 



C# 

using System; 

using System.Buffers; 

 

public class HighPerformancePayloadParser 

{ 

   // Zero alocações no Heap: fatiamento direto em memória stack via Spans 

   public static void ParseHeader(ReadOnlySpan<char> rawPayload, out ReadOnlySpan<char> header, out ReadOnlySpan<char> value) 

   { 

       int delimiterIndex = rawPayload.IndexOf(':'); 

       if (delimiterIndex == -1) 

       { 

           header = ReadOnlySpan<char>.Empty; 

           value = ReadOnlySpan<char>.Empty; 

           return; 

       } 

 

       // Método Slice manipula referências sem instanciar novas strings 

       header = rawPayload.Slice(0, delimiterIndex).Trim(); 

       value = rawPayload.Slice(delimiterIndex + 1).Trim(); 

   } 

 

   // Uso do ArrayPool para evitar alocações temporárias no LOH 

   public static void ProcessLargeBuffer(int size) 

   { 

       // Aluga o buffer do pool compartilhado em vez de instanciar 'new byte[size]' 

       byte[] buffer = ArrayPool<byte>.Shared.Rent(size); 

       try 

       { 

           Span<byte> bufferSpan = buffer.AsSpan(0, size); 

           // Executa operações de I/O diretamente sobre o Span 

       } 

       finally 

       { 

           // Devolve o buffer ao pool zerando a pressão sobre o LOH 

           ArrayPool<byte>.Shared.Return(buffer); 

       } 

   } 

} 


Resumo Estratégico para Lideranças Técnicas


Otimizar o uso de memória no ecossistema .NET exige equilibrar a topologia do hardware com a escrita de código consciente. Adotar o Server GC aliado ao Dynamic Adaptation to Topology (DAT) em contêineres e aplicar padrões de alocação zero com Span<T> e ArrayPool<T> fornece a base para sistemas estáveis, com menor tempo de resposta e custos de infraestrutura otimizados.


Como a sua equipe está ajustando as configurações de GC do .NET e aplicando padrões de alocação zero em microsserviços de alta demanda este ano?

Marcio Mazeu
Escrito por
Marcio Mazeu

.NET Software Developer

Analista-Programador .NET graduado em Engenharia de Computação, especializado na modernização de aplicações e arquitetura de APIs (C#, SQL Server, JavaScript). Apaixonado pelo desenvolvimento de ferramentas de alta performance e pelo aprendizado contínuo, contribui para a criação de soluções tecnológicas confiáveis e focadas na experiência do usuário.