Mudanças no Garbage Collector do .NET 10
O garbage collector do .NET 10 traz uma mudança que pode alterar o consumo de memória e a latência das suas aplicações logo após a migração. Por isso, entender o que mudou e como medir o impacto é tão importante quanto atualizar o target framework.
O GC é um dos componentes mais críticos de performance e previsibilidade em aplicações modernas. Ele automatiza o ciclo de vida dos objetos alocados no heap gerenciado, equilibrando throughput, latência e consumo de memória.
À medida que o runtime evolui, o GC também muda, e o garbage collector do .NET 10 segue essa lógica. Isso afeta diretamente aplicações com alto volume de alocação, serviços com cargas variadas e sistemas que precisam de baixa latência.
Visão geral do GC no .NET e por que ele evolui
O GC do .NET foi projetado para tornar o gerenciamento de memória produtivo e seguro, removendo a necessidade de liberação manual. Ainda assim, ele precisa tomar decisões sofisticadas o tempo todo, como estas:
- Quando executar uma coleta.
- Quais regiões de memória inspecionar.
- Como compactar e reduzir a fragmentação.
- De que forma lidar com diferentes tamanhos e perfis de objetos.
- Como distribuir o trabalho entre threads, no workstation GC e no server GC.
No garbage collector do .NET 10, essas decisões deixam de ser apenas técnicas. Na prática, elas se ligam diretamente a métricas de produção:
- Latência no percentil 99 (P99).
- Custo de infraestrutura, ou seja, a memória provisionada.
- Estabilidade sob carga.
- Tempo de resposta em picos de tráfego.
Se você quer revisar os fundamentos antes de seguir, o artigo garbage collector em C#: o que é e como funciona explica gerações, heap e coletas em detalhes.
Workstation GC e server GC
O .NET oferece dois modos principais de coleta. O workstation GC foi pensado para aplicações cliente e é o padrão em aplicações standalone.
Já o server GC foca em throughput e escalabilidade. Ele trata o processo como o dono da máquina e, por isso, cria um heap por núcleo de CPU disponível.
Por exemplo, aplicações ASP.NET Core usam o server GC por padrão. Por outro lado, aplicações de desktop e cenários interativos tendem a se beneficiar do workstation GC, dependendo das metas de latência e responsividade.
Você define o modo explicitamente no arquivo de projeto:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<!-- Usa o server GC, padrão em aplicações ASP.NET Core -->
<ServerGarbageCollection>true</ServerGarbageCollection>
<!-- Mantém o background GC ativo -->
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>
</Project>
O problema do server GC tradicional, que o garbage collector do .NET 10 tenta resolver, é que o tamanho do heap depende da máquina. Assim, o mesmo serviço pode usar muito mais memória ao ser movido para um servidor com mais núcleos, mesmo fazendo o mesmo trabalho.
O que muda no garbage collector do .NET 10
A principal mudança no garbage collector do .NET 10 é o DATAS, sigla para Dynamic Adaptation To Application Sizes. Ele surgiu como recurso opcional no .NET 8 e passou a vir ativado por padrão no .NET 9 para quem usa server GC.
Porém, o .NET 9 não é uma versão LTS. Portanto, muitas equipes vão encontrar o DATAS pela primeira vez justamente ao migrar para o .NET 10, que é LTS.
Foi por isso que Maoni, arquiteta do GC do .NET na Microsoft, escreveu o artigo Preparing for the .NET 10 GC. Segundo ela, quem usa server GC pode notar um perfil de performance bem mais diferente do que em upgrades anteriores do runtime.
A própria autora explica a escolha do título. Normalmente, features do GC não exigem intervenção do usuário, mas o DATAS é diferente, e por isso o texto fala em preparação, não apenas em novidade.
Na prática, o uso de memória no garbage collector do .NET 10 pode ficar drasticamente diferente, muito provavelmente menor. Então, cabe a cada equipe verificar se esse comportamento combina com seus objetivos de otimização.
Como o DATAS funciona no garbage collector do .NET 10
No garbage collector do .NET 10, o objetivo do DATAS é fazer o tamanho do heap acompanhar a necessidade real da aplicação. Em outras palavras, o heap fica aproximadamente proporcional ao tamanho dos dados de longa duração.
Assim, se o app faz o mesmo trabalho em máquinas com especificações diferentes, o heap fica igual ou parecido. Além disso, quando a carga fica mais leve ou mais pesada, o heap se ajusta de acordo.
Para isso, o DATAS faz quatro coisas, segundo a documentação oficial do DATAS:
- Define o máximo de alocações permitidas antes da próxima coleta com base no tamanho dos dados de longa duração.
- Define o volume real de alocações permitido com base no throughput.
- Ajusta o número de heaps conforme a necessidade.
- Executa coletas completas com compactação quando a fragmentação fica alta demais.
Orçamento da gen0 e live data size
O ponto central é o orçamento da geração 0, isto é, quanto a aplicação pode alocar antes de disparar a próxima coleta. O DATAS calcula esse teto a partir do live data size (LDS), o volume de dados que sobrevive às coletas.
A fórmula usa um multiplicador: 20 menos o valor de conserve_memory, dividido pela raiz quadrada do LDS em MB. Como o conserve_memory vale 5 por padrão no DATAS, o resultado fica 15 / sqrt(LDS em MB), limitado entre 0,1 e 10.
Veja como o orçamento muda conforme o tamanho da aplicação:
| LDS | Multiplicador | Orçamento da gen0 |
|---|---|---|
| 1 MB | 10,00 | 10 MB |
| 10 MB | 4,74 | 47 MB |
| 100 MB | 1,50 | 150 MB |
| 1 GB | 0,47 | 474 MB |
| 10 GB | 0,15 | 1,5 GB |
Ou seja, aplicações pequenas ganham multiplicadores maiores, enquanto aplicações grandes ganham multiplicadores menores. Dessa forma, o heap não explode só porque a máquina tem muitos núcleos.
Número de heaps dinâmico
No server GC tradicional, o número de heaps acompanha o número de núcleos, mas o garbage collector do .NET 10 muda essa regra. No DATAS, o processo começa com um único heap e cresce ou diminui conforme a carga.
Com isso, o DATAS funciona como um híbrido entre os modos existentes. Ele pode usar tão pouco quanto um heap, como o workstation GC, ou tantos quanto os núcleos da máquina, como o server GC.
Contudo, começar com um heap tem um custo. Se muitas threads alocam ao mesmo tempo no início, algumas precisam esperar até o GC aumentar a quantidade de heaps.
Throughput cost percentage
Para equilibrar memória e desempenho, o DATAS mira um throughput cost percentage (TCP) de 2%. Essa métrica considera as pausas do GC e o tempo que as alocações precisam esperar.
Normalmente, as pausas dominam, então o percentual de tempo pausado em GC serve como boa aproximação. No entanto, o tempo de espera das alocações pesa mais em dois momentos: na inicialização do processo e na transição de uma carga leve para uma pesada.
Mudanças de comportamento e sintomas
O GC não é apenas uma implementação de coleta automática. Na verdade, ele é um conjunto de heurísticas com impacto direto em throughput e pausas.
Por isso, aplicações no garbage collector do .NET 10 podem ter variações de comportamento que antes eram pouco perceptíveis. Ao migrar, principalmente em sistemas de alta escala, monitore estes sinais:
- Aumento ou redução no número total de coletas por minuto.
- Alteração no tempo médio de pausa.
- Crescimento do heap em padrões diferentes do esperado.
- Mudanças na frequência de coletas da geração 2.
- Fragmentação e custo de compactação.
Os benchmarks da Microsoft mostram o tamanho dessa mudança no garbage collector do .NET 10. Nos testes TechEmpower JSON e Fortunes, em uma máquina Linux com 48 núcleos, o working set caiu mais de 80%, enquanto o throughput máximo caiu de 2% a 3%.
Além disso, o número de coletas das gerações 0 e 1 aumenta bastante com o DATAS ativo. Isso é esperado, porque o orçamento de alocação fica menor.
Em um dos casos de cliente citados por Maoni, o orçamento da gen0 caiu de 4,22 GB para 1,64 GB. Ao mesmo tempo, o percentual de pausa subiu de 1,2% para 2,1%, bem próximo da meta de 2%.
Quando desativar o DATAS no garbage collector do .NET 10
O DATAS não serve para todos os cenários. A própria autora lista quatro situações em que desativá-lo no garbage collector do .NET 10 faz sentido.
O primeiro cenário surge quando você não tem uso para a memória liberada. Por exemplo, se o serviço roda sozinho em uma máquina dedicada e ninguém vai aproveitar a memória economizada, não há motivo para mudar.
O segundo envolve inicialização crítica. Como o DATAS sempre começa com um heap e leva tempo para crescer, o throughput na partida tende a regredir.
Em terceiro lugar vem a tolerância zero a regressões de throughput. Se o percentual de pausa atual já é muito baixo, como 1%, a meta de 2% do DATAS pode representar uma perda que você não aceita.
Por fim, cenários dominados por coletas da geração 2 também podem não se beneficiar do garbage collector do .NET 10 com DATAS. Muitas vezes, isso acontece por excesso de objetos grandes temporários, e vale investigar a causa antes de desligar o recurso.
Configurações do garbage collector do .NET 10
Com o garbage collector do .NET 10, depender de padrões implícitos vira um risco técnico. Portanto, a recomendação é definir explicitamente o modo de GC, documentar o motivo e validar com benchmark e observabilidade.
Para desativar o DATAS, use a propriedade GarbageCollectionAdaptationMode no projeto:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<!-- 0 desativa o DATAS e volta ao server GC tradicional -->
<GarbageCollectionAdaptationMode>0</GarbageCollectionAdaptationMode>
</PropertyGroup>
Também é possível configurar no runtimeconfig.template.json, que gera o runtimeconfig.json na publicação:
{
"configProperties": {
"System.GC.Server": true,
"System.GC.DynamicAdaptationMode": 0
}
}
Se preferir manter o DATAS e apenas ajustá-lo, o garbage collector do .NET 10 trouxe novas opções. A System.GC.DGen0GrowthPercent aplica um percentual sobre o multiplicador, e as opções DGen0GrowthMaxFactor e DGen0GrowthMinFactor mudam os limites em permil.
{
"configProperties": {
"System.GC.Server": true,
"System.GC.DGen0GrowthPercent": 200,
"System.GC.DTargetTCP": 5
}
}
Nesse exemplo, o valor 200 dobra o multiplicador calculado pelo DATAS. Já o DTargetTCP igual a 5 aceita até 5% de custo de throughput, em vez dos 2% padrão.
Em contêineres, as variáveis de ambiente costumam ser mais práticas. Lembre que os valores numéricos das variáveis usam notação hexadecimal:
# Desativa o DATAS
export DOTNET_GCDynamicAdaptationMode=0
# Dobra o multiplicador da gen0 (200 em hexadecimal é C8)
export DOTNET_GCDGen0GrowthPercent=C8
# Define a meta de TCP em 5%
export DOTNET_GCDTargetTCP=5
Atenção a um detalhe importante: definir GCHeapCount desativa o DATAS. Isso acontece porque essa opção manda o GC não mudar o número de heaps, o que contradiz o funcionamento do recurso.
Essas configurações só são lidas na inicialização do GC. Assim, alterar uma variável de ambiente com o processo rodando não tem efeito até o próximo restart.
Como medir o impacto da mudança
A melhor forma de medir o garbage collector do .NET 10 é capturar um trace de GC com e sem o DATAS. No PerfView, a visão GCStats mostra a coluna Pause Time, que aproxima o TCP, e a coluna Gen0 Alloc MB, que aproxima o orçamento da gen0.
Também vale expor métricas da própria aplicação. O código abaixo usa APIs do namespace System e System.Runtime para registrar o estado do GC em um endpoint de diagnóstico:
using System.Runtime;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/diagnostics/gc", () =>
{
// Informações da última coleta realizada
var info = GC.GetGCMemoryInfo();
return Results.Ok(new
{
IsServerGc = GCSettings.IsServerGC,
HeapSizeMb = info.HeapSizeBytes / 1024 / 1024,
PauseTimePercentage = info.PauseTimePercentage,
Gen0Collections = GC.CollectionCount(0),
Gen1Collections = GC.CollectionCount(1),
Gen2Collections = GC.CollectionCount(2),
// Lista as configurações efetivas do GC no processo
Settings = GC.GetConfigurationVariables()
});
});
app.Run();
Para acompanhar em tempo real, use a ferramenta dotnet-counters. Ela mostra tamanho do heap, coletas por geração e tempo em GC sem alterar o código:
# Instala a ferramenta global
dotnet tool install --global dotnet-counters
# Monitora os contadores de runtime do processo
dotnet-counters monitor --name OrdersApi --counters System.Runtime
Compare sempre os números do garbage collector do .NET 10 com uma linha de base sem o DATAS. Caso contrário, você não sabe se a mudança veio do GC ou de outra parte da migração.
Recomendações práticas para migrar ao garbage collector do .NET 10
Com base no artigo oficial, a melhor forma de se preparar para o garbage collector do .NET 10 envolve um conjunto direto de ações:
- Estabeleça uma linha de base com métricas de GC e consumo na versão atual.
- Migre para um ambiente de teste com o runtime do .NET 10.
- Execute cargas representativas, evitando testes sintéticos que não reflitam o tráfego real.
- Compare indicadores de latência, sobretudo P95 e P99.
- Valide os limites de memória, especialmente em contêineres.
- Documente as decisões de configuração, mantendo rastreabilidade.
Além disso, revise configurações antigas. Por exemplo, quem limitava o número de heaps manualmente para conter memória pode remover essa opção e testar o DATAS com ajustes no multiplicador.
Em um segundo caso citado por Maoni, um processo que usava dois heaps fixos teve o percentual de pausa reduzido drasticamente após adotar o DATAS com o multiplicador dobrado. Por isso, teste antes de decidir desligar o recurso.
Se a migração também envolve outras mudanças do runtime, o artigo sobre a mudança no TimeSpan.FromMilliseconds no .NET 10 mostra outro ponto que merece atenção.
Conclusão
O garbage collector não é um detalhe interno do runtime. Pelo contrário, ele é parte ativa do comportamento operacional de qualquer sistema em .NET.
No garbage collector do .NET 10, o DATAS ajusta o heap ao tamanho real da aplicação, reduzindo bastante a memória usada em troca de um pequeno custo de throughput. Para a maioria dos serviços em contêineres, essa troca vale a pena.
Ainda assim, a adoção do .NET 10 deve vir acompanhada de instrumentação, benchmark e validação. Afinal, ajustes no GC podem alterar padrões de consumo de memória e latência.
Em resumo, trate o GC como uma dimensão de arquitetura e operação. Meça, entenda e configure de forma intencional, reduzindo riscos e aumentando a previsibilidade.
FAQ
O DATAS vale para o workstation GC?
Não. O DATAS atua sobre o server GC, que é o modo padrão das aplicações ASP.NET Core.
O DATAS é novidade do .NET 10?
Não exatamente. Ele surgiu como opcional no .NET 8 e ficou ativo por padrão no .NET 9, mas muita gente vai conhecê-lo ao migrar para o .NET 10, que é LTS.
Como desativo o DATAS?
Defina GarbageCollectionAdaptationMode como 0 no projeto, System.GC.DynamicAdaptationMode como 0 no runtimeconfig ou DOTNET_GCDynamicAdaptationMode=0 no ambiente.
Por que minha aplicação faz mais coletas depois da migração?
Com o DATAS, o orçamento da gen0 fica menor e proporcional aos dados de longa duração. Por isso, coletas das gerações 0 e 1 ficam mais frequentes.
Configurar GCHeapCount afeta o DATAS?
Sim. Definir GCHeapCount desativa o DATAS, porque impede o GC de ajustar o número de heaps.