Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Av Sébastien Ros och Rick Anderson
Minneshantering är komplext, även i ett hanterat ramverk som .NET. Det kan vara svårt att analysera och förstå minnesproblem. This article:
- Motiverades av många problem med minnesläckor och GC-fel. De flesta av dessa problem orsakades av att inte förstå hur minnesförbrukning fungerar i .NET eller att du inte förstår hur den mäts.
- Visar problematisk minnesanvändning och föreslår alternativa metoder.
Så här fungerar skräpinsamling (GC) i .NET
GC allokerar heapsegment där varje segment är ett sammanhängande minnesintervall. Objekt som placeras i heapen kategoriseras i någon av tre generationer: 0, 1 eller 2. Genereringen avgör hur ofta GC försöker frigöra minne på hanterade objekt som inte längre refereras till av appen. Lägre numrerade generationer är GC'd oftare.
Objekt flyttas från en generation till en annan baserat på deras livslängd. När objekten lever längre flyttas de till en högre generation. Som tidigare nämnts är högre generationer GC'd mindre ofta. Kortvariga objekt finns alltid kvar i generation 0. Objekt som refereras under en webbbegärans livslängd är till exempel kortvariga. Singletons på applikationsnivå migreras vanligtvis till generation 2.
När en ASP.NET Core-app startar, GC:
- Reserverar lite minne för de första heapsegmenten.
- Allokerar en liten del av minnet när runtime laddas in.
De föregående minnesallokeringarna görs av prestandaskäl. Prestandafördelarna kommer från heap-segment i sammanhängande minne.
GC.Collect caveats
I allmänhet bör ASP.NET Core-appar i produktion inte använda GC.Collect explicit. Att inducera skräpsamlingar vid underoptimala tider kan minska prestanda avsevärt.
GC.Collect är användbart när du undersöker minnesläckor. Anrop GC.Collect() utlöser en blockerande skräpinsamlingscykel som försöker frigöra alla objekt som inte är tillgängliga från hanterad kod. Det är ett användbart sätt att förstå storleken på de nåbara levande objekten i heapen och spåra minnesstorlekens tillväxt över tid.
Analysera minnesanvändningen för en app
Dedikerade verktyg kan hjälpa till att analysera minnesanvändning:
- Räknar objektreferenser
- Mäta hur stor inverkan GC har på CPU-användningen
- Mäta minnesutrymme som används för varje generation
Använd följande verktyg för att analysera minnesanvändning:
- dotnet-trace: Kan användas på produktionsdatorer.
- Analysera minnesanvändning utan Visual Studio-felsökningsprogrammet
- Profilera minnesanvändning i Visual Studio
Identifiera minnesproblem
Aktivitetshanteraren kan användas för att få en uppfattning om hur mycket minne en ASP.NET app använder. Minnesvärdet för Aktivitetshanteraren:
- Representerar mängden minne som används av ASP.NET processen.
- Innehåller appens levande objekt och andra minneskonsumenter, till exempel intern minnesanvändning.
Om värdet för aktivitetshanterarens minne ökar oavbrutet och aldrig planar ut, har appen en minnesläcka. Följande avsnitt visar och förklarar flera minnesanvändningsmönster.
Exempel på användningsapp för visningsminne
MemoryLeak-exempelappen är tillgänglig på GitHub. MemoryLeak-appen:
- Innehåller en diagnostikkontrollant som samlar in realtidsminne och GC-data för appen.
- Har en indexsida som visar minne och GC-data. Sidan Index uppdateras varje sekund.
- Innehåller en API-kontrollant som innehåller olika minnesbelastningsmönster.
- Är inte ett verktyg som stöds, men det kan användas för att visa minnesanvändningsmönster för ASP.NET Core-appar.
Run MemoryLeak. Allokerat minne ökar långsamt tills en GC inträffar. Minnet ökar eftersom verktyget allokerar anpassade objekt för att samla in data. Följande bild visar sidan MemoryLeak Index när en Gen 0 GC inträffar. Diagrammet visar 0 RPS (begäranden per sekund) eftersom inga API-slutpunkter från API-kontrollanten har anropats.
Diagrammet visar två värden för minnesanvändningen:
- Allokerat: mängden minne som upptas av hanterade objekt
- Arbetsuppsättning: Uppsättningen med sidor i det virtuella adressutrymmet för den process som för närvarande finns i fysiskt minne. Den arbetsuppsättning som visas är samma värde som Aktivitetshanteraren visar.
Transient objects
Följande API skapar en 20 KB Stränginstans och returnerar den till klienten. På varje begäran allokeras ett nytt objekt i minnet och skrivs till svaret. Strängar lagras som UTF-16 tecken i .NET så varje tecken tar 2 byte i minnet.
[HttpGet("bigstring")]
public ActionResult<string> GetBigString()
{
return new String('x', 10 * 1024);
}
Följande diagram genereras med en relativt liten inläsning för att visa hur minnesallokeringar påverkas av GC.
Föregående diagram visar:
- 4K RPS (begäranden per sekund).
- Generation 0 GC-samlingar sker ungefär varannan sekund.
- Arbetsmängden är konstant på cirka 500 MB.
- Processorn är 12%.
- Minnesförbrukningen och frisättningen (via GC) är stabil.
Följande diagram tas med maximalt dataflöde som kan hanteras av datorn.
Föregående diagram visar:
- 22K RPS
- Generation 0 GC-insamlingar sker flera gånger per sekund.
- Generation 1-samlingar utlöses eftersom appen allokerade betydligt mer minne per sekund.
- Arbetsmängden är konstant på cirka 500 MB.
- CPU är 33%.
- Minnesförbrukningen och frisättningen (via GC) är stabil.
- Processorn (33%) är inte överutnyttjad, därför kan skräpinsamlingen hålla jämna steg med ett stort antal allokeringar.
Arbetsstation GC jämfört med Server GC
.NET Garbage Collector har två olika lägen:
- Arbetsstation GC: Optimerad för skrivbordet.
- Standard-GC för ASP.NET Core-appar. Optimerad för servern. Optimized for the server.
GC-läget kan anges explicit i projektfilen eller i filen för runtimeconfig.json den publicerade appen. Följande markering visar inställningen ServerGarbageCollection i projektfilen:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
Om du ändrar ServerGarbageCollection i projektfilen måste appen återskapas.
Observera: Serveravfallsinsamling är inte tillgänglig på datorer med en enkelkärna. Mer information finns i IsServerGC.
Följande bild visar minnesprofilen under en 5K RPS med hjälp av arbetsstationens GC.
Skillnaderna mellan det här diagrammet och serverversionen är betydande:
- Arbetsuppsättningen sjunker från 500 MB till 70 MB.
- GC gör generation 0-samlingar flera gånger per sekund i stället för varannan sekund.
- GC sjunker från 300 MB till 10 MB.
I en typisk webbservermiljö är cpu-användning viktigare än minne, och därför är server-GC bättre. Om minnesanvändningen är hög och processoranvändningen är relativt låg kan arbetsstationens GC vara mer högpresterande. Till exempel att ha hög densitet och köra flera webbappar där minne är en bristvara.
GC med Docker och små containrar
När flera containerbaserade appar körs på en dator kan Arbetsstation GC vara mer högpresterande än Server GC. För mer information, se Kör med Server GC i en liten container och Kör med Server GC i ett litet containerscenario del 1 – Hård gräns för GC Heap.
Beständiga objektreferenser
GC kan inte frigöra objekt som refereras. Objekt som refereras men inte längre behövs resulterar i en minnesläcka. Om appen ofta allokerar objekt och inte kan frigöra dem när de inte längre behövs, ökar minnesanvändningen med tiden.
Följande API skapar en 20 KB Stränginstans och returnerar den till klienten. Skillnaden med föregående exempel är att den här instansen refereras av en statisk medlem, vilket innebär att den aldrig är tillgänglig för samling.
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;
}
Föregående kod:
- Är ett exempel på en typisk minnesläcka.
- Vid frekventa anrop ökar minnesanvändningen i appen tills processen kraschar med ett
OutOfMemoryundantag.
I föregående bild:
- Belastningstestning av
/api/staticstringslutpunkten orsakar en linjär ökning av minnet. - GC försöker frigöra minne när minnestrycket växer genom att anropa en samling av generation 2.
- GC kan inte frigöra det läckta minnet. Allokerade och arbetssats ökar över tid.
Vissa scenarier, till exempel cachelagring, kräver att objektreferenser hålls kvar tills minnestrycket tvingar dem att släppas. Klassen WeakReference kan användas för den här typen av cachelagringskod. Ett WeakReference objekt samlas upp vid minnesbelastning. Standardimplementeringen av IMemoryCache använder WeakReference.
Native memory
Vissa .NET-objekt förlitar sig på internt minne. Inbyggt minne kan inte samlas in av GC. .NET-objektet med internt minne måste frigöra det med inbyggd kod.
.NET tillhandahåller IDisposable gränssnittet så att utvecklare kan frigöra inbyggt minne. Även om Dispose inte anropas, så kommer korrekt implementerade klasser att anropa Dispose när finaliseraren körs.
Överväg följande kod:
[HttpGet("fileprovider")]
public void GetFileProvider()
{
var fp = new PhysicalFileProvider(TempPath);
fp.Watch("*.*");
}
PhysicalFileProvider är en hanterad klass, så alla instanser samlas in i slutet av begäran.
Följande bild visar minnesprofilen medan API:et fileprovider anropas kontinuerligt.
Föregående diagram visar ett uppenbart problem med implementeringen av den här klassen, eftersom den fortsätter att öka minnesanvändningen. Det här är ett känt problem som spåras i det här ärendet.
Samma läcka kan inträffa i användarkoden med något av följande:
- Släpper inte klassen korrekt.
- Glömmer att anropa metoden för
Disposede beroende objekt som ska tas bort.
Heap för stora objekt
Frekvent minnesallokering/kostnadsfria cykler kan fragmentera minne, särskilt när stora delar av minnet allokeras. Objekt allokeras i sammanhängande minnesblock. När GC frigör minne försöker det defragmentera det för att minimera fragmenteringen. Den här processen kallas komprimering. Komprimering innebär att flytta objekt. Att flytta stora objekt medför en prestationsnedsättning. Därför skapar GC en särskild minneszon för stora objekt, som kallas för den stora objekthögen (LOH). Objekt som är större än 85 000 byte (cirka 83 KB) är:
- Placerad på LOH.
- Not compacted.
- Insamlat under generation 2 GCs.
När LOH är full utlöser GC en samling av generation 2. Generation 2-samlingar:
- Är till sin natur långsamma.
- Dessutom medföra kostnader för att initiera en insamling i alla andra generationer.
Följande kod komprimerar LOH omedelbart:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
Se LargeObjectHeapCompactionMode för information om hur du komprimerar LOH.
I containrar som använder .NET Core 3.0 eller senare komprimeras LOH automatiskt.
Följande API som illustrerar det här beteendet:
[HttpGet("loh/{size=85000}")]
public int GetLOH1(int size)
{
return new byte[size].Length;
}
Följande diagram visar minnesprofilen för att anropa /api/loh/84975 slutpunkten under maximal belastning:
Följande diagram visar minnesprofilen för att anropa /api/loh/84976 slutpunkten och allokera bara ytterligare en byte:
Observera: Strukturen byte[] har överliggande byte. Därför utlöser 84 976 byte gränsen på 85 000.
Jämföra de två föregående diagrammen:
- Arbetsuppsättningen liknar båda scenarierna, cirka 450 MB.
- LOH-begäranden under (84 975 byte) visar mestadels insamlingar av generation 0.
- Överdrivna LOH-förfrågningar genererar ständiga generation 2-samlingar. Generation 2-samlingar är dyra. Mer PROCESSOR krävs och dataflödet sjunker nästan 50%.
Tillfälliga stora objekt är särskilt problematiska eftersom de orsakar gen2-GCs.
För maximal prestanda bör användningen av stora objekt minimeras. Dela om möjligt upp stora objekt. Till exempel delar Response Caching middleware i ASP.NET Core cacheposterna i block som är mindre än 85 000 byte.
Följande länkar visar metoden ASP.NET Core för att hålla objekt under LOH-gränsen:
Mer information finns i:
HttpClient
Felaktig användning HttpClient kan resultera i en resursläcka. Systemresurser, till exempel databasanslutningar, sockets, filhandtag osv.:
- Är mer knappa än minne.
- Är mer problematiska när de läcker än minne.
Erfarna .NET-utvecklare vet att anropa på Dispose på objekt som implementerar IDisposable. Att inte ta bort objekt som implementeras IDisposable resulterar vanligtvis i läckt minne eller läckta systemresurser.
HttpClient implementerar IDisposable, men bör inte tas bort vid varje anrop. I stället bör HttpClient återanvändas.
Följande slutpunkt skapar och bortser från en ny HttpClient instans för varje begäran:
[HttpGet("httpclient1")]
public async Task<int> GetHttpClient1(string url)
{
using (var httpClient = new HttpClient())
{
var result = await httpClient.GetAsync(url);
return (int)result.StatusCode;
}
}
Under belastning loggas följande felmeddelanden:
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)
Även om HttpClient-instanserna tas bort, tar det lite tid innan operativsystemet frigör den faktiska nätverksanslutningen. Genom att kontinuerligt skapa nya anslutningar uppstår portöverbelastning . Varje klientanslutning kräver en egen klientport.
Ett sätt att förhindra portöverbelastning är att återanvända samma HttpClient instans:
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;
}
Instansen HttpClient släpps när appen stoppas. Det här exemplet visar att inte alla disponibla resurser ska tas bort efter varje användning.
Se följande för ett bättre sätt att hantera livslängden för en HttpClient instans:
Object pooling
I föregående exempel visades hur instansen HttpClient kan göras statisk och återanvändas av alla begäranden. Återanvändning förhindrar att resurserna börjar ta slut.
Object pooling:
- Använder återanvändningsmönstret.
- Är utformad för objekt som är dyra att skapa.
En pool är en samling förinitierade objekt som kan reserveras och släppas över trådar. Pooler kan definiera allokeringsregler som gränser, fördefinierade storlekar eller tillväxttakt.
NuGet-paketet Microsoft.Extensions.ObjectPool innehåller klasser som hjälper dig att hantera sådana pooler.
Följande API-slutpunkt instansierar en byte buffert som fylls med slumpmässiga tal på varje begäran:
[HttpGet("array/{size}")]
public byte[] GetArray(int size)
{
var random = new Random();
var array = new byte[size];
random.NextBytes(array);
return array;
}
Följande diagram visar anrop till föregående API med måttlig belastning:
I föregående diagram sker generation 0-samlingar ungefär en gång per sekund.
Föregående kod kan optimeras genom att poola bufferten byte med hjälp av ArrayPool<T>. En statisk instans återanvänds mellan begäranden.
Det som skiljer sig åt med den här metoden är att ett poolobjekt returneras från API:et. That means:
- Objektet är utom din kontroll så snart du kommer tillbaka från metoden.
- Du kan inte släppa objektet.
Så här konfigurerar du bortskaffande av objektet:
- Kapsla in den poolade arrayen i ett engångsobjekt.
- Registrera det poolade objektet med HttpContext.Response.RegisterForDispose.
RegisterForDispose tar hand om anropet Dispose på målobjektet så att det bara släpps när HTTP-begäran är klar.
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;
}
Om du använder samma belastning som den icke-poolade versionen visas följande diagram:
Den största skillnaden är allokerade byte och därmed mycket färre generation 0-samlingar.
Additional resources
ASP.NET Core