Evento
Campionato do Mundo de Power BI DataViz
Feb 14, 4 PM - Mar 31, 4 PM
Con 4 posibilidades de entrar, poderías gañar un paquete de conferencias e facelo ao Live Grand Finale en Las Vegas
Máis informaciónEste explorador xa non é compatible.
Actualice a Microsoft Edge para dispoñer das funcionalidades máis recentes, as actualizacións de seguranza e a asistencia técnica.
Por Sébastien Ros y Rick Anderson
La administración de memoria es compleja, incluso en un marco administrado como .NET. Analizar y comprender los problemas de memoria puede ser difícil. Este artículo:
El GC asigna segmentos de montón donde cada segmento es un rango contiguo de memoria. Los objetos colocados en el montón se clasifican en una de tres generaciones: 0, 1 o 2. La generación determina la frecuencia con la que el GC intenta liberar memoria en objetos administrados a los que ya no hace referencia la aplicación. Las generaciones numeradas más bajas tienen GC con más frecuencia.
Los objetos se mueven de una generación a otra en función de su duración. A medida que los objetos residen más tiempo, se mueven a una generación superior. Como se mencionó anteriormente, las generaciones más altas tienen GC con menos frecuencia. Los objetos de corta duración siempre permanecen en la generación 0. Por ejemplo, los objetos a los que se hace referencia durante la vida de una solicitud web son de corta duración. Los singleton de nivel de aplicación suelen migrar a la generación 2.
Cuando se inicia una aplicación ASP.NET Core, el GC:
Las asignaciones de memoria anteriores se realizan por motivos de rendimiento. La ventaja de rendimiento proviene de segmentos de montón en memoria contigua.
En general, las aplicaciones de ASP.NET Core en producción no deben usar GC.Collect explícitamente. La inducción de recolecciones de elementos no utilizados en momentos poco óptimos puede reducir significativamente el rendimiento.
GC.Collect resulta útil al investigar fugas de memoria. La llamada a GC.Collect()
desencadena un ciclo de recolección de elementos no utilizados bloqueante que intenta reclamar todos los objetos inaccesibles del código administrado. Es una manera útil de comprender el tamaño de los objetos activos accesibles en el montón y realizar un seguimiento del crecimiento del tamaño de la memoria a lo largo del tiempo.
Las herramientas dedicadas pueden ayudar a analizar el uso de memoria:
Use las siguientes herramientas para analizar el uso de memoria:
El Administrador de tareas se puede usar para hacerse una idea de la cantidad de memoria que usa una aplicación de ASP.NET. Valor de memoria del Administrador de tareas:
Si el valor de memoria del Administrador de tareas aumenta indefinidamente y nunca se aplana, la aplicación tiene una pérdida de memoria. En las secciones siguientes se muestran y explican varios patrones de uso de memoria.
La aplicación de ejemplo MemoryLeak está disponible en GitHub. La aplicación MemoryLeak:
Ejecutar MemoryLeak. La memoria asignada aumenta lentamente hasta que se produce una GC. La memoria aumenta porque la herramienta asigna un objeto personalizado para capturar datos. En la imagen siguiente se muestra la página Índice MemoryLeak cuando se produce una GC de generación 0. El gráfico muestra 0 RPS (solicitudes por segundo) porque no se ha llamado a ningún punto de conexión de API del controlador de API.
El gráfico muestra dos valores para el uso de memoria:
La SIGUIENTE API crea una instancia de string de 20 KB y la devuelve al cliente. En cada solicitud, se asigna un nuevo objeto en memoria y se escribe en la respuesta. Las cadenas se almacenan como caracteres UTF-16 en .NET, por lo que cada carácter toma 2 bytes en memoria.
[HttpGet("bigstring")]
public ActionResult<string> GetBigString()
{
return new String('x', 10 * 1024);
}
El gráfico siguiente se genera con una carga relativamente pequeña para mostrar cómo las asignaciones de memoria se ven afectadas por el GC.
En el gráfico anterior se muestra lo siguiente:
El siguiente gráfico se toma en el rendimiento máximo que puede controlar la máquina.
En el gráfico anterior se muestra lo siguiente:
El Recolector de elementos no utilizados de .NET tiene dos modos diferentes:
El modo GC se puede establecer explícitamente en el archivo del proyecto o en el archivo runtimeconfig.json
de la aplicación publicada. El marcado siguiente muestra la configuración de ServerGarbageCollection
en el archivo de proyecto:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
El cambio de ServerGarbageCollection
en el archivo del proyecto requiere que se vuelva a compilar la aplicación.
Nota: La recolección de elementos no utilizados del servidor no está disponible en las máquinas con un único núcleo. Para obtener más información, vea IsServerGC.
En la imagen siguiente se muestra el perfil de memoria en un RPS de 5K mediante el GC de estación de trabajo.
Las diferencias entre este gráfico y la versión del servidor son significativas:
En un entorno de servidor web típico, el uso de CPU es más importante que la memoria, por lo que el GC de servidor es mejor. Si el uso de memoria es alto y el uso de CPU es relativamente bajo, el GC de estación de trabajo podría ser más eficaz. Por ejemplo, el hospedaje de alta densidad de varias aplicaciones web donde la memoria es escasa.
Cuando varias aplicaciones en contenedores se ejecutan en una máquina, el GC de estación de trabajo podría ser más eficaz que el GC de servidor. Para obtener más información, vea Ejecución con GC de servidor en un contenedor pequeño y Ejecución con GC de servidor en un escenario de contenedor pequeño Parte 1: límite máximo para el montón de GC.
El GC no puede liberar objetos a los que se hace referencia. Los objetos a los que se hace referencia, pero que ya no son necesarios, generan una pérdida de memoria. Si la aplicación asigna con frecuencia objetos y no los libera después de que ya no sean necesarios, el uso de memoria aumentará con el tiempo.
La SIGUIENTE API crea una instancia de string de 20 KB y la devuelve al cliente. La diferencia con el ejemplo anterior es que un miembro estático hace referencia a esta instancia, lo que significa que nunca está disponible para la colección.
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;
}
El código anterior:
OutOfMemory
.
En la imagen anterior:
/api/staticstring
provoca un aumento lineal de la memoria.Algunos escenarios, como el almacenamiento en caché, requieren que las referencias a objetos se mantengan hasta que la presión de memoria los obliga a liberarse. La clase WeakReference se puede usar para este tipo de código de almacenamiento en caché. Un objeto WeakReference
se recopila bajo presión de memoria. La implementación predeterminada de IMemoryCache usa WeakReference
.
Algunos objetos de .NET Core dependen de la memoria nativa. El GC no puede recopilar memoria nativa. El objeto .NET que usa memoria nativa debe liberarlo mediante código nativo.
.NET proporciona la interfaz IDisposable para permitir que los desarrolladores liberen memoria nativa. Incluso si no se llama a Dispose, las clases implementadas correctamente llaman a Dispose
cuando se ejecuta el finalizador.
Observe el código siguiente:
[HttpGet("fileprovider")]
public void GetFileProvider()
{
var fp = new PhysicalFileProvider(TempPath);
fp.Watch("*.*");
}
PhysicalFileProvider es una clase administrada, por lo que cualquier instancia se recopilará al final de la solicitud.
En la imagen siguiente se muestra el perfil de memoria al invocar la API fileprovider
continuamente.
En el gráfico anterior se muestra un problema obvio con la implementación de esta clase, ya que sigue aumentando el uso de memoria. Se trata de un problema conocido que está siendo objeto de seguimiento en este problema.
La misma fuga podría producirse en el código de usuario, mediante una de las siguientes acciones:
Dispose
de los objetos dependientes que se deben eliminar.Los ciclos frecuentes de asignación/liberación de memoria pueden fragmentar la memoria, especialmente cuando se asignan grandes fragmentos de memoria. Los objetos se asignan en bloques contiguos de memoria. Para mitigar la fragmentación, cuando el GC libera memoria, intenta desfragmentarla. Este proceso se denomina compactación. La compactación implica mover objetos. Mover objetos grandes supone una penalización de rendimiento. Por este motivo, el GC crea una zona de memoria especial para objetos grandes , denominado montón de objetos grandes (LOH). Los objetos que tienen más de 85 000 bytes (aproximadamente 83 KB) están:
Cuando el LOH esté lleno, el GC desencadenará una colección de generación 2. Colecciones de generación 2:
El código siguiente compacta el LOH inmediatamente:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
Consulte LargeObjectHeapCompactionMode para obtener información sobre cómo compactar el LOH.
En contenedores que usan .NET Core 3.0 y versiones posteriores, el LOH se compacta automáticamente.
La siguiente API muestra este comportamiento:
[HttpGet("loh/{size=85000}")]
public int GetLOH1(int size)
{
return new byte[size].Length;
}
En el gráfico siguiente se muestra el perfil de memoria de llamar al punto de conexión /api/loh/84975
, bajo la carga máxima:
En el gráfico siguiente se muestra el perfil de memoria de llamar al punto de conexión /api/loh/84976
, asignando un solo byte más:
Nota: La estructura byte[]
tiene bytes de sobrecarga. Por eso 84 976 bytes desencadena el límite de 85 000.
Comparación de los dos gráficos anteriores:
Los objetos grandes temporales son especialmente problemáticos porque provocan GC de generación 2.
Para obtener un rendimiento máximo, se debe minimizar el uso de objetos grandes. Si es posible, divida los objetos grandes. Por ejemplo, el middleware de almacenamiento en caché de respuesta en ASP.NET Core divide las entradas de caché en bloques inferiores a 85 000 bytes.
Los vínculos siguientes muestran el enfoque de ASP.NET Core para mantener los objetos bajo el límite de LOH:
Para más información, consulte:
El uso incorrecto de HttpClient puede dar lugar a una fuga de recursos. Recursos del sistema, como conexiones de base de datos, sockets, manipuladores de archivo, etc.:
Los desarrolladores experimentados de .NET saben llamar a Dispose en objetos que implementan IDisposable. No eliminar objetos que implementan IDisposable
normalmente da como resultado la pérdida de memoria o recursos del sistema filtrados.
HttpClient
implementa IDisposable
, pero no debe eliminarse en todas las invocaciones. En su lugar, debe reutilizarse HttpClient
.
El siguiente punto de conexión crea y elimina una nueva instancia de HttpClient
en cada solicitud:
[HttpGet("httpclient1")]
public async Task<int> GetHttpClient1(string url)
{
using (var httpClient = new HttpClient())
{
var result = await httpClient.GetAsync(url);
return (int)result.StatusCode;
}
}
En carga, se registran los siguientes mensajes de error:
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)
Aunque las instancias de HttpClient
se eliminan, el sistema operativo tarda algún tiempo en liberarse la conexión de red real. Al crear continuamente nuevas conexiones, se produce el agotamiento de puertos. Cada conexión de cliente requiere su propio puerto de cliente.
Una manera de evitar el agotamiento de puertos es reutilizar la misma instancia de 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;
}
La instancia de HttpClient
se libera cuando la aplicación se detiene. En este ejemplo se muestra que no se deben eliminar todos los recursos descartables después de cada uso.
Consulte lo siguiente para obtener una mejor manera de controlar la duración de una instancia de HttpClient
:
En el ejemplo anterior se mostró cómo la instancia de HttpClient
puede hacerse estática y reutilizarla en todas las solicitudes. La reutilización evita la ejecución de recursos.
Agrupación de objetos:
Un grupo es una colección de objetos inicializados previamente que se pueden reservar y liberar entre subprocesos. Los grupos pueden definir reglas de asignación como límites, tamaños predefinidos o tasa de crecimiento.
El paquete NuGet Microsoft.Extensions.ObjectPool contiene clases que ayudan a administrar dichos grupos.
El siguiente punto de conexión de API crea una instancia de un búfer de byte
que se rellena con números aleatorios en cada solicitud:
[HttpGet("array/{size}")]
public byte[] GetArray(int size)
{
var random = new Random();
var array = new byte[size];
random.NextBytes(array);
return array;
}
En el gráfico siguiente se muestra una llamada a la API anterior con carga moderada:
En el gráfico anterior, las colecciones de generación 0 se producen aproximadamente una vez por segundo.
El código anterior se puede optimizar agrupando el búfer de byte
mediante ArrayPool<T>. Una instancia estática se reutiliza en todas las solicitudes.
Lo que es diferente con este enfoque es que se devuelve un objeto agrupado de la API. Esto significa lo siguiente:
Para configurar la eliminación del objeto:
RegisterForDispose
se encargará de llamar a Dispose
en el objeto de destino para que solo se libere cuando se complete la solicitud 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;
}
La aplicación de la misma carga que la versión no agrupada da como resultado el siguiente gráfico:
La principal diferencia son los bytes asignados y, como consecuencia, muchas menos colecciones de generación 0.
Comentarios de ASP.NET Core
ASP.NET Core é un proxecto de código aberto. Selecciona unha ligazón para ofrecer comentarios:
Evento
Campionato do Mundo de Power BI DataViz
Feb 14, 4 PM - Mar 31, 4 PM
Con 4 posibilidades de entrar, poderías gañar un paquete de conferencias e facelo ao Live Grand Finale en Las Vegas
Máis informaciónFormación
Módulo
Mejore el rendimiento con una memoria caché en proyectos de .NET Aspire - Training
En este módulo, obtendrá información sobre las memorias caché en una aplicación nativa de nube de .NET Aspire y cómo usarlas para optimizar el rendimiento de los microservicios.
Documentación
Valores de configuración del recolector de elementos no utilizados - .NET
Obtenga información sobre los valores del entorno de ejecución para configurar el modo en el que el recolector de elementos no utilizados administra la memoria de las aplicaciones de .NET.
Tutorial Depuración de una fuga de memoria - .NET
Obtenga información sobre cómo depurar una pérdida de memoria en .NET.
Herramienta de diagnóstico dotnet-trace: CLI de .NET - .NET
Aprenda a instalar y usar la herramienta dotnet-trace de la CLI para recopilar seguimientos de .NET de un proceso en ejecución sin el generador de perfiles nativo, mediante EventPipe de .NET.