Správa paměti a uvolňování paměti (GC) v ASP.NET Core
Správa paměti je složitá, dokonce i ve spravovaném rozhraní, jako je .NET. Analýza a pochopení problémů s pamětí může být náročná. Tento článek:
- Byl motivován mnoha problémy s nevrácenou pamětí a nepracuje uvolňování paměti. Většina těchto problémů byla způsobena tím, že neporozumily tomu, jak funguje spotřeba paměti v .NET Core, nebo nepochožily, jak se měří.
- Demonstruje problematické využití paměti a navrhuje alternativní přístupy.
Jak funguje uvolňování paměti v .NET Core
Uvolňování paměti přiděluje segmenty haldy, kde je každý segment souvislého rozsahu paměti. Objekty umístěné v haldě jsou rozdělené do jedné ze 3 generací: 0, 1 nebo 2. Generace určuje frekvenci, s kterou se globální katalog pokouší uvolnit paměť u spravovaných objektů, na které už aplikace nekazuje. Generace s nižším číslem jsou gc'd častěji.
Objekty se přesouvají z jedné generace do druhé na základě jejich životnosti. S delší životností se objekty přesunou do vyšší generace. Jak už bylo zmíněno dříve, u vyšších generací je uvolňování paměti méně často. Krátkodobé objekty vždy zůstávají ve generaci 0. Například objekty, na které se odkazuje během životnosti webového požadavku, jsou krátkodobé. Jednocinové úrovně aplikace se obecně migrují na generaci 2.
Když se ASP.NET Core aplikace, globální katalog:
- Vyhrazuje si paměť pro počáteční segmenty haldy.
- Při načtení modulu runtime potvrdí malou část paměti.
Předchozí přidělení paměti se provádí z důvodů výkonu. Výhoda výkonu pochází ze segmentů haldy v souvislé paměti.
Volání globálního katalogu. Shromažďovat
Volání globálního katalogu. Shromažďování explicitně:
- Nemělo by se to dělat v produkčních ASP.NET Core aplikacích.
- Je užitečný při zkoumání nevrácené paměti.
- Při zkoumání ověří, že uvolňování paměti odebralo z paměti všechny prochájené objekty, aby bylo možné paměť změřit.
Analýza využití paměti aplikace
S analýzou využití paměti vám můžou pomoct vyhrazené nástroje:
- Počítání odkazů na objekty
- Měření dopadu uvolňování paměti na využití procesoru
- Měření paměti využité pro každou generaci
K analýze využití paměti použijte následující nástroje:
- dotnet-trace:Lze použít na produkčních počítačích.
- Analýza využití paměti bez ladicího programu Visual Studio paměti
- Využití paměti profilu v sadě Visual Studio
Detekce problémů s pamětí
Správce úloh můžete použít k získání představu o tom, kolik paměti ASP.NET aplikace používá. Hodnota Správce úloh paměti:
- Představuje velikost paměti, kterou používá ASP.NET procesu.
- Zahrnuje živé objekty aplikace a další spotřebiteli paměti, jako je nativní využití paměti.
Pokud se Správce úloh hodnota paměti na neomezenou dobu a nikdy se nevyplodní, aplikace nevrácena do paměti. Následující části ukazují a vysvětlují několik vzorů využití paměti.
Ukázková aplikace využití paměti pro zobrazení
Ukázková aplikace MemoryLeak je dostupná na GitHub. Aplikace MemoryLeak:
- Zahrnuje diagnostický kontroler, který shromažďuje data paměti a globálního katalogu pro aplikaci v reálném čase.
- Má stránku Index, která zobrazuje paměť a data uvolňování paměti. Stránka Index se aktualizuje každou sekundu.
- Obsahuje kontroler rozhraní API, který poskytuje různé vzory zatížení paměti.
- Není ale podporovaný nástroj, ale můžete ho použít k zobrazení vzorců využití paměti pro ASP.NET Core aplikace.
Spusťte MemoryLeak. Přidělená paměť se pomalu zvyšuje, dokud nenastane uvolňování paměti. Paměť se zvyšuje, protože nástroj přiděluje vlastní objekt k zachytávání dat. Následující obrázek ukazuje stránku indexu MemoryLeak, když dojde ke globálnímu katalogu Gen 0. Graf zobrazuje 0 RPS (žádosti za sekundu), protože se nevolaly žádné koncové body rozhraní API z kontroleru rozhraní API.

Graf zobrazuje dvě hodnoty využití paměti:
- Přidělené: množství paměti zabíráné spravovanými objekty
- Pracovní sada:Sada stránek ve virtuálním adresním prostoru procesu, které aktuálně jsou ve fyzické paměti. Zobrazená pracovní sada je stejná jako Správce úloh zobrazení.
Přechodné objekty
Následující rozhraní API vytvoří 10kB instanci řetězce a vrátí ji klientovi. U každého požadavku je nový objekt přidělen v paměti a zapsán do odpovědi. Řetězce jsou v .NET uložené jako znaky UTF-16, takže každý znak přijímá 2 bajty v paměti.
[HttpGet("bigstring")]
public ActionResult<string> GetBigString()
{
return new String('x', 10 * 1024);
}
Následující graf se generuje s relativně malým zatížením v , aby bylo možné ukázat, jaký vliv má uvolňování paměti na přidělení paměti.

Předchozí graf ukazuje:
- 4 000 RPS (požadavky za sekundu)
- Kolekce uvolňování paměti generace 0 se vyskytují přibližně každé dvě sekundy.
- Pracovní sada je konstantní přibližně na 500 MB.
- Využití procesoru je 12 %.
- Spotřeba paměti a jejich uvolnění (prostřednictvím uvolňování paměti) je stabilní.
Následující graf ukazuje maximální propustnost, kterou může počítač zpracovat.

Předchozí graf ukazuje:
- 22 000 OT/min
- Kolekce uvolňování paměti generace 0 se vyskytují několikrát za sekundu.
- Kolekce 1. generace se spustí, protože aplikace přiděluje výrazně více paměti za sekundu.
- Pracovní sada je konstantní přibližně na 500 MB.
- Využití procesoru je 33 %.
- Spotřeba paměti a jejich uvolnění (prostřednictvím uvolňování paměti) je stabilní.
- Procesor (33 %) se nevyužívá příliš, a proto uvolňování paměti dokáže držet pod tíhou vysokého počtu přidělení.
Uvolňování paměti pracovní stanice vs. uvolňování paměti serveru
Systém uvolňování paměti .NET má dva různé režimy:
- Uvolňování paměti pracovní stanice: Optimalizované pro plochu.
- Server GC. Výchozí globální katalog pro ASP.NET Core aplikace. Optimalizováno pro server.
Režim uvolňování paměti lze nastavit explicitně v souboru projektu nebo v souboru runtimeconfig.jsna soubor publikované aplikace. Následující kód ukazuje nastavení ServerGarbageCollection v souboru projektu:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
Změna ServerGarbageCollection v souboru projektu vyžaduje opětovné sestavení aplikace.
Poznámka: Uvolňování paměti serveru není k dispozici na počítačích s jedním jádrem. Další informace naleznete v tématu IsServerGC.
Následující obrázek ukazuje profil paměti s 5 000 ot./min pomocí uvolňování paměti pracovní stanice.

Rozdíly mezi tímto grafem a verzí serveru jsou významné:
- Pracovní sada klesne z 500 MB na 70 MB.
- Uvolňování paměti v GC vyprší víckrát za sekundu místo každé dvě sekundy.
- Uvolňování paměti se zamítá z 300 MB na 10 MB.
V typických prostředích webového serveru je využití procesoru důležitější než paměť, proto je server GC lepší. Pokud je využití paměti vysoké a využití procesoru je poměrně nízké, může být UVOLŇOVÁNí paměti pracovní stanice více výkonné. Například vysoká hustota hostující několik webových aplikací, kde je paměť omezených.
GC s použitím Docker a malých kontejnerů
Když je na jednom počítači spuštěných víc aplikací s více kontejnery, může být uvolňování paměti v GC více než v GC serveru. Další informace najdete v tématu spuštění s uvolňováním paměti serveru v malém kontejneru a spuštění s uvolňováním paměti serveru v případě malých kontejnerů v části 1 – pevný limit pro haldu GC.
Trvalé odkazy na objekty
GC nemůže uvolnit objekty, na které se odkazuje. Objekty, na které se odkazuje, ale které už nepotřebují, mají za následek nevracení paměti. Pokud aplikace často přiděluje objekty a neuvolní je až po jejich nepotřebení, využití paměti se v průběhu času zvýší.
Následující rozhraní API vytvoří instanci řetězce 10 KB a vrátí ji klientovi. Rozdíl s předchozím příkladem je, že tato instance je odkazována statickým členem, což znamená, že není nikdy k dispozici pro shromažďová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;
}
Předchozí kód:
- Je příkladem typické nevrácení paměti.
- S častými voláními způsobí, že se paměť aplikace zvětšuje, dokud proces neselže s
OutOfMemoryvýjimkou.

Na předchozím obrázku:
- Zátěžové testování
/api/staticstring: koncový bod způsobuje lineární zvětšení paměti. - GC se pokusí uvolnit paměť, protože tlak paměti roste, voláním kolekce 2. generace.
- GC nemůže uvolnit nevrácenou paměť. Přidělená a pracovní sada se zvýšila s časem.
Některé scénáře, jako je například ukládání do mezipaměti, vyžadují, aby byly uloženy odkazy na objekty, dokud tlak vynutí uvolnění paměti. WeakReferenceTřídu lze použít pro tento typ kódu pro ukládání do mezipaměti. WeakReferenceObjekt je shromážděn v části tlaky paměti. Výchozí implementace IMemoryCache použití WeakReference .
Nativní paměť
Některé objekty .NET Core spoléhají na nativní paměť. UVOLŇOVÁNí paměti nelze shromáždit nativní paměť. Objekt .NET s použitím nativní paměti musí uvolnit pomocí nativního kódu.
Rozhraní .NET poskytuje IDisposable rozhraní, které vývojářům umožňuje uvolnit nativní paměť. I když Dispose není volána, správně implementované třídy volají Dispose při spuštění finalizační metody .
Vezměme si následující kód:
[HttpGet("fileprovider")]
public void GetFileProvider()
{
var fp = new PhysicalFileProvider(TempPath);
fp.Watch("*.*");
}
PhysicalFileProvider je spravovaná třída, takže všechny instance budou shromážděny na konci žádosti.
Následující obrázek ukazuje profil paměti při fileprovider průběžném vyvolání rozhraní API.

Předchozí graf znázorňuje zjevné problémy s implementací této třídy, protože zajišťuje zvýšení využití paměti. Jedná se o známý problém, který se sleduje v tomto problému.
Stejná netěsnost by mohla být provedena v uživatelském kódu, a to jedním z následujících způsobů:
- Neuvolňuje třídu správně.
- Forgetting k vyvolání
Disposemetody závislých objektů, které by měly být uvolněny.
Halda velkých objektů
Časté přiřazování paměti/bezplatné cykly můžou rozdělit paměť, zejména při přidělování velkých bloků paměti. Objekty jsou přidělovány v souvislém bloku paměti. Chcete-li zmírnit fragmentaci, když GC uvolní paměť, pokusí se ji defragmentovat. Tento proces se nazývá komprimace. Komprimace zahrnuje přesun objektů. Přesunutí velkých objektů ukládá snížení výkonu. Z tohoto důvodu GC vytvoří speciální zónu paměti pro velké objekty označované jako halda velkých objektů (LOH). Objekty, které jsou větší než 85 000 bajtů (přibližně 83 KB), jsou:
- Umístit do LOH.
- Není zkomprimováno.
- Shromážděno během generace 2 GC.
Když je LOH plný, GC spustí kolekci 2. generace. Kolekce 2. generace:
- Jsou ve své podstatě pomalé.
- Navíc se účtují náklady na aktivaci kolekce na všech ostatních generacích.
Následující kód komprimuje LOH hned:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
LargeObjectHeapCompactionModeInformace o komprimaci LOH najdete v tématu.
V kontejnerech pomocí .NET Core 3,0 a novějších se LOH automaticky zkomprimuje.
Toto chování ilustruje následující rozhraní API:
[HttpGet("loh/{size=85000}")]
public int GetLOH1(int size)
{
return new byte[size].Length;
}
Následující graf znázorňuje profil paměti volání /api/loh/84975 koncového bodu v rámci maximálního zatížení:

Následující graf znázorňuje profil paměti volajícího /api/loh/84976 koncového bodu, který přiděluje pouze jeden bajt:

Poznámka: byte[] Struktura má režijní bajty. To je důvod, proč 84 976 bajtů spouští limit 85 000.
Porovnání dvou předchozích grafů:
- Pracovní sada je pro oba scénáře podobná, přibližně 450 MB.
- V části požadavky LOH (84 975 bajtů) se zobrazí hlavně kolekce 0. generace.
- Požadavky over LOH generují kolekce konstant generace 2. Kolekce 2. generace jsou nákladné. Je potřeba více PROCESORů a propustnost se sníží téměř 50%.
Dočasné velké objekty jsou obzvláště problematické, protože způsobují Gen2 GC.
Pro maximální výkon by se mělo minimalizovat použití velkých objektů. Pokud je to možné, rozdělte velké objekty. například Response Ukládání do mezipaměti middleware v ASP.NET Core rozdělí položky mezipaměti do bloků menších než 85 000 bajtů.
následující odkazy znázorňují ASP.NET Core přístup k udržení objektů pod limitem LOH:
Další informace naleznete v tématu:
HttpClient
Nesprávné použití HttpClient může mít za následek nevracení prostředků. Systémové prostředky, jako jsou databázová připojení, sokety, popisovače souborů atd.:
- Je více omezených než paměť.
- Jsou více problematické při nevracení paměti.
Zkušení vývojáři rozhraní .NET znají volání Dispose objektů, které implementují IDisposable . Pokud nedojde k vyřazení objektů, které implementují, IDisposable obvykle dojde k nevrácené paměti nebo nevráceným systémovým prostředkům
HttpClientimplementuje IDisposable , ale nemělo by být uvolněno při každém vyvolání. Místo toho HttpClient by se mělo použít znovu.
Následující koncový bod vytvoří a odstraní novou HttpClient instanci na každém požadavku:
[HttpGet("httpclient1")]
public async Task<int> GetHttpClient1(string url)
{
using (var httpClient = new HttpClient())
{
var result = await httpClient.GetAsync(url);
return (int)result.StatusCode;
}
}
Při načtení se zaprotokolují následující chybové zprávy:
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)
I když HttpClient jsou instance vyřazené, vlastní síťové připojení bude nějakou dobu vydávat operačnímu systému. Průběžným vytvářením nových připojení dojde k vyčerpání portů . Každé připojení klienta vyžaduje svůj vlastní port klienta.
Jedním ze způsob, jak zabránit vyčerpání portů, je znovu použít stejnou HttpClient instanci:
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;
}
Instance HttpClient se uvolní, když se aplikace zastaví. Tento příklad ukazuje, že ne každý jednorázový prostředek by měl být po každém použití uvolněn.
Lepší způsob, jak zpracovat životnost instance, najdete v následujících HttpClient bodech:
Sdružování objektů
Předchozí příklad ukázal, jak HttpClient lze nastavit instanci jako statickou a znovu použít pro všechny požadavky. Opakované použití brání tomu, aby došly prostředky.
Sdružování objektů:
- Používá model opakovaného použití.
- Je navržený pro objekty, které je nákladné vytvořit.
Fond je kolekce předem inicializované objekty, které lze rezervovat a uvolnit napříč vlákny. Fondy mohou definovat pravidla přidělování, jako jsou limity, předdefinované velikosti nebo míra růstu.
Balíček NuGet Microsoft.Extensions.ObjectPool obsahuje třídy, které pomáhají takové fondy spravovat.
Následující koncový bod rozhraní API vytvoří instanci vyrovnávací paměti, která je u každého požadavku vyplněná byte náhodnými čísly:
[HttpGet("array/{size}")]
public byte[] GetArray(int size)
{
var random = new Random();
var array = new byte[size];
random.NextBytes(array);
return array;
}
Následující graf zobrazuje volání předchozího rozhraní API se středním zatížením:

V předchozím grafu dochází ke kolekcím generace 0 přibližně jednou za sekundu.
Předchozí kód lze optimalizovat sdružování vyrovnávací paměti byte pomocí <T> ArrayPool. Statická instance se opakovaně používá napříč požadavky.
Tento přístup se liší tím, že se z rozhraní API vrátí objekt ve fondu. To znamená:
- Objekt je mimo váš ovládací prvek, jakmile se vrátíte z metody .
- Objekt nelze uvolnit.
Nastavení vyřazení objektu:
- Zapouzdřte pole ve fondu do jednodušového objektu.
- Zaregistrujte objekt ve fondu pomocí třídy HttpContext.Response.RegisterForDispose.
RegisterForDispose se postará o volání cílového objektu tak, aby byl uvolněn až Dispose po dokončení požadavku 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;
}
Výsledkem použití stejného zatížení jako verze, která není ve fondu, je následující graf:

Hlavním rozdílem jsou přidělené bajty a v důsledku toho mnohem méně kolekcí generace 0.