Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Por Sébastien Ros e Rick Anderson
O gerenciamento da memória é complexo, mesmo em uma estrutura gerenciada como o .NET. Analisar e resolver problemas de memória pode ser desafiador. Os problemas de GC (vazamentos de memória e coleta de lixo) geralmente ocorrem devido à falta de compreensão sobre como o consumo de memória funciona em .NET ou não compreendendo o processo de medição do uso.
Este artigo demonstra padrões comuns de uso de memória que podem ser problemáticos e sugere abordagens alternativas.
Explorar a coleta de lixo (GC) no .NET
O processo de GC aloca segmentos de heap em que cada segmento é um intervalo contíguo de memória. Os objetos colocados no heap são categorizados em uma das três gerações: 0, 1 ou 2. A geração determina a frequência com que o coletor de lixo (GC) tenta liberar memória em objetos gerenciados que o aplicativo não referencia mais. O GC acessa as gerações de número mais baixo com mais frequência.
Os objetos são movidos de uma geração para outra com base no seu tempo de vida. À medida que os objetos vivem mais, eles são movidos para uma geração mais alta. Conforme mencionado anteriormente, o GC é executado com menos frequência em gerações mais altas. Objetos de curta duração sempre permanecem na Geração 0. Por exemplo, os objetos referenciados durante a vida útil de uma solicitação da Web têm vida curta. Singletons de nível de aplicativo geralmente migram para Gen 2.
Quando um aplicativo ASP.NET Core é iniciado, o processo do GC:
- Reserva alguma memória para os segmentos de heap iniciais.
- Aloca uma pequena porção de memória quando o runtime é carregado.
As alocações de memória anteriores são feitas por motivos de desempenho. O benefício do desempenho vem dos segmentos de heap na memória contígua.
Examinar as ressalvas ao usar GC.Collect
Em geral, aplicativos ASP.NET Core em produção não devem usar o método GC.Collect explicitamente. Induzir coletas de lixo em tempos abaixo do ideal pode diminuir significativamente o desempenho.
GC.Collect é útil ao investigar vazamentos de memória. Chamar GC.Collect() dispara um ciclo de coleta de lixo de bloqueio que tenta recuperar todos os objetos inacessíveis do código gerenciado. É uma maneira útil de entender o tamanho dos objetos dinâmicos acessíveis no heap e acompanhar o crescimento do tamanho da memória ao longo do tempo.
Analisar o uso de memória de um aplicativo
As ferramentas dedicadas podem ajudar a analisar o uso da memória, incluindo:
- Contagem de referências de objetos.
- Medindo quanto efeito o GC tem sobre o uso da CPU.
- Medindo o espaço de memória usado para cada geração.
Utilize as seguintes ferramentas para analisar o uso da memória:
- utilitário dotnet-trace (pode ser usado em computadores de produção)
- Analisar o uso da memória sem o depurador do Visual Studio
- Medir o uso de memória no Visual Studio
Detectar problemas de memória
O Gerenciador de Tarefas pode ser utilizado para você ter uma ideia da quantidade de memória que um aplicativo ASP.NET está usando. O valor da memória do Gerenciador de Tarefas:
- Representa a quantidade de memória usada pelo processo de ASP.NET.
- Inclui os objetos ativos do aplicativo e outros consumidores de memória, como o uso de memória nativa.
Se o valor da memória do Gerenciador de Tarefas aumentar indefinidamente e nunca se estabilizar, o aplicativo tem um perda de memória. As seções a seguir demonstram e explicam vários padrões de uso da memória.
Explorar o aplicativo de exemplo de uso de memória de exibição
O exemplo de aplicativo MemoryLeak está disponível no GitHub. O aplicativo MemoryLeak:
- Inclui um controlador de diagnóstico que reúne dados em tempo real de memória e GC para o aplicativo.
- Tem uma página de índice que exibe os dados da memória e do GC. A página de índice é atualizada a cada segundo.
- Contém um controlador de API que fornece vários padrões de carga da memória.
- Pode ser usado para exibir padrões de uso de memória de aplicativos ASP.NET Core, mas não é uma ferramenta com suporte.
Executar o MemoryLeak. A memória alocada aumenta lentamente até que ocorra um GC. A memória aumenta porque a ferramenta aloca um objeto personalizado para capturar dados. A imagem a seguir mostra a página do índice do MemoryLeak quando um GC de geração 0 ocorre. O gráfico mostra 0 RPS (Solicitações por segundo) porque nenhum endpoint do controlador da API foi chamado.
O gráfico exibe dois valores para o uso da memória:
- Alocado: a quantidade de memória ocupada por objetos gerenciados.
- Conjunto de trabalho: o conjunto de páginas no espaço de endereço virtual do processo que atualmente residem na memória física. O conjunto de trabalho mostrado tem o mesmo valor exibido pelo Gerenciador de Tarefas. Para obter mais informações, consulte Conjunto de Trabalho.
Objetos transitórios
A API a seguir cria uma instância de String de 20 KB e a retorna ao cliente. Em cada solicitação, um novo objeto é alocado na memória e gravado na resposta. As cadeias de caracteres são armazenadas como caracteres UTF-16 no .NET, de modo que cada caractere ocupa 2 bytes na memória.
[HttpGet("bigstring")]
public ActionResult<string> GetBigString()
{
return new String('x', 10 * 1024);
}
O gráfico a seguir é gerado com uma carga relativamente pequena que mostra como o GC afeta as alocações de memória.
O gráfico ilustra os seguintes detalhes:
- RPS de 4K (solicitações por segundo)
- As coleções de GC de geração 0 ocorrem a cada 2 segundos
- Conjunto de Trabalho Constante, aproximadamente 500 MB
- A CPU é 12%
- Consumo e liberação de memória estáveis (pelo GC)
O gráfico a seguir é obtido na taxa de transferência máxima que o computador pode manipular.
O gráfico ilustra os seguintes detalhes:
- RPS de 22 K
- As coleções de GC gen 0 ocorrem várias vezes por segundo
- As coleções da geração 1 são acionadas porque o aplicativo aloca significativamente mais memória por segundo.
- Conjunto de Trabalho Constante, aproximadamente 500 MB
- A CPU é 33%
- Consumo e liberação de memória estáveis (pelo GC)
- A CPU (33%) não está sobrecarregada, portanto, a coleta de lixo consegue lidar com um grande número de alocações.
GC da estação de trabalho versus GC do servidor
O Coletor de Lixo do .NET tem dois modos diferentes:
- Estação de trabalho GC: otimizado para computador de mesa.
- Server GC: o GC padrão para aplicativos ASP.NET Core. Otimizado para o servidor.
O modo GC pode ser definido explicitamente no arquivo de projeto ou no arquivo runtimeconfig.json do aplicativo publicado. A marcação a seguir mostra a configuração ServerGarbageCollection no arquivo do projeto:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
Se você alterar ServerGarbageCollection no arquivo do projeto, será necessário recriar o aplicativo.
Note
A coleta de lixo do servidor não está disponível em máquinas com um único núcleo. Para obter mais informações, consulte a propriedade IsServerGC.
A imagem a seguir mostra o perfil de memória em um RPS de 5K usando o GC da estação de trabalho.
As diferenças entre esse gráfico e a versão do servidor são significativas:
- Conjunto de Trabalho cai de 500 MB para 70 MB
- O GC faz coletas de geração 0 várias vezes por segundo em vez de a cada 2 segundos
- GC cai de 300 MB para 10 MB
Em um ambiente típico de servidor Web, o uso da CPU é mais importante do que a memória, portanto, o GC do servidor é melhor. Se o uso da memória for alto e o uso da CPU for relativamente baixo, o GC da estação de trabalho poderá ter um desempenho melhor. Por exemplo, hospedagem de alta densidade com vários aplicativos web onde a memória é escassa.
GC usando o Docker e contêineres pequenos
Quando vários aplicativos em contêineres são executados no mesmo computador, o GC da Estação de Trabalho pode ter um desempenho maior do que o GC do servidor. Para obter mais informações, consulte Executando com o Servidor GC em um Contêiner Pequeno (blog) e Executando com o Servidor GC em um Cenário de Contêiner Pequeno Parte 1 – Limite Rígido para o Heap GC (blog).
Referências de objetos persistentes
O GC não pode liberar objetos referenciados. Os objetos referenciados, mas que não são mais necessários, resultam em perda de memória. Se o aplicativo frequentemente aloca objetos e falha ao liberá-los depois que eles não são mais necessários, o uso de memória aumenta ao longo do tempo.
A API a seguir cria uma instância de String de 20 KB e a retorna ao cliente. A diferença com o exemplo anterior é que um membro estático faz referência a essa instância, o que significa que a instância nunca está disponível para coleção.
private static ConcurrentBag<string> _staticStrings = new ConcurrentBag<string>();
[HttpGet("staticstring")]
public ActionResult<string> GetStaticString()
{
var bigString = new String('x', 10 * 1024);
_staticStrings.Add(bigString);
return bigString;
}
O código anterior:
- Demonstra um vazamento de memória típico.
- Com chamadas frequentes, faz com que a memória do aplicativo aumente até que o processo seja interrompido com uma exceção
OutOfMemory.
O gráfico ilustra os seguintes detalhes:
- O teste de carga do endpoint
/api/staticstringcausa um aumento linear na memória - O GC tenta liberar memória à medida que a pressão sobre a memória aumenta, chamando uma coleta de Geração 2.
- O GC não consegue liberar a memória vazada; os conjuntos alocado e de trabalho aumentam com o tempo.
Alguns cenários, como o armazenamento em cache, exigem que as referências a objetos sejam mantidas até que a pressão da memória os force a serem liberados. A classe WeakReference pode ser utilizada para esse tipo de código de cache. Um objeto WeakReference é coletado sob pressão da memória. A implementação padrão da IMemoryCache interface usa WeakReference.
Memória nativa
Alguns objetos .NET dependem da memória nativa, mas a memória nativa não é coletável pelo GC. O objeto .NET que usa memória nativa deve liberá-lo usando código nativo.
O .NET fornece a interface IDisposable para permitir que os desenvolvedores liberem a memória nativa. Mesmo que o Dispose método não seja chamado, classes implementadas corretamente chamam Dispose quando o finalizador é executado.
Considere o seguinte código:
[HttpGet("fileprovider")]
public void GetFileProvider()
{
var fp = new PhysicalFileProvider(TempPath);
fp.Watch("*.*");
}
PhysicalFileProvider é uma classe gerenciada, portanto, qualquer instância é coletada no final da solicitação.
A imagem a seguir mostra o perfil da memória ao invocar a API fileprovider continuamente.
O gráfico anterior mostra um problema óbvio com a implementação dessa classe, pois ela continua aumentando o uso da memória. Esse resultado é um problema conhecido registrado em GitHub issue dotnet/aspnetcore nº 844.
O mesmo vazamento ocorre no código do usuário nos seguintes cenários:
- Não liberar a classe corretamente
- Esquecendo de invocar o
Disposemétodo dos objetos dependentes que devem ser descartados
Heap de Objetos Grandes
A alocação de memória frequente/os ciclos livres resultam em memória fragmentada, especialmente ao alocar grandes partes da memória. Os objetos são alocados em blocos contíguos de memória. Para mitigar a fragmentação, quando o GC libera a memória, ele tenta desfragmentá-la. Esse processo é chamado de compactação. A compactação envolve a movimentação de objetos. A movimentação de objetos grandes acarreta um impacto no desempenho. Por esse motivo, o GC cria uma zona de memória especial para objetos grandes, chamada large object heap (LOH). Os objetos com mais de 85.000 bytes (aproximadamente 83 KB) são:
- Colocado no LOH
- Não compactado
- Processado durante a coleta de Geração 2.
Quando o LOH está cheio, o GC aciona uma coleta de Geração 2.
- As coleções da Geração 2 são intrinsecamente lentas.
- Eles podem incorrer no custo de acionar uma coleta em todas as outras gerações.
O código a seguir compacta o LOH imediatamente.
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
Para obter informações sobre como compactar o LOH, consulte a propriedade LargeObjectHeapCompactionMode.
Em contêineres que utilizam .NET Core 3.0 ou superior, o LOH é compactado automaticamente.
A API a seguir ilustra esse comportamento:
[HttpGet("loh/{size=85000}")]
public int GetLOH1(int size)
{
return new byte[size].Length;
}
O gráfico a seguir mostra o perfil de memória da chamada do ponto de extremidade /api/loh/84975, sob carga máxima:
O gráfico a seguir mostra o perfil de memória ao chamar o endpoint /api/loh/84976, alocando apenas mais um byte:
Note
A estrutura byte[] tem bytes adicionais, razão pela qual 84.976 bytes acionam o limite de 85.000.
Comparando os dois gráficos anteriores:
- O Conjunto de Trabalho é semelhante para ambos os cenários, cerca de 450 MB
- Solicitações abaixo do LOH (84.975 bytes) mostram principalmente coletas de Geração 0.
- Solicitações acima do LOH geram coletas de Geração 2 constantes, que são caras. Mais CPU é necessária e a taxa de transferência cai cerca de 50%.
Objetos grandes temporários são problemáticos porque causam coletas de Geração 2.
Para obter o desempenho máximo, minimize o uso de objetos grandes. Se possível, divida os objetos grandes. Por exemplo, Response Caching Middleware em ASP.NET Core divide as entradas de cache em blocos inferiores a 85.000 bytes.
Os links a seguir mostram a abordagem do ASP.NET Core para manter os objetos abaixo do limite do LOH:
Para obter mais informações, consulte:
HttpClient
Usar a HttpClient classe incorretamente pode resultar em um vazamento de recursos.
Os recursos do sistema (como conexões de banco de dados, soquetes, identificadores de arquivo e assim por diante) apresentam dois problemas:
- Eles são mais escassos do que memória.
- Eles são mais problemáticos quando vazados do que a memória.
Desenvolvedores experientes do .NET sabem chamar o método Dispose em objetos que implementam a interface IDisposable. Se você não descartar os objetos que implementam IDisposable, geralmente isso resultará em vazamento de memória ou de recursos do sistema.
HttpClient implementa IDisposable, mas não deve ser descartado a cada invocação. Em vez disso, HttpClient deve ser reutilizado.
O seguinte endpoint cria e descarta uma nova instância de HttpClient a cada solicitação.
[HttpGet("httpclient1")]
public async Task<int> GetHttpClient1(string url)
{
using (var httpClient = new HttpClient())
{
var result = await httpClient.GetAsync(url);
return (int)result.StatusCode;
}
}
Sob carga, as seguintes mensagens de erro são registradas:
fail: Microsoft.AspNetCore.Server.Kestrel[13]
Connection id "0HLG70PBE1CR1", Request id "0HLG70PBE1CR1:00000031":
An unhandled exception was thrown by the application.
System.Net.Http.HttpRequestException: Only one usage of each socket address
(protocol/network address/port) is normally permitted --->
System.Net.Sockets.SocketException: Only one usage of each socket address
(protocol/network address/port) is normally permitted
at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port,
CancellationToken cancellationToken)
Mesmo que as HttpClient instâncias sejam descartadas, leva algum tempo para que o sistema operacional libere a conexão de rede real. O processo de criação contínua de novas conexões resulta no esgotamento das portas. Cada conexão de cliente exige sua própria porta de cliente.
Uma maneira de evitar o esgotamento da porta é reutilizar a mesma instância HttpClient:
private static readonly HttpClient _httpClient = new HttpClient();
[HttpGet("httpclient2")]
public async Task<int> GetHttpClient2(string url)
{
var result = await _httpClient.GetAsync(url);
return (int)result.StatusCode;
}
A instância HttpClient é liberada quando o aplicativo é interrompido. Esse exemplo mostra que nem todo recurso descartável deve ser descartado após cada uso.
Os artigos a seguir descrevem uma maneira melhor de lidar com o tempo de vida de uma HttpClient instância:
Pool de objetos
O exemplo anterior mostrou como a instância HttpClient pode se tornar estática e ser reutilizada por todas as solicitações. A reutilização evita que você fique sem recursos.
O agrupamento de objetos é uma alternativa.
- Ele usa o padrão de reutilização.
- O design é ideal para objetos que são caros de criar.
Um pool é uma coleção de objetos pré-inicializados que podem ser reservados e liberados entre threads. Os pools podem definir regras de alocação, como limites, tamanhos predefinidos ou taxa de crescimento.
O pacote NuGet Microsoft.Extensions.ObjectPool contém classes que ajudam a gerenciar esses pools.
O próximo endpoint da API instanciará um buffer byte que é preenchido com números aleatórios a cada solicitação.
[HttpGet("array/{size}")]
public byte[] GetArray(int size)
{
var random = new Random();
var array = new byte[size];
random.NextBytes(array);
return array;
}
O gráfico a seguir exibe a chamada à API anterior com carga moderada:
O gráfico revela que as coleções gen 0 ocorrem cerca de uma vez por segundo.
O código pode ser otimizado agrupando o byte buffer usando a classe T< ArrayPool>. Uma instância estática é reutilizada entre as solicitações.
O que é diferente com essa abordagem é que um objeto em pool é retornado da API:
- O objeto fica fora do seu controle assim que você retorna do método.
- Você não pode liberar o objeto.
Para configurar o descarte do objeto:
- Encapsule a matriz agrupada em um objeto descartável.
- Registre o objeto em pool usando o método HttpContext.Response.RegisterForDispose .
RegisterForDispose cuida da chamada Dispose no objeto de destino, portanto, o objeto é liberado somente após a conclusão da solicitação HTTP.
private static ArrayPool<byte> _arrayPool = ArrayPool<byte>.Create();
private class PooledArray : IDisposable
{
public byte[] Array { get; private set; }
public PooledArray(int size)
{
Array = _arrayPool.Rent(size);
}
public void Dispose()
{
_arrayPool.Return(Array);
}
}
[HttpGet("pooledarray/{size}")]
public byte[] GetPooledArray(int size)
{
var pooledArray = new PooledArray(size);
var random = new Random();
random.NextBytes(pooledArray.Array);
HttpContext.Response.RegisterForDispose(pooledArray);
return pooledArray.Array;
}
A aplicação da mesma carga que a versão não agrupada resulta no seguinte gráfico:
A principal diferença é os bytes alocados e, como consequência, menos coleções de Geração 0.