Vítejte na blog.vyvojar.cz Přihlásit | Registrovat | Pomoc
Titulní Blogy Fotky Soubory

Petr Lazecký

Chorobovýplody

ROTOR - Struktura GC Heapu

Zajímali jste se někdy o to, jakým způsobem je organizován GC heap v CLR? Zde je výsledek mého bádání. GC objekty jsou alokovány z vyhrazeného souvislého bloku paměti. Čím jsou objekty starší, tím se nacházejí na vyšších adresách tohoto souvislého bloku paměti. Speciální ukazatel směřuje na místo, kde bude alokován další objekt. Čas od času dojde k úklidu nepoužitých objektů. Tím se uvolní na heapu nový prostor ale zároveň se vytvoří ostrůvky volného prostoru na heapu. Dále, čas od času dojde ke "smrštění" těchto volných ostrůvků (defragmentation). Defragmentace se neprovádí při každém úklidu haldy z výkonnostních důvodů. Objekty, zůstávají uloženy na zásobníku v pořadí, v jakém byly alokovány, a to i po defragmentaci. To zajišťuje dobrou lokalitu dat (v tom smyslu, že data, která spolu souvisí mohou být nahrána do souvislých bloků procesorové cache).

GC halda je organizována do dvou základních částí. Halda pro velké objekty (objekty větší než 85KB) a halda pro malé objekty. Dále je halda organizována to tří generací podle stáří objektu. Všechny objekty začínají svůj životní cyklus v generaci 0. Pokud objekt "přežije" úklidový cyklus (garbage collection) na haldě dojde k přesunutí objektu do vyšší generace. Maximální stáří, a tedy generace objektu je 2. Smyslem generací je frekvence úklidu objektů na GC haldě. Generace 0 je čištěna častěji než generace 1 a ta je zase čištěna častěji než generace 2. Halda pro velké objekty je čištěna společně s úklidem generace 2.

ROTOR kód obsahuje pouze dvě generace. Na základě tohoto kódu si lze udělat obrázek o struktuře haldy pro malé objekty (objekty menší než 85KB):

GC halda je rozdělena do segmentů. Segment je souvislý blok virtuálního adresového prostoru procesu. Velikost tohoto bloku je 16 MB (tato velikost se ovšem liší pro jednotlivé edice CLR (workstation, workstation 64 bit, server, server 64 bit).  Celý souvislý blok paměti pro každý nový segment je prvně pouze rezervován (reserve). Současně je první 4KB stránka z tohoto bloku paměti označena jako "commited". 

Operační systém Windows rozlišuje mezi těmito dvěma operacemi. Rezervovaná paměť (reserved) má pouze formu zápisu do patřičných systémových tabulek daného procesu. Teprve v okamžiku, kdy je paměťový blok označen jako "commited" memory je tato paměť spravována pomocí mechanismu virtuální paměti (pokud znáte pojmy jako page directory entry, page table entry či page direcoty tak máte představu, co mám na mysli).

Jeden ze segmentů je označen jako "prchavý" (emphemeral). Tento "prchavý" segment hostuje všechny objekty z nulté generace. Tak je tomu v kódu ROTOR. V případě "ostré" verze emphemeral segment hostuje jak objekty nulté generace, tak objekty generace 1 jak je vidět na výše uvedeném schématu.

Segment je rozdělen na hlavičku a tělo. Tělo obsahuje vlastní alokované objekty. Hlavička se skládá ze sady ukazatelů, které popisují jednotlivé části těla segmentu. "reserved" ukazuje na poslední platnou adresu segmentu. "reserved" je tedy sentinel, který je v kódu CLR použit pro detekci toho, zda je daný segment zaplněn. "commited" je poslední platná "commited" adresa. Adresy za tímto ukazatelem jsou označeny jako "reserved" a nejsou platné dokud je systém neoznačí jako "commited". "allocated" je za ideálních podmínek poslední adresa obsahující platný objekt. Tato hodnota souvisí s ukazatelem "plan_allocated". V případě, že živé objekty v daném segmentu tvoří souvislý blok (no fragmentation) obsahují ukazatelé "allocated" a "plan_allocated" stejnou hodnotu. Pokud při úklidu segmentu vzniknou mezi živými objekty "díry" volného prostoru, tak "plan_allocated" ukazuje na první volnou "díru". "next" ukazuje na další segment, jak je naznačeno na obrázku.

gc_heap je datová struktura definována v souboru Gcsmpriv.h. Tato struktura obsahuje velké množství ukazatelů, s nichž nejzajímavější je ukazatel na "finalizator" frontu, ukazatel na emphemeral segment a ukazatel na dvě optimalizační tabulky zvané "brick table" a "card table".

Card a brick tabulky tvoří tělo datové struktury card_table_info. Tyto tabulky definují mapu použití GC haldy. Každá struktura card_table_info popisuje daný blok paměti jehož počátek je dán hodnotou pole "lowest_address".

Card Table

Card table je pole DWORD prvků. Každý DWORD mapuje použití 4KB bloku paměti. To znamená, že jeden bit odpovídá 128 byte paměti. Kdykoliv dojde k změně reference na daný objekt, JITter emituje kód, který nastaví patřičný bit v daném DWORD prvku. To je tak vše, co se dá říci k této tabulce.

Brick Table

Card table mapuje použití paměti. "card table" umožňuje GC rychle najít místa v paměti, která se změnila od poslední GC. To je sice fajn, ale GC potřebuje mít nějaký mechanismus překladu mezi lineární adresou a daným bitem v card table. K tomuto účelu slouží brick table. Brick table je opět tvořena 16-bitovými hodnotami. Každá z těchto hodnot pokrývá 2KB z haldy. Pokud je hodnota v dané brick položce větší než nula, pak tato hodnota reprezentuje offset .NET objektu v daném 2KB segmentu. Pokud je tato hodnota záporná, pak tato hodnota udává počet brick položek, o které se je třeba vrátit zpět. Po tomto přesunu bychom se měli nacházet na brick položce s kladnou hodnotu a tedy s offsetem nového objektu. Tento mechanismus pokrývá situace, kdy daný .NET objekt pokrývá paměťový prostor několika 2KB bloků. Graficky jsem se pokusil znázornit tento mechanismus takto:

Červeně je vyznačen daný .NET objekt. Tento objekt je rozprostřen přes několik brick prvků. Počátek daného card prvku odpovídá jistému 4KB bloku paměti. Tento počátek také odpovídá počátku daného brick prvku. Po nalezení brick prvku pro 2KB v něm najdeme hodnotu -2. To znamená vrať se o dva brick bloky zpět. V tomto zpětném brick prvku najdeme kladnou hodnotu, které obsahuje offset počátku .NET objektu.

Optimalizace CLR 2.0

Správa GC paměti prošla v CLR 2.0 třemi zásadními změnami. Obě změny mají za cíl snížit fragmentování paměti. První změnou je tzv. "demotion". Tato situace pomůže tehdy, máme-li v nulté generaci nějaké "pinned" objekty. Pak se může stát, že mezi těmito "pinned" objekty je dost volného prostoru pro alokaci dalších objektů. V .NET 1.x "pinned" objekty přežily GC cyklus a počátek bloku pro generaci 0 byl posunut za poslední "pinned" objekt. To mělo za následek nevyužitý prostor mezi "pinned" objekty. Pokud byly toto objekty "pinned" dostatečně dlouhou dobu, byly postupně přesunuty do ještě vyšší generace spolu s "dírou" uvnitř. Situace byla horší o to více tehdy, pokud se spolu s "promotion" "pinned" objektů do vyšších generací objevily další, nové "pinned" objekty. Výsledkem byla fragmentovaná paměť.

"Demotion" se snaží tomuto procesu zabránit. Pokud je mezi "pinned" objekty v nulté generaci dostatek místa, tak GC neprovede přesun "pinned" objektů do vyšší generace, ale nechá je v původní nulté generaci. A to i přesto, že tyto objekty přežily úklid. Prostor mezi "pinned" objekty je pak použit pro alokaci nových objektů.

Druhým mechanismem snižující fragmentaci paměti je lepší správa segmentů. V předchozích verzích CLR platilo, že pokud nebylo v aktuálním ephemeral segmentu (segment, který hostuje objekty 1 a 0 generace) dostatek místa, byl vytvořen nový ephemeral segment a původní ephemeral segment se stal segmentem hostujícím 2 generaci objektu (první a druhá  generace v aktuální implementaci nikdy nepřesahuje jeden segment). CLR 2.0 se snaží vylepšit správu segmentů tak, že pokud je nutné alokovat nový segment pro generaci 1 a 0, GC se nejdříve pokusí najít existující segment hostující generaci 2, který má dostatek místa (je téměř prázdný). Místo alokace nové paměti se v takovémto případě provede záměna několika ukazatelů.

Posledním mechanismem správy paměti je funkce zvaná recyklace segmentů (VM hoarding). Tato funkce udržuje "stand-by" list uvolněných segmentů. Pokud GC potřebuje alokovat nový segment, zkontroluje nejdříve tento "stand-by" list a pokud je v tomto seznamu nějaký dostupný segment, použije jej. Tato funkce má ošetřit scénář, kdy se aplikace dostane do bodu, kdy dochází k častému vytváření a zároveň uvolňování segmentů. Například pokud aplikace nahrává a uvolňuje mnoho malých assembly.

Zveřejněno 6. září 2006 21:37 by lazo
Vedeno pod:

Upozornění na nové komentáře

Pokud chčeš dostávat upozornění emailem na změny u toho příspěvku,tak se zaregistruj zde.zde

Odebírat komentáře k tomuto příspěvku pomocí RSS

Komentář

 

Michal napsal:

staci uz jen prelozit a dat na MSDN. Skvely clanek
září 8, 2006 6:36
 

Patterns & practices napsal:

IDisposable je interface, který předepisuje implementovat jednu jedinou metodu: public interface IDisposable

února 6, 2007 8:07
 

Jirka napsal:

Skvělý článek. Velice děkuji. Pokud budete mít opět chuť a sílu, zajímaly by mě souvislosti ohledně souvislostí CLR, GC a multithreadingu.

Slyšel jsem, že i klasická jednothreadově napsaná aplikace pro CLR do jisté míry dokáže v případě vícejádrového hardware využít více, než jen jedno jádro, například pro úklid.

března 29, 2008 20:26
 

IDisposable – ResourceWrapper design pattern | HAVIT Knowledge Base napsal:

listopadu 8, 2013 14:51

Vytvoření nového komentáře

(povinný) 
(nepovinný)
(povinný) 
Opiš čísla, která vidíš na obrázku:
Odeslat
Powered by Community Server (Personal Edition), by Telligent Systems
Vyvojar.cz na prodej!