Základní informace o uvolňování paměti a nápovědy k výkonu
Rico Mariani
Microsoft Corporation
Dne
Shrnutí: Systém uvolňování paměti .NET poskytuje službu vysokorychlostního přidělování s dobrým využitím paměti a bez dlouhodobých problémů s fragmentací. Tento článek vysvětluje, jak fungují systém uvolňování paměti, a pak se věnuje některým problémům s výkonem, ke kterým může dojít v prostředí uvolňování paměti. (10 tištěných stránek)
Platí pro:
Microsoft® .NET Framework
Obsah
Úvod
Zjednodušený model
Shromažďování odpadků
Výkon
Dokončení
Závěr
Úvod
Abyste pochopili, jak správně využívat systém uvolňování paměti a jaké problémy s výkonem můžete narazit při spuštění v prostředí uvolňování paměti, je důležité porozumět základům fungování systému uvolňování paměti a vlivu těchto vnitřních funkcí na spuštěné programy.
Tento článek je rozdělen do dvou částí: Nejprve probereme povahu systému uvolňování paměti modulu CLR (Common Language Runtime) obecně pomocí zjednodušeného modelu a pak probereme některé dopady této struktury na výkon.
Zjednodušený model
Pro účely vysvětlení si představte následující zjednodušený model spravované haldy. Všimněte si, že toto není to, co se ve skutečnosti implementuje.
Obrázek 1: Zjednodušený model spravované haldy
Pravidla pro tento zjednodušený model jsou následující:
- Všechny objekty uvolňování paměti jsou přiděleny z jednoho souvislého rozsahu adresního prostoru.
- Halda je rozdělena do generací (více o tom později), takže je možné odstranit většinu odpadků tím, že se podíváte pouze na malý zlomek haldy.
- Všechny objekty v rámci jedné generace jsou zhruba stejně starší.
- Vyšší počet generací označuje oblasti haldy se staršími objekty – u těchto objektů je mnohem větší pravděpodobnost, že budou stabilní.
- Nejstarší objekty jsou na nejnižších adresách, zatímco nové objekty se vytvářejí na rostoucích adresách. (Adresy na obrázku 1 výše narůstají.)
- Ukazatel přidělení pro nové objekty označuje hranici mezi využitými (přidělenými) a nepoužívanými (volnými) oblastmi paměti.
- Halda se pravidelně komprimuje odebráním mrtvých objektů a posunutím živých objektů směrem k dolnímu konci adresy haldy. Tím se rozbalí nevyužitá oblast v dolní části diagramu, ve které se vytvářejí nové objekty.
- Pořadí objektů v paměti zůstává pořadím, ve kterém byly vytvořeny, pro dobrou lokalitu.
- Mezi objekty v haldě nikdy nejsou žádné mezery.
- Je potvrzeno pouze některé volné místo. V případě potřeby se ** z operačního systému získá více paměti v rozsahu rezervovaných adres.
Shromažďování odpadků
Nejjednodušším druhem kolekce pochopit je plně komprimující uvolňování paměti, takže začnu diskuzí o tom.
Úplné kolekce
V úplné kolekci musíme zastavit provádění programu a najít všechny kořeny v haldě uvolňování paměti. Tyto kořeny mají různé formy, ale nejvýrazněji se jedná o stack a globální proměnné, které ukazují na haldu. Počínaje kořeny, navštívíme každý objekt a sledujeme každý ukazatel objektu obsažený v každém navštívený objekt, který označuje objekty. Tímto způsobem kolektor najde všechny dostupné nebo živé objekty. Ostatní objekty, nedostupné , jsou nyní odsouzeny.
Obrázek 2. Kořeny do haldy GC
Jakmile jsou nedostupné objekty identifikovány, chceme tento prostor uvolnit pro pozdější použití; cílem kolektoru v tomto bodě je posunout živé objekty nahoru a eliminovat tak plýtvání prostorem. Když je provádění zastaveno, je bezpečné, aby kolektor všechny tyto objekty přesunul a opravil všechny ukazatele tak, aby vše bylo správně propojeno v novém umístění. Přeživší objekty se povyšují na číslo další generace (to znamená, že se aktualizují hranice generací) a provádění může pokračovat.
Částečné kolekce
Bohužel, úplné uvolňování paměti je prostě příliš drahé na to, aby se pokaždé, takže je vhodné prodiskutovat, jak nám generace v kolekci pomáhají.
Nejprve se podívejme na imaginární případ, kdy máme mimořádné štěstí. Předpokládejme, že nedávno proběhla úplná kolekce a halda je pěkně zhutněná. Provádění programu se obnoví a dojde k určitým přidělením. Ve skutečnosti dochází k velkému a velkému množství přidělení a po dostatečném přidělení systém správy paměti rozhodne, že je čas je shromáždit.
Tady máme štěstí. Předpokládejme, že po celou dobu, po kterou jsme byli spuštěni od poslední kolekce, jsme vůbec nezapisovali na žádný ze starších objektů, pouze nově přidělená generace nula (Gen0), do kterých byly objekty zapsány. Pokud by k tomu došlo, byli bychom ve skvělé situaci, protože můžeme proces uvolňování paměti masivně zjednodušit.
Místo našeho obvyklého úplného sběru můžeme jednoduše předpokládat, že všechny starší objekty (gen1, gen2) jsou stále živé – nebo jich je alespoň dost, že se na tyto objekty nevyplatí dívat. Navíc vzhledem k tomu, že žádný z nich nebyl napsán (pamatujete si, jak jsme šťastní?), neexistují žádné ukazatele ze starších objektů na novější objekty. To, co můžeme udělat, je podívat se na všechny kořeny jako obvykle, a pokud nějaké kořeny ukazují na staré objekty, prostě je ignorujte. Pro ostatní kořeny (ty, které ukazují na gen0) postupujeme jako obvykle a postupujeme podle všech ukazatelů. Kdykoli najdeme interní ukazatel, který se vrací zpět do starších objektů, ignorujeme ho.
Až tento proces dokončíme, navštívíme všechny živé objekty v Gen0 , aniž bychom navštívili objekty ze starších generací. Objekty gen0 pak mohou být odsouzeny jako obvykle a my vyklouzneme nahoru právě v této oblasti paměti, takže starší objekty zůstane beze klidu.
Teď je to pro nás opravdu skvělá situace, protože víme, že většina mrtvého prostoru bude pravděpodobně v mladších objektech, kde je velký počet změn. Mnoho tříd vytváří dočasné objekty pro své návratové hodnoty, dočasné řetězce a různé další třídy nástrojů, jako jsou enumerátory a whatnot. Pohled jen na gen0 nám dává snadný způsob, jak získat zpět většinu mrtvého prostoru tím, že se podíváme jen na velmi málo objektů.
Bohužel nemáme nikdy to štěstí, abychom tento přístup použili, protože alespoň některé starší objekty se musí změnit tak, aby ukazovaly na nové objekty. Pokud se to stane, nestačí je prostě ignorovat.
Práce generací s překážkami zápisu
Aby výše uvedený algoritmus skutečně fungoval, musíme vědět, které starší objekty byly změněny. K zapamatování umístění nezapsaných objektů používáme datovou strukturu označovanou jako tabulka karet a pro zachování této datové struktury kompilátor spravovaného kódu generuje tzv . překážky zápisu. Tyto dva pojmy jsou pro úspěch uvolňování paměti založeného na generaci zásadní.
Tabulku karet lze implementovat různými způsoby, ale nejjednodušší způsob, jak si ji představit, je jako pole bitů. Každý bit v tabulce karet představuje rozsah paměti na haldě – řekněme 128 bajtů. Pokaždé, když program zapíše objekt na nějakou adresu, musí kód bariéry pro zápis vypočítat, který blok o velikosti 128 bajtů byl zapsán, a pak nastavit odpovídající bit v tabulce karty.
Díky tomuto mechanismu se teď můžeme znovu vrátit k algoritmu shromažďování dat. Pokud provádíme uvolňování paměti Gen0 , můžeme použít algoritmus, jak je popsáno výše, ignorovat všechny ukazatele na starší generace, ale jakmile to uděláme, musíme také najít každý ukazatel objektu v každém objektu, který leží na bloku, který byl označen jako upravený v tabulce karty. Musíme s nimi zacházet jako s kořeny. Pokud vezmeme v úvahu i tyto ukazatele, pak správně shromáždíme pouze objekty gen0 .
Tento přístup by vůbec nepomohl, kdyby byla tabulka karet vždy plná, ale v praxi se ve skutečnosti změnilo poměrně málo ukazatelů ze starších generací, takže tento přístup výrazně šetří.
Výkon
Teď, když máme základní model toho, jak věci fungují, zvažme některé věci, které by se mohly pokazit a které by mohly zpomalit. To nám poskytne dobrou představu, jaké druhy věcí bychom se měli snažit vyhnout, abychom ze sběrače získali nejlepší výkon.
Příliš mnoho přidělení
To je opravdu nejzásadnější věc, která se může pokazit. Přidělení nové paměti s uvolňováním paměti je opravdu poměrně rychlé. Jak vidíte na obrázku 2 výše, vše, co se obvykle musí stát, je přesunutí ukazatele přidělení, aby se vytvořil prostor pro nový objekt na straně přiděleno – to není o moc rychlejší. Dříve nebo později ale musí dojít ke sběru odpadků, a když je všechno stejné, je lepší, aby se to stalo později než dříve. Proto chcete mít jistotu, že když vytváříte nové objekty, je to opravdu nezbytné a vhodné, i když vytvoření jenom jednoho objektu je rychlé.
To může znít jako zřejmá rada, ale ve skutečnosti je velmi snadné zapomenout, že jeden malý řádek kódu, který napíšete, může aktivovat velké množství přidělení. Předpokládejme například, že píšete nějakou porovnávací funkci a předpokládáte, že objekty mají pole s klíčovými slovy a že chcete, aby se při porovnání nerozlišovaly malá a velká písmena u klíčových slov v daném pořadí. Teď v tomto případě nemůžete jednoduše porovnat celý řetězec klíčových slov, protože první klíčové slovo může být velmi krátké. Bylo by lákavé použít String.Split k rozdělení řetězce klíčového slova na části a pak porovnat jednotlivé části v pořadí pomocí normálního porovnání nerozlišující malá a velká písmena. Zní to skvěle, že?
No, jak se ukazuje, dělat to takhle není tak dobrý nápad. Vidíte, String.Split vytvoří pole řetězců, což znamená jeden nový objekt řetězce pro každé klíčové slovo původně v řetězci klíčových slov a další objekt pro pole. Jejda! Pokud to děláme v kontextu řazení, je to hodně porovnání a vaše dvouřádová porovnávací funkce teď vytváří velmi velký počet dočasných objektů. Najednou bude odpadkový sběrač pracovat velmi tvrdě za vás, a dokonce i s nejchytřejším sběrem schématu je jen spousta odpadků, které je třeba vyčistit. Lepší je napsat porovnávací funkci, která přidělení vůbec nevyžaduje.
přidělení Too-Large
Při práci s tradičním alokátorem, jako je malloc(), programátoři často píší kód, který provede co nejméně volání malloc(), protože vědí, že náklady na přidělení jsou poměrně vysoké. To se projeví v praxi přidělování v blocích, často spekulativním přidělováním objektů, které bychom mohli potřebovat, abychom mohli provést méně celkových přidělení. Předem přidělené objekty se pak ručně spravují z nějakého fondu, čímž se ve skutečnosti vytvoří druh vysokorychlostního vlastního alokátoru.
Ve spravovaném světě je tento postup mnohem méně přesvědčivý z několika důvodů:
Za prvé, náklady na alokaci jsou velmi nízké – neexistuje vyhledávání volných bloků jako u tradičních alokátorů; Vše, co se musí stát, je hranice mezi volnými a přidělenou oblastí, která se musí přesunout. Nízké náklady na přidělení znamenají, že nejpřesvědčivější důvod pro sdružování prostě neexistuje.
Za druhé, pokud se rozhodnete předem přidělit, budete samozřejmě provádět více přidělení, než je vyžadováno pro vaše okamžité potřeby, což by pak mohlo vynutit další uvolňování paměti, které by jinak mohlo být zbytečné.
Systém uvolňování paměti nebude moct uvolnit místo pro objekty, které ručně recyklujete, protože z globálního hlediska jsou všechny tyto objekty, včetně těch, které se aktuálně nepoužívají, stále aktivní. Možná zjistíte, že velké množství paměti se plýtvají udržováním objektů připravených k použití, ale ne při použití po ruce.
To neznamená, že předběžné přidělování je vždycky špatný nápad. Můžete to například chtít udělat, abyste vynutili, aby byly určité objekty původně přiděleny společně, ale pravděpodobně zjistíte, že je méně přesvědčivá jako obecná strategie, než by byla v nespravovaném kódu.
Příliš mnoho ukazatelů
Pokud vytvoříte datovou strukturu, která je velkou sítí ukazatelů, budete mít dva problémy. Za prvé bude existovat mnoho zápisů objektů (viz obrázek 3 níže) a za druhé, když přijde čas shromáždit tuto datovou strukturu, přinutíte systém uvolňování paměti, aby sledoval všechny tyto ukazatele a v případě potřeby je všechny měnily, jak se věci pohybují. Pokud je vaše datová struktura dlouhodobá a moc se nezmění, bude kolektor muset všechny tyto ukazatele navštívit, až dojde k úplnému shromažďování (na úrovni Gen2 ). Ale pokud vytvoříte takovou strukturu na přechodném základě, například v rámci zpracování transakcí, pak budete platit náklady mnohem častěji.
Obrázek 3: Datová struktura je velmi zatížená ukazateli
Datové struktury, které jsou náročné na ukazatele, můžou mít také jiné problémy, které nesouvisejí s časem uvolňování paměti. Opět platí, že jak jsme si řekli dříve, při vytváření objektů se přidělují souvisle v pořadí přidělení. To je skvělé, pokud vytváříte velkou, možná složitou datovou strukturu, například obnovením informací ze souboru. I když máte různorodé datové typy, všechny objekty budou v paměti blízko sebe, což procesoru pomůže mít k těmto objektům rychlý přístup. Jak ale čas uplyne a změní se struktura dat, bude pravděpodobně potřeba ke starým objektům připojit nové objekty. Tyto nové objekty budou vytvořeny mnohem později, takže se nebudou blížit původním objektům v paměti. I když systém uvolňování paměti komprimuje vaši paměť, vaše objekty nebudou promíchané v paměti, pouze se "posunou" dohromady, aby se odstranilo zbytečné místo. Výsledná porucha se může časem tak zhoršovat, že budete chtít vytvořit novou kopii celé datové struktury, vše pěkně zabalené, a nechat starý neuspořádaný, aby byl včas odsouzen sběratelem.
Příliš mnoho kořenů
Odpadkový sběrač musí samozřejmě kořeny při sběru ošetřovat – vždy je třeba je vyčíslit a řádně zvážit. Kolekce Gen0 může být rychlá jenom do té míry, že jí nedáte záplavu kořenů ke zvážení. Pokud byste chtěli vytvořit hluboce rekurzivní funkci, která má mezi místními proměnnými mnoho ukazatelů na objekt, může být výsledek ve skutečnosti poměrně nákladný. Tyto náklady vznikají nejen v případě, že je třeba vzít v úvahu všechny tyto kořeny, ale také u mimořádně velkého počtu objektů gen0 , které by tyto kořeny mohly udržovat naživu ne příliš dlouho (viz níže).
Příliš mnoho zápisů objektů
Ještě jednou odkazujeme na předchozí diskuzi, mějte na paměti, že pokaždé, když spravovaný program změní ukazatel objektu, aktivuje se také kód bariéry pro zápis. To může být špatné ze dvou důvodů:
Zaprvé, náklady na bariéru pro zápis můžou být srovnatelné s náklady na to, co jste se snažili udělat. Pokud například provádíte jednoduché operace v nějaké třídě enumerátoru, můžete zjistit, že budete muset přesunout některé klíčové ukazatele z hlavní kolekce do enumerátoru v každém kroku. To je vlastně něco, čemu byste se mohli chtít vyhnout, protože v podstatě zdvojnásobíte náklady na kopírování těchto ukazatelů kolem kvůli bariérě pro zápis a možná to budete muset udělat jednou nebo vícekrát za každou smyčku na enumerátoru.
Za druhé, aktivace překážek zápisu je až tak špatná, pokud ve skutečnosti píšete na starších objektech. Při úpravě starších objektů efektivně vytváříte další kořeny ke kontrole (probírané výše), když dojde k dalšímu uvolňování paměti. Pokud byste upravili dostatek starých objektů, v podstatě byste negovali obvyklá vylepšení rychlosti spojená se shromažďováním pouze nejmladší generace.
Tyto dva důvody jsou samozřejmě doplněny obvyklými důvody, proč neděláte příliš mnoho zápisů v jakémkoli druhu programu. Když je všechno stejné, je lepší se dotýkat menšího množství paměti (čtení nebo zápisu), aby se mezipaměť procesoru lépe využívala.
Příliš mnoho objektů s téměř dlouhou životností
A konečně, možná největší úskalí generačního systému uvolňování paměti je vytvoření mnoha objektů, které nejsou ani přesně dočasné, ani nejsou přesně dlouhodobé. Tyto objekty můžou způsobit spoustu potíží, protože je nevyčistí kolekce Gen0 (nejlevnější), protože budou stále nutné a mohou dokonce přežít i kolekci1 . generace, protože se stále používají, ale brzy po tom zemřou.
Problém je v tom, že jakmile se objekt dostane na úroveň Gen2 , zbaví se ho pouze úplná kolekce a úplné kolekce jsou dostatečně nákladné, aby je systém uvolňování paměti zpožďoval tak dlouho, jak je to možné. Výsledkem mnoha "téměř dlouhodobých" objektů je, že vaše Gen2 bude mít tendenci růst, potenciálně alarmující rychlostí; nemusí se vyčistit téměř tak rychle, jak byste chtěli, a když se vyčistí, bude to určitě mnohem nákladnější, než byste si přáli.
Pokud se chcete těmto druhům objektů vyhnout, vaše nejlepší linie ochrany budou vypadat takto:
- Přidělte co nejméně objektů s náležitou pozorností na množství dočasného místa, které používáte.
- Zachovejte velikost objektů s delší životností na minimum.
- Udržujte v zásobníku co nejméně ukazatelů na objekt (to jsou kořeny).
Pokud to uděláte, vaše kolekce Gen0 budou pravděpodobně vysoce efektivní a gen1 nebude růst příliš rychle. V důsledku toho mohou být kolekce Gen1 prováděny méně často, a pokud je rozumné provést kolekci gen1 , vaše objekty střední životnosti již budou mrtvé a bude možné je levně obnovit.
Pokud se věci dějí skvěle, pak během operací stabilního stavu se velikost Gen2 vůbec nezvětší!
Dokončení
Teď, když jsme se zjednodušeným modelem přidělování probrali několik témat, chtěl bych to trochu zkomplikovat, abychom mohli prodiskutovat ještě jeden důležitý jev, a tou jsou náklady na finalizační metody a finalizaci. Stručně řečeno, finalizační prostředek může být přítomen v libovolné třídě – je to volitelný člen, který systém uvolňování paměti přislíbí volání u jinak mrtvých objektů předtím, než uvolní paměť pro tento objekt. V jazyce C# použijete syntaxi ~Class k určení finalizační metody.
Vliv finalizace na kolekci
Když systém uvolňování paměti poprvé narazí na objekt, který je jinak mrtvý, ale stále je třeba ho dokončit, musí opustit svůj pokus uvolnit místo pro tento objekt v daném okamžiku. Objekt je místo toho přidán do seznamu objektů, které potřebují finalizaci, a kromě toho musí kolektor zajistit, aby všechny ukazatele v objektu zůstaly platné, dokud finalizace není dokončena. To je v podstatě totéž jako tvrzení, že každý objekt, který potřebuje finalizaci, je z pohledu kolektoru jako dočasný kořenový objekt.
Jakmile je kolekce dokončena, vlákno finalizace s výstižným názvem projde seznamem objektů, které potřebují finalizaci, a vyvolá finalizační metody. Po dokončení se objekty opět stanou mrtvými a budou přirozeně shromážděny normálním způsobem.
Finalizace a výkon
Díky tomuto základnímu pochopení finalizace už můžeme odvodit některé velmi důležité věci:
Za prvé, objekty, které potřebují finalizaci, žijí déle než objekty, které je nemají. Ve skutečnosti mohou žít mnohem déle. Předpokládejme například, že je potřeba dokončit objekt, který je vgen2 . Dokončení bude naplánováno, ale objekt je stále v gen2, takže se znovu neshromáždí, dokud nedojde k další kolekciGen2 . To může být opravdu velmi dlouhá doba, a ve skutečnosti, pokud se věci daří, bude to dlouhá doba, protože kolekce Gen2 jsou nákladné, a proto chceme , aby se děly velmi zřídka. Starší objekty, které potřebují finalizaci, můžou před uvolněním místa čekat na desítky, ne-li stovky kolekcí gen0 .
Za druhé, objekty, které potřebují finalizaci, způsobují vedlejší škody. Vzhledem k tomu, že interní ukazatele na objekty musí zůstat platné, zůstanou v paměti nejen objekty, které vyžadují přímou finalizaci, ale vše, na co objekt odkazuje, přímo i nepřímo, také zůstane v paměti. Pokud byl obrovský strom objektů ukotvený jedním objektem, který vyžadoval dokončení, celý strom by zůstal, potenciálně po dlouhou dobu, jak jsme právě probírali. Proto je důležité používat finalizační metody střídmě a umístit je na objekty, které mají co nejméně interních ukazatelů na objekty. V příkladu stromu, který jsem právě uvedl, můžete snadno zabránit problému přesunutím prostředků, které potřebují finalizaci, do samostatného objektu a zachováním odkazu na tento objekt v kořenu stromu. S těmito skromnými změnami by zůstal pouze jeden objekt (doufejme, že pěkný malý objekt) a náklady na finalizaci jsou minimalizovány.
Nakonec objekty, které potřebují finalizaci, vytvoří práci pro vlákno finalizační metody. Pokud je proces finalizace složitý, jedno a jediné vlákno finalizační metody bude trávit spoustu času prováděním těchto kroků, což může způsobit backlog práce a tím způsobit, že více objektů zůstane čekání na dokončení. Proto je zásadně důležité, aby finalizační metody dělaly co nejmenší množství práce. Mějte také na paměti, že i když všechny ukazatele na objekt zůstávají během finalizace platné, může se stát, že tyto ukazatele vedou k objektům, které již byly finalizovány, a proto mohou být méně užitečné. Obecně je nejbezpečnější vyhnout se následujícím ukazatelům na objekt v finalizačním kódu, i když jsou ukazatele platné. Nejlepší je bezpečná a krátká cesta ke kódu pro finalizaci.
IDisposable a Dispose
V mnoha případech je možné, aby objekty, které by jinak vždy bylo nutné dokončit, aby se zabránilo těmto nákladům implementací rozhraní IDisposable . Toto rozhraní poskytuje alternativní metodu pro uvolnění prostředků, jejichž životnost je dobře známa programátorovi, a to se ve skutečnosti stává docela dost. Samozřejmě je to ještě lepší, pokud vaše objekty jednoduše používají pouze paměť, a proto nevyžadují žádné finalizace nebo likvidaci vůbec; Ale pokud je finalizace nutná a existuje mnoho případů, kdy explicitní správa objektů je snadná a praktická, pak implementace rozhraní IDisposable je skvělý způsob, jak se vyhnout, nebo alespoň snížit, finalizace nákladů.
V jazyce C# může být tento vzor docela užitečný:
class X: IDisposable
{
public X(…)
{
… initialize resources …
}
~X()
{
… release resources …
}
public void Dispose()
{
// this is the same as calling ~X()
Finalize();
// no need to finalize later
System.GC.SuppressFinalize(this);
}
};
Kde ruční volání Dispose obviňuje potřebu kolektoru udržovat objekt naživu a volat finalizační metodu.
Závěr
Systém uvolňování paměti .NET poskytuje službu vysokorychlostního přidělování s dobrým využitím paměti a bez dlouhodobých problémů s fragmentací, je však možné dělat věci, které vám poskytnou mnohem méně než optimální výkon.
Pokud chcete alokátor co nejlépe použít, měli byste zvážit postupy, jako jsou tyto:
- Přidělte veškerou paměť (nebo co nejvíce), která se má použít s danou datovou strukturou současně.
- Odeberte dočasná přidělení, kterým se můžete vyhnout s minimálními penalizacemi ve složitosti.
- Minimalizujte počet zápisů ukazatelů na objekt, zejména těch, které se provádějí na starší objekty.
- Snižte hustotu ukazatelů v datových strukturách.
- Používejte pouze omezené finalizační metody a pak pouze na "listových" objektech, jak je to možné. V případě potřeby přerušte objekty, které vám s tím pomohou.
Pravidelný postup kontroly klíčových datových struktur a provádění profilů využití paměti pomocí nástrojů, jako je alokační profiler, bude dlouze udržovat efektivní využití paměti a systém uvolňování paměti pro vás bude fungovat co nejlépe.