Programování

Optimalizace výkonu JVM, část 3: Sběr odpadu

Mechanismus sběru odpadků platformy Java výrazně zvyšuje produktivitu vývojáře, ale špatně implementovaný sběrač odpadků může nadměrně spotřebovávat prostředky aplikace. V tomto třetím článku v Optimalizace výkonu JVM série, Eva Andreasson nabízí začátečníkům Java přehled paměťového modelu platformy Java a mechanismu GC. Poté vysvětlí, proč je fragmentace (a ne GC) hlavní „mám to!“ výkonu Java aplikací a proč je generační sběr a zhutňování odpadu v současnosti hlavním (i když ne nejinovativnějším) přístupem ke správě fragmentace haldy v aplikacích Java.

Sběr odpadu (GC) je proces, jehož cílem je uvolnit obsazenou paměť, na kterou již neodkazuje žádný dosažitelný objekt Java, a je nezbytnou součástí systému pro správu dynamické paměti Java Virtual Machine (JVM). V typickém cyklu uvolňování paměti jsou zachovány všechny objekty, na které se stále odkazuje, a jsou tak dosažitelné. Místo obsazené dříve odkazovanými objekty je uvolněno a uvolněno, aby bylo možné přidělit nové objekty.

Abychom pochopili sběr odpadků a různé přístupy a algoritmy GC, musíte nejprve vědět několik věcí o paměťovém modelu platformy Java.

Optimalizace výkonu JVM: Přečtěte si sérii

  • Část 1: Přehled
  • Část 2: Překladače
  • Část 3: Sběr odpadu
  • Část 4: Současné zhutňování GC
  • Část 5: Škálovatelnost

Odpadky a paměťový model platformy Java

Když zadáte možnost spuštění -Xmx na příkazovém řádku vaší aplikace Java (například: java -Xmx: 2g MyApp) paměť je přiřazena procesu Java. Tato paměť se označuje jako Halda Java (nebo prostě halda). Toto je vyhrazený adresní prostor paměti, kam budou přiděleny všechny objekty vytvořené vaším programem Java (nebo někdy JVM). Vzhledem k tomu, že váš program Java běží a přiděluje nové objekty, hromada Java (což znamená adresní prostor) se zaplní.

Nakonec bude hromada prostředí Java plná, což znamená, že podproces přidělení není schopen najít dostatečně velkou po sobě jdoucí část volné paměti pro objekt, který chce přidělit. V tomto okamžiku JVM určuje, že je třeba provést uvolnění paměti, a upozorní na to uvolnění paměti. Uvolňování paměti lze také spustit při volání programu Java System.gc (). Použitím System.gc () nezaručuje odvoz odpadu. Než bude možné spustit jakékoli uvolňování paměti, mechanismus GC nejprve určí, zda je bezpečné jej spustit. Je bezpečné spustit uvolňování paměti, když jsou všechna aktivní vlákna aplikace na bezpečném místě, aby to umožňovaly, např. jednoduše vysvětlil, že by bylo špatné začít sbírat odpadky uprostřed probíhající alokace objektu nebo uprostřed provádění sekvence optimalizovaných instrukcí CPU (viz můj předchozí článek o kompilátorech), protože byste mohli ztratit kontext a tím pokazit konec Výsledek.

Popelář by měl nikdy získat zpět aktivně odkazovaný objekt; to by porušilo specifikaci virtuálního stroje Java. Sběratel odpadků také není povinen okamžitě shromažďovat mrtvé objekty. Mrtvé objekty se nakonec shromažďují během následujících cyklů uvolňování paměti. I když existuje mnoho způsobů, jak implementovat odvoz odpadu, tyto dva předpoklady platí pro všechny odrůdy. Skutečnou výzvou pro uvolňování paměti je identifikovat vše, co je živé (stále odkazované), a získat zpět jakoukoli neodkazovanou paměť, ale bez ovlivnění spuštěných aplikací více, než je nutné. Sběratel odpadků má tedy dva mandáty:

  1. Chcete-li rychle uvolnit neodkazovanou paměť, abyste uspokojili rychlost alokace aplikace, aby nedocházelo z paměti.
  2. Chcete-li získat zpět paměť při minimálním dopadu na výkon (např. Latenci a propustnost) spuštěné aplikace.

Dva druhy sběru odpadu

V prvním článku v této sérii jsem se dotkl dvou hlavních přístupů k uvolňování paměti, kterými jsou sběratelé počítání a trasování. Tentokrát rozeberu podrobněji každý přístup a poté představím některé z algoritmů použitých k implementaci sledovacích kolektorů v produkčním prostředí.

Přečtěte si řadu optimalizace výkonu JVM

  • Optimalizace výkonu JVM, část 1: Přehled
  • Optimalizace výkonu JVM, Část 2: Překladače

Sběratelé počítání referencí

Sběratelé počítání referencí sledovat, kolik odkazů směřuje na každý objekt Java. Jakmile se počet objektů stane nulovým, lze paměť okamžitě znovu získat. Tento okamžitý přístup k regenerované paměti je hlavní výhodou přístupu počítání referencí k uvolňování paměti. Existuje jen velmi málo režie, pokud jde o přidržení neodkázané paměti. Udržování všech počtů referencí v aktuálním stavu však může být docela nákladné.

Hlavním problémem sběratelů počítání referencí je udržování přesných počtů referencí. Další dobře známou výzvou je složitost spojená s manipulací s kruhovými strukturami. Pokud se dva objekty navzájem odkazují a žádný živý objekt na ně neodkazuje, jejich paměť nebude nikdy uvolněna. Oba objekty navždy zůstanou s nenulovým počtem. Rekultivace paměti spojené s kruhovými strukturami vyžaduje velkou analýzu, která přináší nákladnou režii algoritmu, a tedy i aplikaci.

Trasování sběratelů

Trasování sběratelů jsou založeny na předpokladu, že všechny živé objekty lze najít iterativním sledováním všech odkazů a následných odkazů z počáteční sady známých jako živé objekty. Počáteční sada živých objektů (tzv kořenové objekty nebo prostě kořeny zkráceně) jsou umístěny analýzou registrů, globálních polí a rámců zásobníku v okamžiku, kdy je spuštěna garbage collection. Poté, co byla identifikována počáteční živá sada, sledovací kolektor sleduje odkazy z těchto objektů a zařadí je do fronty, aby byly označeny jako živé, a následně nechají jejich odkazy trasovat. Označení všech nalezených odkazovaných objektů žít znamená, že známá živá sestava se časem zvyšuje. Tento proces pokračuje, dokud nebudou nalezeny a označeny všechny odkazované (a tedy všechny živé) objekty. Jakmile kolektor trasování najde všechny živé objekty, získá zpět zbývající paměť.

Trasovací kolektory se od kolektorů pro počítání referencí liší v tom, že mohou zpracovávat kruhové struktury. Úlovkem u většiny sledovacích kolektorů je fáze značení, což znamená čekání, než bude možné získat zpět neodkazovanou paměť.

Trasovací kolektory se nejčastěji používají pro správu paměti v dynamických jazycích; jsou zdaleka nejběžnější pro jazyk Java a byly komerčně ověřeny v produkčním prostředí po mnoho let. Ve zbývající části tohoto článku se zaměřím na trasování kolektorů, počínaje některými algoritmy, které implementují tento přístup k uvolňování paměti.

Trasování sběratelských algoritmů

Kopírování a označit a zamést Garbage Collection nejsou nové, ale stále jsou to dva nejběžnější algoritmy, které dnes implementují trasování Garbage Collection.

Kopírování sběratelů

Tradiční kopírovací sběratelé používají a z vesmíru a do vesmíru - tj. dva samostatně definované adresní prostory haldy. V místě uvolňování paměti se živé objekty v oblasti definované jako z vesmíru zkopírují do dalšího dostupného prostoru v oblasti definované jako prostor. Když jsou všechny živé objekty v prostoru z vesmíru přesunuty, lze celý prostor znovu získat. Když alokace začíná znovu, začíná od prvního volného místa v prostoru.

Ve starších implementacích tohoto algoritmu se přepínají místa z prostoru a do prostoru, což znamená, že když je prostor do prostoru plný, znovu se spustí sběr odpadků a prostor se stane prostorem, jak je znázorněno na obrázku 1.

Modernější implementace kopírovacího algoritmu umožňují přiřazení libovolných prostorů adres v rámci haldy jako do prostoru a z prostoru. V těchto případech nemusí nutně navzájem měnit polohu; spíše se každý stane dalším adresním prostorem v haldě.

Jednou z výhod kopírování kolektorů je, že objekty jsou v prostoru těsně přidělovány společně, což zcela eliminuje fragmentaci. Fragmentace je běžný problém, se kterým se potýkají jiné algoritmy sběru odpadu; něco, o čem budu diskutovat dále v tomto článku.

Nevýhody kopírování sběratelů

Kopírující sběratelé jsou obvykle sběratelé stop-the-world, což znamená, že po celou dobu, kdy je cyklus uvolňování paměti, nelze provádět žádnou práci s aplikacemi. V implementaci stop-the-world platí, že čím větší oblast budete muset kopírovat, tím vyšší bude dopad na výkon vaší aplikace. To je nevýhoda pro aplikace, které jsou citlivé na dobu odezvy. S kopírovacím sběratelem musíte také zvážit nejhorší scénář, kdy je vše živé z vesmíru. Vždy musíte ponechat dostatek světlého prostoru pro přesun živých předmětů, což znamená, že vesmír musí být dostatečně velký, aby hostil vše v prostoru. Algoritmus kopírování je kvůli tomuto omezení mírně paměťově neefektivní.

Mark-and-sweep collectors

Většina komerčních prostředí JVM nasazených v podnikových produkčních prostředích používá kolektory označující a zametající (nebo označující), které nemají dopad na výkon jako kopírující kolektory. Mezi nejznámější sběrače značení patří CMS, G1, GenPar a DeterministicGC (viz zdroje).

A sběrač značek a zametání sleduje odkazy a označuje každý nalezený objekt bitem „live“. Obvykle sada bitů odpovídá adrese nebo v některých případech sadě adres na haldě. Živý bit může být například uložen jako bit v záhlaví objektu, bitovém vektoru nebo bitové mapě.

Poté, co bylo vše označeno živě, nastartuje fáze zametání. Pokud má kolektor fázi zametání, v zásadě obsahuje nějaký mechanismus pro opětovné procházení haldy (nejen živou sadu, ale celou délku haldy) k vyhledání všech neznačených bloky po sobě jdoucích paměťových adresních prostorů. Neoznačená paměť je volná a obnovitelná. Sběratel poté spojí tyto neoznačené bloky do organizovaných bezplatných seznamů. Ve sběrači odpadků mohou být různé bezplatné seznamy - obvykle uspořádané podle velikostí bloků. Některé JVM (například JRockit Real Time) implementují kolektory s heuristikou, které dynamicky vytvářejí seznamy velikostí na základě dat profilování aplikace a statistik velikosti objektu.

Když je fáze zametání dokončena, přidělování začne znovu. Nové alokační oblasti jsou přidělovány z bezplatných seznamů a bloky paměti lze přizpůsobit velikostem objektů, průměrům velikosti objektu na ID vlákna nebo aplikačně vyladěným velikostem TLAB. Přizpůsobení volného místa přesněji velikosti toho, co se vaše aplikace pokouší přidělit, optimalizuje paměť a může pomoci snížit fragmentaci.

Více informací o velikostech TLAB

Rozdělení TLAB a TLA (Thread Local Allocation Buffer nebo Thread Local Area) jsou popsány v optimalizaci výkonu JVM, část 1.

Nevýhody sběratelů značek a zametání

Fáze označování závisí na množství živých dat na vaší haldě, zatímco fáze rozmítání závisí na velikosti haldy. Vzhledem k tomu, že musíte počkat, až oba označit a zametat fáze jsou plné pro získání paměti, tento algoritmus způsobuje problémy s pauzou pro větší hromady a větší živé datové sady.

Jedním ze způsobů, jak můžete značně pomoci aplikacím náročným na paměť, je použití možností ladění GC, které vyhoví různým scénářům a potřebám aplikací. Ladění může v mnoha případech pomoci alespoň odložit kteroukoli z těchto fází, aby se nestala rizikem pro vaši aplikaci nebo smlouvy na úrovni služeb (SLA). (SLA specifikuje, že aplikace bude splňovat určité doby odezvy aplikace - tj. Latenci.) Ladění pro každou změnu zatížení a modifikaci aplikace je opakující se úkol, protože ladění je platné pouze pro konkrétní pracovní zátěž a rychlost přidělení.

Implementace značkování

Existují nejméně dva komerčně dostupné a osvědčené přístupy k implementaci sběru značek. Jedním z nich je paralelní přístup a druhým je souběžný (nebo většinou souběžný) přístup.

Paralelní kolektory

Paralelní sběr znamená, že prostředky přiřazené procesu se používají paralelně za účelem uvolnění paměti. Většina komerčně implementovaných paralelních kolektorů jsou monolitické stop-the-world kolektory - všechna vlákna aplikací jsou zastavena, dokud není dokončen celý cyklus uvolňování paměti. Zastavení všech vláken umožňuje, aby byly všechny prostředky efektivně využívány paralelně k dokončení uvolňování paměti prostřednictvím fází značky a zametání. To vede k velmi vysoké úrovni efektivity, což obvykle vede k vysokému skóre v benchmarcích propustnosti, jako je SPECjbb. Pokud je pro vaši aplikaci nezbytná propustnost, je vynikající volbou paralelní přístup.