PermGen error

Většina nově vzniklých programovacích jazyků používá k uvolňování paměti konceptu garbage collectoru. Obecně rozšířený ja názor, že tento koncept odstraňuje nebezpečí úniku paměti (memory leak). Pravda je však taková, že k únikům paměti může docházet stále a například Java může mít v této oblasti velmi podobné problémy jako C++. Navíc má Java několik specifik, jako jsou dynamické načítání bytekódu a jiné, které tento problém značně zhoršují. Jedna z takových slabých stránek Javy se projevuje výskytem výjimky OutOfMemoryError: PermGen space. O co se jedná a jak reagovat?

Poznámka na začátek

Tento článek vznikl z důvodu zoufalého nedostatku studií, které by se tomuto problému věnovaly komplexně a zároveň stravitelně. Cílem bylo vytvořit článek, který uvede čtenáře do problému ve všech jeho souvislostech a zároveň bude moci sloužit jako jednoduše čitelné how-to pro pro odstranění problému. Bohužel až po napsání textu jsem narazil na podobně zaměřený článek, který obsahuje téměř totožné informace, pouze jinak stylisticky podané. Na jednu stranu by mi jeho přečtení ušetřilo množství času, ale na druhou stranu funguje jako příjemné potvrzení, že mnou nalezené informace jsou víceméně kompletní a věcně správné.

Teorie

Sun Java z důvodu optimalizace práce s pamětí dělí heap paměť na několik segmentů. Segment, ve kterém se nachází konkrétní alokovaný prostor závisí na tom, jak dlouho je již tento prostor alokován. Proto jsou jednotlivé segmenty nazývány "generace" - "generation".

Během standardního běhu programu jsou všechny alokace prováděny v běžném segmentu a do ostatních segmentů JVM přesouvá objekty až po nějaké době, kdy zná charakteristiky běžícího programu. Výjimkou je alokace místa pro metainformace o  třídách, která se automaticky provádí v "segmentu stálé generace" (permanent generation - PermGen). U paměti alokované v tomto segmentu je předpokládáno, že bude uvolňována velmi málo nebo dokonce nikdy, což se pro informace  o třídách zdá jako rozumný předpoklad.

Na první pohled se tedy může zdát, že PermGen error se objeví vždy, když místo pro metainformace o třídách programu je větší než velikost paměťového segmentu permanentní generace. Stojí za povšimnutí, že velikosti jednotlivých segmentů se nastavují při startu JVM zvlášť a přesun volné paměti mezi jednotlivými segmenty není možný. Vzhledem k tomu, že se při startu JVM často pamatuje pouze na parametr pro velikost standardního segmentu heap, vypadá výjimka OutOfMemoryError velmi záhadně, protože i při povrchním pohledu k ní došlo ve chvíly, kdy program v paměti zdaleka nezabíral maximální množství místa. Toto je způsobeno právě tím, že ačkoli bylo nastaveno velké množství standardní heap paměti, velikost permanent generation segmentu byla ponechána na výchozí hodnotě JVM, která je řádově menší.

Pokud tedy dojde k této výjimce v případě příliš objemného programu, je řešení jednoduché - je potřeba nastavit větší velikost permanent generation segmentu. Bohužel, toto je velmi vzácný případ.

Daleko rozšířenější je případ způsobený jednou z vlastností Javy - programově ovlivnitelným načítáním nebo vytvářením bytekódu tříd. Klíčovým slovem tohoto konceptu je "classloader", tedy rozhraní pro dynamické zavádění tříd. Classloader je možné programově nastavovat i řídit.

Nejrozšířenějším případem takového použití je tzv. redeploy aplikace v servlet kontejneru. Servlet kontejner je de-facto webový server v jehož kontextu (jednom spuštěném JVM) může nezávisle běžet několik webových java aplikací. Tyto aplikace není možné jednotlivě restartovat právě proto, že všechny běží v rámci jednoho spuštěného JVM. Proto byl vyvinut postup, ve kterém jsou třídy jednotlivých aplikací načítány oddělenými classloadery. Pokud potom chce kontejner danou aplikaci restartovat, jednoduše zahodí její classloader a vytvoří nový. Tento postup je většinou součástí procedury nazývané redeploy.

V ideálním případě je zahozený classloader (a tím i veškerý bytekód tříd) uvolněn garbage collectorem. Velmi často se však stává, že garbage collector nemůže classloader vymazat kvůli nějakým zbytkovým referencím a dojde tedy k úniku paměti v segmentu permanentní generace, který má v důsledku těsné provázanosti classloaderu a tříd velikost rovnou celkové velikosti všech tříd. Je jasné, že po několika redeployích se celý servlet kontejner zhroutí kvůli výjimce OutOfMemoryError: PermGen space.

Praxe

V předchozí kapitole je podán nejhutnější možný úvod do problému PermGen paměti, proto ještě pro jistotu rekapitulace v bodech:

K předchozím bodům je nyní vhodné doplnit další stručné informace

Přidání paměti do segmentu PermGen

Jednoduchý postup pro jednoduchý případ. Při jakýchkoli problémech s pamětí je dobré si udělat zevrubnou analýzu paměťových nastavení JVM. Základem analýzy je uvědomění si, jaká nastavení JVM se vztahují k jakým segmentům paměti. Pro případ PermGen je vidět, že nastavení, se kterým je potřeba pracovat je "-XX:MaxPermSize". Pokud v aplikaci nedochází k únikům paměti, může toto nastavení pomoci od problémů natrvalo. Pokud dochází k únikům paměti, může toto nastavení snížit četnost výskytu výjimek PermGen.

Zjišťování referencí blokujících GC

Pokud navýšení PermGen paměti pouze oddálí problém, je jasné, že v aplikaci dochází k únikům paměti a je potřeba najít její příčiny, tedy najít reference, které blokují uvolnění classloaderu

Komerční profilery

Nejjednodušší postup je použít komerční Java profilery, které mají některé specializovaná nástroje pro řešení PermGen problému.Odkazovaný YourKit profiler se mně osobně osvědčil, jelikož jeho plně funkční zkušební verze zdarma můžete používat celý měsíc, takže bohatě stačí pro vyřešení jednoho problému s pamětí.

Zabudované nástroje Sun Java

Update 12. 2. 2012: Od doby napsání tohoto článku došlo k významnému posunu v debugovacích nástrojích poskytovaných v základní distribuci Javy. JHat, JMap i jiné jsou prakticky úplně nahrazeny schopnostmi nástroje VisualVM který umožňuje použít všechny zde uvedené praktiky v uživatelsky podstatně příjemnější podobě.

Základní prostředky pro řešení PermGen problému však poskytuje také přímo Sun distribuce Javy. Postup pro použití je zhruba následující:

  1. Je vhodné začít s pozorováním celkového provozu JVM pomocí nástroje jconsole.
  2. Zjistit Java ID procesu servletového kontejneru pomocí nástroje jps. Je-li spuštěn bez parametrů, vypíše seznam všech Java procesů s rozumnými popisky a odpovídajícími ID.
  3. Získat dump paměti JVM ve kterém už došlo k nějakým únikům (typicky tedy po několika redeploy) nástrojem jmap, pomocí příkazu "jmap -dump:format=b,file=leak <javaProcessId>". Pokud k problémům dochází pouze na produkci, je možné použít k získání dumpu paměti parametry JVM "-XX:-HeapDumpOnOutOfMemoryError" a "-XX:HeapDumpPath=/tmp/leak".
  4. Analyzovat dump paměti a nalézt residuální reference na classloader, který byl zahozen.
    • Pomocí nástroje jhat. Spuštění (pro v předchozím bodě získaný dump) se provede příkazem "jhat -J-Xmx1024m leak". Aplikace má podobu webové aplikace spuštěné na adrese localhost s portem vypsaným při startu aplikace.
    • Pomocí Eclipse memory analyzer. Tento nástroj jsem příliš nepoužíval, ale údajně je práce s ním rychlejší než s jhat.
  5. Přijít na způsob, jak se residuálních referencí zbavit. Pro několik běžných knihoven jsou známy postupy, jak únikům zamezit konfigurací. Většinou je ale také potřeba vytvořit třídu implementující servlet rozhraní "application lifecycle listener" a zaregistrovat jí ve své webové aplikaci. V této třídě je pak možné při obsluze události "contextDestroyed" zrušit všechny nalezené residuální reference.

JHat

JHat je velmi mocný nástroj (hlavně díky možnosti automatizovat některé postupy), ale jeho ovládání je velmi strohé. Pro jednoduchý návod doporučuji stránku již jednou citovaného F. Kievieta. Postup, který je na ní popsaný by se dal shrnout následovně:

  1. Postup předpokládá memory dump z kontejneru ze kterého byla testovaná aplikace "undeploynuta" (první část redeploy).
  2. Nalézt třídu, která se vyskytuje v aplikaci (tudíž by ve výpisu vůbec neměla být = unikla GC) a kliknout na odkaz k jejímu classloaderu.
  3. Na stránce classloaderu najít odkaz zjišťující reference z root set (s výjimkou weak referencí - exclude weak refs).
  4. Tento seznam obsahuje třídy podezřelé z uchovávání residuálních referencí. Nyní už je potřeba použít intuici (s možností lehké dopomoci od histogramu tříd v referenčním řetězu).

Často používané knihovny s úniky paměti

Nedořešené otázky

Přestože jsem na toto téma přečetl mnoho materiálů, stále mi není jasných několik podrobností. Pokud o nich víte něco víc, budu vděčný, když se podělíte.

Last update
201202120000