Zlepšete výkon aplikací pomocí těchto pokročilých technik GC

Výkon aplikací je v popředí našeho zájmu a optimalizace Garbage Collection je vhodným místem pro malé, ale významné pokroky

Automatický garbage collection (spolu s JIT HotSpot Compiler) je jednou z nejpokročilejších a nejoceňovanějších součástí JVM, ale mnoho vývojářů a inženýrů je mnohem méně obeznámeno s Garbage Collection (GC), jeho fungováním a vlivem na výkon aplikací.

Předně, k čemu vůbec GC slouží? Garbage collection je proces správy paměti pro objekty na haldě. Když jsou objekty alokovány na haldu, projdou několika fázemi sběru – obvykle poměrně rychle, protože většina objektů na haldě má krátkou životnost.

Sbírání odpadu obsahuje tři fáze – označování, mazání a kopírování/zahušťování. V první fázi GC projde haldu a označí vše buď jako živé (odkazované) objekty, neodkazované objekty nebo volné místo v paměti. Nereferencované objekty jsou poté odstraněny a zbývající objekty jsou zkomprimovány. Při generačních garbage collections objekty „stárnou“ a ve svém životě postupují přes 3 prostory – Eden, Survivor space a Tenured (Old) space. K tomuto posunu dochází také v rámci fáze kompakce.

Ale dost o tom, přejděme k zábavné části!

Pst! Hledáte řešení pro zlepšení výkonu aplikací?OverOps pomáhá společnostem identifikovat nejen to, kdy a kde dochází ke zpomalení, ale také proč a jak k němu dochází. Podívejte se na živou ukázku a zjistěte, jak to funguje.

Poznejte Garbage Collection (GC) v Javě

Jednou ze skvělých věcí na automatizovaném GC je, že vývojáři vlastně nemusí rozumět tomu, jak funguje. Bohužel to znamená, že mnoho vývojářů NEchápe, jak funguje. Porozumět garbage collection a mnoha dostupným GC, to je něco jako znát příkazy CLI v Linuxu. Technicky je nemusíte používat, ale jejich znalost a pohodlné používání může mít významný vliv na vaši produktivitu.

Stejně jako u příkazů CLI existují naprosté základy. ls příkaz pro zobrazení seznamu složek v nadřazené složce, mv pro přesun souboru z jednoho umístění do druhého atd. V GC by tyto druhy příkazů odpovídaly tomu, kdybyste věděli, že existuje více než jeden GC na výběr a že GC může způsobit problémy s výkonem. Samozřejmě je toho mnohem více, co je třeba se naučit (o používání linuxového CLI A o garbage collection).

Účelem učení se o procesu garbage collection v Javě není jen bezdůvodné (a nudné) zahájení konverzace, účelem je naučit se efektivně implementovat a udržovat správný GC s optimálním výkonem pro vaše konkrétní prostředí. Vědět, že garbage collection ovlivňuje výkon aplikace, je základ a existuje mnoho pokročilých technik pro zvýšení výkonu GC a snížení jeho vlivu na spolehlivost aplikace.

Problémy výkonu GC

Úniky paměti –

Se znalostí struktury haldy a způsobu, jakým garbage collection probíhá, víme, že využití paměti postupně roste, dokud nenastane událost garbage collection a využití opět neklesne. Využití haldy pro odkazované objekty obvykle zůstává stabilní, takže pokles by měl být víceméně na stejný objem.

Při úniku paměti každá událost GC vyčistí menší část objektů haldy (ačkoli mnoho objektů, které zůstaly, se nepoužívá), takže využití haldy se bude dále zvyšovat, dokud nebude paměť haldy plná a nebude vyhozena výjimka OutOfMemoryError. Příčinou je to, že GC označuje ke smazání pouze neodkazované objekty. Takže i když se odkazovaný objekt již nepoužívá, nebude z haldy vymazán. Existuje několik užitečných kódovacích triků, jak tomu zabránit, kterými se budeme zabývat o něco později.

Kontinuální události „Stop the World“ –

V některých scénářích lze garbage collection nazvat událostí Stop the World, protože když k ní dojde, všechna vlákna v JVM (a tedy i aplikace, která na něm běží) jsou zastavena, aby bylo možné GC provést. Ve zdravých aplikacích je doba provádění GC relativně nízká a nemá velký vliv na výkon aplikace.

V neoptimálních situacích však mohou události Stop the World výrazně ovlivnit výkon a spolehlivost aplikace. Pokud událost GC vyžaduje pauzu Stop the World a její provedení trvá 2 sekundy, koncový uživatel dané aplikace zaznamená 2sekundové zpoždění, protože vlákna běžící v aplikaci jsou zastavena, aby bylo umožněno provedení GC.

Při výskytu úniků paměti jsou problematické i nepřetržité události Stop the World. Protože se při každém provedení GC vyčistí méně místa v paměti haldy, trvá kratší dobu, než se zbývající paměť zaplní. Když je paměť zaplněna, spustí JVM další událost GC. Nakonec bude JVM spouštět opakované události Stop the World, což způsobí velké problémy s výkonem.

Využití CPU –

A to vše se týká využití CPU. Hlavním příznakem neustálých událostí GC / Stop the World je nárůst využití CPU. GC je výpočetně náročná operace, a tak může zabírat více než slušný podíl výkonu procesoru. U GC, při kterých běží souběžná vlákna, může být využití CPU ještě vyšší. Výběr správného GC pro vaši aplikaci bude mít největší vliv na využití CPU, ale existují i další způsoby optimalizace pro lepší výkon v této oblasti.

Z těchto výkonnostních problémů týkajících se garbage collection můžeme pochopit, že ať už jsou GC jakkoli pokročilé (a ony jsou stále dost pokročilé), jejich achillova pata zůstává stejná. Nadbytečné a nepředvídatelné alokace objektů. Ke zlepšení výkonu aplikace nestačí jen zvolit správný GC. Musíme vědět, jak tento proces funguje, a musíme optimalizovat náš kód tak, aby naše GC nečerpaly nadměrné prostředky a nezpůsobovaly nadměrné pauzy v naší aplikaci.

Generační GC

Než se ponoříme do různých GC Javy a jejich vlivu na výkon, je důležité pochopit základy generačního garbage collection. Základní koncept generačního GC je založen na myšlence, že čím déle existuje odkaz na objekt v haldě, tím menší je pravděpodobnost, že bude označen ke smazání. Označením objektů obrazným „stářím“ by mohly být rozděleny do různých úložných prostorů, aby je GC označoval méně často.

Když je objekt alokován na haldu, je umístěn do tzv. prostoru Eden. Tam objekty začínají a ve většině případů jsou tam označeny k odstranění. Objekty, které tuto fázi přežijí, „slaví narozeniny“ a jsou zkopírovány do prostoru Survivor. Tento proces je znázorněn níže:

Prostory Eden a Survivor tvoří tzv. mladou generaci. Zde se odehrává většina děje. Když (Pokud) objekt v Mladé generaci dosáhne určitého věku, je povýšen do prostoru Tenured (nazývaného také Old). Výhodou rozdělení pamětí objektů podle věku je, že GC může pracovat na různých úrovních.

Minor GC je kolekce, která se zaměřuje pouze na Mladou generaci a prostor Tenured vlastně zcela ignoruje. Obecně platí, že většina objektů v Mladé generaci je označena ke smazání a k uvolnění paměti na haldě není nutné provádět Major nebo Full GC (včetně Staré generace). V případě potřeby se samozřejmě spustí Major nebo Full GC.

Jedním z rychlých triků, jak na základě toho optimalizovat činnost GC, je upravit velikosti oblastí haldy tak, aby co nejlépe vyhovovaly potřebám vašich aplikací.

Typy kolektorů

Na výběr je mnoho dostupných GC, a přestože se G1 stal výchozím GC v Javě 9, byl původně určen k nahrazení kolektoru CMS, který je Low Pause, takže pro aplikace provozované s kolektory Throughput může být vhodnější zůstat u svého stávajícího kolektoru. Pochopení provozních rozdílů a rozdílů v dopadu na výkonnost kolektorů odpadu Javy je stále důležité.

Propustné kolektory

Vhodnější pro aplikace, které potřebují být optimalizovány pro vysokou propustnost a mohou pro její dosažení vyměnit vyšší latenci.

Sériový –

Sériový kolektor je nejjednodušší a je nejméně pravděpodobné, že ho budete používat, protože je určen hlavně pro jednovláknové prostředí (např. 32bitové nebo Windows) a pro malé haldy. Tento kolektor dokáže vertikálně škálovat využití paměti v JVM, ale vyžaduje několik majoritních/úplných GC k uvolnění nevyužitých prostředků haldy. To způsobuje časté pauzy Stop the World, což jej pro všechny účely diskvalifikuje z použití v uživatelských prostředích.

Paralelní –

Jak popisuje jeho název, tento GC používá k prohledávání a kompaktaci haldy více paralelně běžících vláken. Přestože paralelní GC používá pro sběr odpadu více vláken, stále pozastavuje všechna aplikační vlákna za běhu. Paralelní kolektor je nejvhodnější pro aplikace, které potřebují optimalizovat pro co nejlepší propustnost a výměnou za to mohou tolerovat vyšší latenci.

Kolektory s nízkou pauzou

Většina aplikací zaměřených na uživatele bude vyžadovat GC s nízkou pauzou, aby uživatelský komfort nebyl ovlivněn dlouhými nebo častými pauzami. U těchto GC jde především o optimalizaci odezvy (čas/událost) a silný krátkodobý výkon.

Concurrent Mark Sweep (CMS) –

Podobně jako paralelní kolektor využívá kolektor Concurrent Mark Sweep (CMS) více vláken k označování a procházení (odstraňování) nereferencovaných objektů. Tento GC však iniciuje události Stop the World pouze ve dvou konkrétních případech:

(1) při inicializaci počátečního označení kořenů (objektů ve staré generaci, které jsou dosažitelné ze vstupních bodů vláken nebo statických proměnných) nebo jakýchkoli referencí z metody main() a ještě několikrát

(2), když aplikace změnila stav haldy během souběžného běhu algoritmu, což jej donutilo vrátit se zpět a provést poslední úpravy, aby se ujistil, že má správně označené objekty

G1 –

Sběrač odpadků (obecně známý jako G1) využívá několik vláken na pozadí k prohledávání haldy, kterou rozděluje na oblasti. Funguje tak, že nejprve prohledává ty oblasti, které obsahují nejvíce objektů odpadu, od čehož se odvíjí jeho název (Garbage first).

Tato strategie snižuje pravděpodobnost, že se halda vyčerpá dříve, než vlákna na pozadí dokončí prohledávání nepoužívaných objektů, v takovém případě by kolektor musel zastavit aplikaci. Další výhodou kolektoru G1 je, že kompaktuje haldu za chodu, což kolektor CMS dělá pouze během úplných kolekcí Stop the World.

Zlepšení výkonu GC

Výkon aplikace je přímo ovlivněn četností a délkou trvání garbage collections, což znamená, že optimalizace procesu GC se provádí snížením těchto metrik. Existují dva hlavní způsoby, jak toho dosáhnout. Zaprvé úpravou velikosti haldy mladých a starých generací a zadruhé snížením rychlosti přidělování a povyšování objektů.

Pokud jde o úpravu velikosti haldy, není to tak jednoduché, jak by se dalo očekávat. Logickým závěrem by bylo, že zvětšení velikosti haldy sníží frekvenci GC a zároveň zvýší dobu trvání a zmenšení velikosti haldy sníží dobu trvání GC a zároveň zvýší frekvenci.

Faktem však je, že doba trvání minoritního GC nezávisí na velikosti haldy, ale na počtu objektů, které přežijí sběr. To znamená, že u aplikací, které většinou vytvářejí objekty s krátkou životností, může zvýšení velikosti mladé generace ve skutečnosti snížit jak dobu trvání GC, tak i četnost. Pokud však zvýšení velikosti mladé generace povede k výraznému nárůstu objektů, které je třeba zkopírovat do prostorů pro přežití, budou pauzy GC trvat déle, což povede ke zvýšení latence.

3 tipy pro psaní kódu efektivního z hlediska GC

Tip č. 1: Předvídejte kapacity kolekcí –

Všechny standardní kolekce Java i většina vlastních a rozšířených implementací (například Trove a Guava společnosti Google) používají základní pole (buď primitivní, nebo objektová). Vzhledem k tomu, že pole mají po přidělení neměnnou velikost, může přidávání položek do kolekce v mnoha případech způsobit, že staré podkladové pole bude zrušeno ve prospěch většího nově přiděleného pole.

Většina implementací kolekcí se snaží tento proces přerozdělování optimalizovat a udržet jej na amortizovaném minimu, i když není uvedena očekávaná velikost kolekce. Nejlepších výsledků však dosáhnete, pokud při konstrukci kolekce poskytnete její očekávanou velikost.

Tip č. 2: Zpracovávejte proudy přímo –

Při zpracování proudů dat, jako jsou například data načtená ze souborů nebo data stažená ze sítě, se velmi často setkáváme s něčím podobným:

Výsledné pole bajtů by pak mohlo být analyzováno do dokumentu XML, objektu JSON nebo zprávy Protocol Buffer, abychom jmenovali alespoň několik oblíbených možností.

Při práci s velkými soubory nebo soubory s nepředvídatelnou velikostí je to samozřejmě špatný nápad, protože nás to vystavuje chybám OutOfMemoryErrors v případě, že JVM skutečně nemůže alokovat vyrovnávací paměť o velikosti celého souboru.

Lepší způsob, jak k tomu přistupovat, je použít příslušný InputStream (v tomto případě FileInputStream) a vložit ho přímo do parseru, aniž by se nejprve celý načetl do pole bajtů. Všechny hlavní knihovny vystavují API pro přímé parsování proudů, například:

Tip #3: Používejte neměnné objekty –

Neměnnost má mnoho výhod. Jednou z nich, které se málokdy věnuje pozornost, jakou si zaslouží, je její vliv na garbage collection.

Neměnný objekt je objekt, jehož pole (a v našem případě konkrétně neprimitivní pole) nelze měnit poté, co byl objekt zkonstruován.

Neměnnost znamená, že všechny objekty, na které odkazuje neměnný kontejner, byly vytvořeny před dokončením konstrukce kontejneru. Řečeno terminologií GC: Kontejner je alespoň tak mladý jako nejmladší reference, kterou drží. To znamená, že při provádění cyklů vybírání odpadu v mladých generacích může GC přeskočit nezměnitelné objekty, které leží ve starších generacích, protože s jistotou ví, že nemohou odkazovat na nic v generaci, která se vybírá.

Méně objektů ke skenování znamená méně stránek paměti ke skenování a méně stránek paměti ke skenování znamená kratší cykly GC, což znamená kratší pauzy GC a lepší celkovou propustnost.

Další tipy a podrobné příklady najdete v tomto příspěvku, který se zabývá podrobnými taktikami pro psaní paměťově úspornějšího kódu.

*** Obrovský dík patří Amitovi Hurvitzovi z R& týmu OverOps za jeho nadšení a vhled do problematiky, který se promítl do tohoto příspěvku!

Napsat komentář