V tomto článku si stručně popíšeme důležité a v některých případech i poněkud méně známé či méně často používané nástroje, které je možné využít pro monitorování virtuálního stroje Javy (JVM) a samozřejmě taktéž aplikací, které jsou ve vybraném virtuálním stroji spuštěny. Nejprve si popíšeme základní nástroje určené pro práci v terminálu (plus jeden nástroj pracující jako webový server) a v navazujícím článku i nástroje plnohodnotně využívající grafické uživatelské rozhraní. Při popisu vlastností jednotlivých nástrojů se zaměříme především na OpenJDK7, i když naprostá většina dále prezentovaných informací bude využitelná i při použití OpenJDK6 a Oracle JDK 6 a 7.

Programovací jazyk Java a operační systém Linux

Programovací jazyk Java, který byl široké vývojářské veřejnosti představen již v roce 1991, se v současnosti stal nedílnou součástí mnoha instalací operačního systému Linux. To znamená, že běhové prostředí Javy (JRE) dnes můžeme najít jak na linuxových serverech, kde je tento jazyk použit například pro provozování webových aplikací založených na servletech a stránkách JSP či dokonce celého aplikačního serveru, tak na běžných desktopových počítačích (nutno dodat, že mnohdy k nevelké radosti uživatelů, což je však téma na jiný článek týkající se návrhu efektivně pracujících aplikací naprogramovaných v Javě).

Obrázek 1: Jednou z poměrně populárních desktopových aplikací naprogramovaných v Javě je i editor myšlenkových map, který nese jméno FreeMind. Více informací o této aplikaci naleznete na stránce http://freemind.sourceforge.net/.

Naprostá většina aplikací vytvořených v Javě vyžaduje pro svůj běh takzvaný virtuální stroj Javy (Java Virtual Machine - JVM), a to z toho důvodu, že zdrojové kódy jsou většinou překládány do bajtkódu JVM a nikoli přímo do nativního binárního kódu dané mikroprocesorové architektury. Bajtkód musí být nejdříve načten do virtuálního stroje Javy, kde proběhne jeho verifikace a teprve následně je ověřený bajtkód „spuštěn“ - v závislosti na použité JVM a jejím nastavení jsou buď instrukce bajtkódu (tvořící těla metod) pouze interpretovány nebo jsou přeloženy do nativního kódu zpracovávaného mikroprocesorem.

Obrázek 2: V Javě je naprogramováno i mnoho aplikací využívaných především programátory či administrátory. Jedná se například o SQuirreL SQL, což je aplikace sloužící vývojářům a databázovým administrátorům pro vytváření a údržbu relačních databází. Více informací o této aplikaci naleznete na stránce http://squirrel-sql.sourceforge.net/.

Zajímavým a v praxi i čím dál tím důležitějším faktem je, že bajtkód dokonce nemusí vznikat pouze překladem zdrojových kódů napsaných v Javě, protože dnes existují i překladače jiných programovacích jazyků, které dokážou generovat bajtkód plně kompatibilní s JVM, a tudíž i (alespoň ve většině případů) se samotnou Javou. Díky tomu mohou vznikat nové programovací jazyky bez nutnosti znovuvytváření všech potřebných knihoven, protože základní infrastruktura je dostupná přímo v JRE, neboli v běhovém prostředí Javy.

Obrázek 3: Jedním z nových programovacích jazyků, v nichž dochází k automatickému či ručnímu překladu programů do bajtkódu kompatibilního s JVM, je i programovací jazyk Clojure (inspirovaný programovacími jazyky LISP a Scheme), jehož vlastnosti ho předurčují pro tvorbu (masivně) paralelních aplikací.

Běh aplikací ve virtuálním stroji - přednost nebo nevýhoda?

Fakt, že jsou aplikace naprogramované v Javě nejprve přeloženy do bajtkódu a posléze je tento bajtkód spuštěn ve virtuálním stroji (JVM), s sebou přináší některé přednosti a nutno říci, že taktéž zápory, protože v IT nedostaneme (téměř) nic zadarmo. Mezi přednosti patří například již zmíněná možnost verifikace bajtkódu, spouštění programů v takzvaném sandboxu s volitelnou mírou izolace programů od operačního systému (například appletům či aplikacím spouštěným přes Java WebStart lze snadno zakázat přístup na lokální systém souborů), ale taktéž možnost velmi detailně sledovat běžící aplikaci, což je i hlavní téma tohoto článku.

Obrázek 4: Přeložený bajtkód je uložen v binárních souborech s pevně určenou strukturou popsanou do nejmenších detailů ve specifikaci „The Java Virtual Machine Specification“. Díky tomu, že je tato specifikace dodržována, je bajtkód přenositelný mezi JVM různých výrobců, i když je nutné říci, že různé překladače mohou generovat různý bajtkód (ten však stále odpovídá specifikaci).

Hned na začátku je totiž nutné říci, že virtuální stroj Javy není v žádném případě nějakou „magickou černou skříňkou“, která by před ostatním světem skrývala, co se děje uvnitř. Mohlo by se dokonce říci, že ve skutečnosti je tomu právě naopak, protože již při instalaci JDK (což je zkratka termínu „Java SE Development Kit“ označujícího balíček se základními vývojovými nástroji i samotným virtuálním strojem) se společně s překladačem, generátorem dokumentace, jednoduchým prohlížečem appletů atd. nainstaluje hned několik nástrojů, které sledování činnosti virtuálního stroje a v něm běžících aplikací umožňují.

Kromě toho nabízí běhové prostředí Javy (JRE - „Java Runtime Environment“) několik standardizovaných rozhraní (zejména JVM TI, JDWP, JMX...), díky nimž se mohou další nástroje připojit přímo k běžícímu virtuálnímu stroji Javy a přes tato rozhraní s virtuálním strojem komunikovat – a to dokonce i vzdáleně. Typicky se jedná o debuggery, trasovací nástroje a některé profilery – což je však téma, kterým se budeme zabývat v některém z navazujících článků, které jsou pro server Fedora.cz připravovány.

Obrázek 5: I integrované vývojové prostředí Eclipse je vybaveno funkcí pro připojení k běžícímu virtuálnímu stroji Javy s možnosti ladění aplikací přímo v JVM.

Popisované monitorovací nástroje dostupné pro Javu

V následujících kapitolách si stručně popíšeme některé standardní nástroje spouštěné v terminálu z příkazové řádky – to mj. znamená, že je možné tyto nástroje v případě potřeby jednoduše spouštět i vzdáleně přes ssh atd. Jedná se o nástroje se jmény jstack, jmap a jhat, k nimž se ještě přidává pomocná aplikace jps. V navazující části tohoto článku se taktéž zmíním o dvou nástrojích vybavených plnohodnotným grafickým uživatelským rozhraním. První z těchto nástrojů nese název jconsole a jedná se o standardní součást některých moderních instalací JDK, včetně OpenJDK verze 6 i 7 a Oracle JDK, taktéž verze 6 i 7.

Obrázek 6: Grafické uživatelské rozhraní nabízené nástrojem jconsole, jenž je standardní součástí instalace OpenJDK 6/7 a Oracle JDK 6/7. Tento nástroj se dokáže připojit k již běžícím JVM a dokáže z těchto virtuálních strojů získávat různé informace, například informace o obsazení haldy (heap), seznam spuštěných vláken, vlastnosti dostupné přes JMX (Java Management Extensions) atd.

Druhý nástroj, který si popíšeme až v navazující části tohoto článku, se sice prozatím nachází ve stadiu poměrně intenzivního vývoje, ovšem již ve své současné verzi obsahuje některé funkce, které v jconsole nenajdeme, takže je mnohdy prospěšné vhodným způsobem zkombinovat možnosti obou aplikací. Tento nástroj nese název Thermostat. Všechny dále popisované nástroje lze samozřejmě nainstalovat ze standardních balíčků Fedory, a to buď v rámci instalace OpenJDK 6/7 (ve Fedoře 17 a 18 je již dostupný pouze balíček OpenJDK7), nebo jako samostatný balíček nezávislý na JDK, což je případ projektu Thermostat.

Obrázek 7: Grafické uživatelské rozhraní projektu Thermostat ve verzi pro systém Fedora 17. Vývojová verze této aplikace je v některých ohledech již vylepšena a obsahuje mnohé nové funkce.

Demonstrační příklad pro odzkoušení funkce popisovaných nástrojů

Pro odzkoušení základní funkcionality monitorovacích nástrojů popsaných v následujících čtyřech kapitolách byl vytvořen velmi krátký demonstrační příklad, jehož činnost je poměrně jednoduchá: program po svém spuštění pouze na standardní výstup vypíše řetězec „Press Enter...“ a posléze čeká na stisk klávesy Enter (přesněji řečeno je ve skutečnosti možné zapsat libovolný počet znaků, které však musí být následovány stiskem Enteru). Po stisku klávesy Enter je program ukončen. Jedná se tedy o mírně upravenou variantu programu typu „Hello world!“, s nímž se jistě každý vývojář alespoň jednou setkal.

Aby však vše nebylo tak jednoduché, volá se ze statické funkce Test.main() nejdříve (již nestatická) metoda nazvaná Test.run(), z ní pak metoda Test.printMessageAndWaitForEnter() a z ní konečně metoda Test.waitForEnter(). Díky tomuto (zbytečně) komplikovanému volání jednotlivých metod se na zásobníku hlavního vlákna aplikace musí vytvořit větší množství takzvaných zásobníkových rámců (stack frames), které jsou vypsány nástrojem jstack. Čekání na stisk klávesy Enter bylo do demonstračního příkladu přidáno proto, aby program ihned neskončil, protože v tomto případě by nám nezbyl žádný čas na zavolání testovaných nástrojů.

Obrázek 8: Zdrojový kód demonstračního příkladu použitého pro testování funkce dále popisovaných monitorovacích nástrojů. Překlad tohoto příkladu do bajtkódu JVM se provede jednoduše pomocí příkazu javac Test.java.

Nástroj jps sloužící pro výpis všech aplikací běžících ve virtuálních strojích Javy

První nástroj, s nímž se musíme seznámit hned na začátku, se jmenuje jps (celým jménem „Java Virtual Machine Process Status Tool“) a jeho úloha je v podstatě velmi jednoduchá: tento program totiž slouží k výpisu seznamu všech javovských procesů (tj. instancí aplikací) běžících ve virtuálním stroji Javy (JVM). Pokud tento program spustíme bez parametrů, vypíšou se dva sloupce informací. Ve sloupci prvním je uveden jednoznačný celočíselný identifikátor procesu a ve sloupci druhém jeho textové označení, které odpovídá buď názvu třídy se statickou metodou main(), nebo názvu Java archivu (Java Archive - JAR), v němž je spouštěná část aplikace uložená (celá aplikace totiž může být ve skutečnosti obsažená ve více Java archivech).

Obrázek 9: Ukázka výpisu všech lokálně běžících javovských aplikací zjištěných pomocí nástroje jps. Nástroj jps je naprogramován v Javě, tudíž vypíše i sám sebe (konkrétně na tomto snímku obrazovky je běžící jps vypsán pod identifikátorem procesu číslo 21214).

Nástroj jps dokonce umožňuje vypsat javovské procesy běžící na vzdáleném stroji, samozřejmě za předpokladu, že tato funkce není přerušena/zakázána například nastavením firewallu atd. Na začátku předchozího odstavce bylo napsáno, že se s nástrojem jps musíme seznámit hned na začátku, tj. ještě před popisem dalších monitorovacích nástrojů. Důvod pro toto tvrzení je poměrně prostý – většina dále popisovaných nástrojů totiž používá pro přesné určení javovského procesu, který se má monitorovat, právě celočíselný identifikátor získaný s využitím jps.

Obrázek 10: Volba jps -l slouží k tomu, aby se zkrácené jméno třídy či archivu (JAR) každého javovského procesu rozepsalo na plné jméno, tj. aby byl například uveden i název balíčku či cesta k archivu (JAR).

Nástroj jstack: výpis obsahu zásobníkových rámců všech vláken

Druhým monitorovacím nástrojem, s nímž se v dnešním článku seznámíme, je nástroj nesoucí jméno jstack. Již název tohoto nástroje, který je taktéž standardní součástí JDK (přesněji řečeno OpenJDK 6/7 a Oracle JDK 6/7), naznačuje, k čemu ho je možné použít: pomocí jstack je totiž možné zjistit a následně i vypsat obsah zásobníkových rámců (stack frames) všech vláken vybrané běžící aplikace. Se způsobem výpisu obsahu zásobníkových rámců použitých v jstack se již zcela jistě setkal prakticky každý uživatel Javy, ať již se jedná o programátora nebo o administrátora či uživatele.

Obrázek 11: Současné verze programu jstack je možné použít i pro připojení ke vzdáleně běžícím procesům, popř. i pro analýzu již neaktivního javovského procesu. Pozor – některé předchozí varianty tohoto programu používaly jinak pojmenované přepínače!

Podobný výpis obsahu zásobníkových rámců (přesněji řečeno pouze jednoho rámce) se totiž používá i v případě, že v běžící aplikaci dojde k výjimce. Sama výjimka je reprezentována objektem, jehož třída implementuje rozhraní Throwable a to předepisuje (kromě mnoha dalších metod) i existenci přetížené metody printStackTrace(), kterou lze použít například pro zalogování chyby. Tato informace totiž může programátorům pomoci najít, ve kterém místě aplikace chyba ve skutečnosti vznikla, protože se většinou jedná o místo odlišné od toho, kde došlo k vytvoření výjimky.

Obrázek 12: Výpis obsahu zásobníkových rámců všech vláken našeho testovacího programu. Z tohoto (zde kvůli omezenému místu jen neúplného) výpisu je patrné, že i v té nejjednodušší aplikaci běží na pozadí několik vláken vytvořených přímo virtuálním strojem Javy.

Výpis generovaný nástrojem jstack je v mnoha ohledech podobný výpisu, který nám nabízí metoda Throwable.printStackTrace(), ovšem s tím rozdílem, že jsou vypsány obsahy zásobníkových rámců pro všechna vlákna, nikoli pouze pro vlákno, v němž nastala výjimka. Při použití nástroje jstack se mu většinou předává celočíselný identifikátor běžící javovské aplikace, jehož hodnota se získá s využitím nástroje jps popsaného v předchozí kapitole.

Obrázek 13: Obsah zásobníkového rámce hlavního vlákna našeho testovacího příkladu. Pořadí volání jednotlivých metod a nativních funkcí je nutné číst odspodu, posledních pět řádků přesně odpovídá naprogramovanému chování.

Podívejme se nyní na část výpisu získaného pro náš jednoduchý demonstrační příklad. Z výpisu, který je ukázán na snímcích obrazovky (screenshotech) číslo 12, 13 a 14, je patrné, že i ve velmi jednoduchém javovském procesu je vytvořeno hned několik různých vláken, z nichž mnohé jsou určeny pro provádění různých interních činností – v některých vláknech běží správci paměti (GC – Garbage Collectors), v dalších vláknech překladač bajtkódu do nativního kódu (HotSpot Compiler) atd.

Obrázek 14: Pokud při spuštění nástroje jstack použijeme volbu -m, vypíšou se navíc i informace o volaných nativních funkcích. Tuto informaci je velmi důležité znát zejména tehdy, pokud se hledá chyba v nativní (tj. systémově závislé) části virtuálního stroje či v systémových knihovnách. Zde například můžeme vidět, že se při čekání na stisk klávesy Enter ve skutečnosti volá funkce z nativní standardní systémové knihovny libc (což asi není velkým překvapením). Volba -m je dostupná až od OpenJDK7u7.

Nástroj jmap: výpis struktury paměti virtuálního stroje

Dalším nástrojem, který se používá prakticky stejným způsobem jako výše popsaný nástroj jstack, je pomocná aplikace jmap, která je taktéž součástí standardní instalace OpenJDK 6/7 a Oracle JDK 6/7. Obsah zásobníkových rámců vypsaný s využitím jstack nám sice může naznačit poměrně hodně informací o interním chování zkoumané javovské aplikace, ovšem v mnoha případech potřebujeme při analýze problémů znát i mnoho dalších informací, mezi jinými i obsah haldy (heap) atd. A právě tyto informace je možné získat mj. i s využitím jmap. Některé funkce nabízené touto aplikací budou ukázány na následující čtveřici snímků obrazovky.

V případě, že se nástroj jmap spustí pouze s jediným parametrem – číselným identifikátorem procesu získaným pomocí jps – vypíše se na standardní výstup seznam načtených sdílených knihoven (pozor – tato funkcionalita je dostupná až od OpenJDK7u7). Díky této informaci lze například zjistit, zda se skutečně používá správná JVM, protože v až překvapivě velkém množství případů lze domnělou chybu „opravit“ velmi snadno takovým způsobem, že se například namísto gcj použije OpenJDK či Oracle JDK. Taktéž lze snadno zjistit, zda JVM náhodou nepoužívá 32bitovou knihovnu namísto knihovny 64bitové (samozřejmě na 64bitových systémech) atd.

Obrázek 15: Výpis sdílených knihoven načtených pro náš jednoduchý demonstrační příklad.

Podrobnější informace o struktuře haldy (heap) lze získat jednoduše tak, že se nástroj jmap spustí s parametrem -heap (opět dostupné od OpenJDK7u7). Povšimněte si, že se kromě dalších dat vypíšou i podrobnější informace o minimální, maximální a současné velikosti (kapacity) haldy, kapacita prostoru, do nějž se ukládají bajtkódy metod (perm.gen.), atd. To mj. znamená, že s využitím nástroje jmap je možné velmi snadno zjistit, zda je velikost haldy nastavena korektně, tj. podle požadavků aplikace. Navíc se jedná o informaci, kterou je vhodné přidávat i do hlášení o chybě (bugzilla...).

Obrázek 16: Použití nástroje jmap s parametrem -heap.

Obrázek 17: Další informace vypsané příkazem jmap -heap pid.

Získání informací o instancích jednotlivých tříd

Dalším typem informací, které nástroj jmap dokáže zobrazit, jsou informace o instancích jednotlivých tříd. Tyto informace se zobrazí ve formě tabulky se čtyřmi sloupci. V prvním sloupci je uvedeno pouze číslo řádku tabulky, ve druhém sloupci počet instancí dané třídy, ve sloupci třetím souhrnná velikost paměti obsazené na haldě (heap) a ve sloupci posledním si můžeme přečíst jméno třídy, jejíž instance jsou prozkoumávány. Jména tříd jsou přitom plně kvalifikována, aby od sebe bylo možné odlišit sice stejně pojmenované třídy, které však ve skutečnosti pochází z jiného balíčku.

V tomto výpisu najdeme i informace o polích, protože pole jsou také speciální forma tříd (dokonce lze říci, že se jedná o jednu z nejdůležitějších informací, protože pole typicky zabírají velkou část kapacity haldy). V samotném jménu pole (čtvrtý sloupec) jsou přesně zakódovány dvě informace: kolik dimenzí toto pole má a jakého typu jsou jeho prvky. Například jméno třídy [I značí, že se jedná o jednorozměrné pole s prvky typu int, [[I je dvourozměrné pole typu int, [B je pole bajtů, [Ljava.lang.String; je pole řetězců atd.

Obrázek 18: Použití nástroje jmap s parametrem -histo:live.

Nástroj jhat – Heap Analysis Tool

Informace získané nástrojem jmap popsaným v předchozí kapitole jsou pro sledování výkonu aplikace, popř. pro zjišťování případných memory leaků atd. velmi důležité, ovšem někdy nám nemusí vyhovovat forma získaných dat. Může se stát, že budeme potřebovat v různých časových okamžicích získat „snímky haldy“ a ty posléze detailněji prozkoumat. Pro tento účel je výhodné spojit vlastnosti nástroje jmap s dalším nástrojem, jehož jméno je jhat, neboli „Heap Analysis Tool“. Na rozdíl od předchozích třech popsaných nástrojů se jhat chová jako jednoúčelový webový server nabízející informace o haldě v uživatelsky příjemné a čitelné podobě.

Obrázek 19: Úvodní stránka o snímku haldy (heapu) generovaná jednoúčelovým webovým serverem běžícím na portu 7000.

Před použitím jhat nejdříve musíme získat „snímek haldy“. Pro tento účel se používá nástroj jmap, který se spustí následujícím způsobem:

jmap -dump:file=jméno_souboru,format=b pid_Java_aplikace

Po zadání tohoto příkazu by se měl v aktuálním adresáři vytvořit soubor <jméno_souboru>, v němž je v binární podobě uložen obsah haldy. Těchto souborů lze samozřejmě vytvořit libovolné množství, což se používá například tehdy, když se zjišťují memory leaky v dlouhodobě běžících aplikacích (jmap lze spouštět z cronu atd.). Pozor musíme dát především na to, že soubory se snímky haldy mohou být v některých případech obrovské – výjimkou nejsou ani soubory o velikosti desítek gigabajtů!

Obrázek 20. Histogram zobrazující typy objektů uložených na haldě (heap). Povšimněte si, že nástroj jhat dokáže správně převést interní jména polí na „čitelnější“ způsob zápisu odpovídající konvencím jazyka Java.

Další práce s nástrojem jhat je v podstatě velmi jednoduchá – postačuje tento nástroj spustit a předat mu jméno binárního souboru obsahujícího snímek haldy. jhat následně vytvoří jednoúčelový webový server, který je implicitně dostupný na portu 7000. Uživateli tedy postačuje do svého oblíbeného webového klienta zadat adresu http://localhost:7000 a interaktivně procházet strukturou haldy. Jak je patrné ze snímků obrazovky, je možné se k tomuto webovému serveru připojit i vzdáleně, samozřejmě za předpokladu, že je tato možnost povolena v konfiguraci firewallu.

Obrázek 21: Jedna z instancí třídy String odpovídá řetězci „Press Enter...“, což je očekávané, protože tento řetězec jsme použili v demonstračním příkladu.