Hlavní navigace

Monitorování procesů a správa paměti v JDK 6 a JDK 7 (4)

17. 2. 2011
Doba čtení: 23 minut

Sdílet

V desáté části seriálu o vlastnostech JDK 6 a JDK 7 si ukážeme způsob ovlivnění chování správců paměti pomocí parametrů, které je možné specifikovat při spouštění běhového prostředí Javy (JRE). Zaměříme se především na způsob nastavování velikostí paměťových oblastí, ze kterých se skládá halda (heap).

Obsah

1. Princip funkce správce paměti typu Mark and Sweep

2. Základní způsoby monitorování správce paměti

3. Podrobnější monitorování činnosti správce paměti

4. Statistika vypsaná při použití volby -XX:PrintGCDetails

5. Parametry ovlivňující funkci správce paměti

6. Chování správce paměti při explicitním nastavení velikosti haldy

7. Změna poměru mezi velikostí jednotlivých oblastí na haldě

8. Výsledky měření

9. Odkazy na Internetu

1. Princip funkce správce paměti typu Mark and Sweep

V předchozí části poněkud nepravidelně vycházejícího seriálu o programovacím jazyce Java i o vlastnostech JDK 6 a JDK 7 jsme se zabývali popisem algoritmů, které jsou použity při implementaci správců paměti (GC – Garbage Collectors) a současně i popisem struktury haldy (heap). Připomeňme si, že se v moderních implementacích správců paměti sice používají trasovací algoritmy, tj. algoritmy prohledávající strom objektů a označujících živé objekty, ovšem kvůli větší výkonnosti tyto trasovací algoritmy nepracují stejným způsobem se všemi objekty uloženými na haldě. Namísto toho jsou objekty podle svých vlastností rozděleny do několika navzájem oddělených částí haldy. Nejhrubší dělení je na objekty „mladé“ a objekty „staré“, přičemž „mladé objekty“ (u nichž je větší pravděpodobnost, že jejich životnost bude krátká – může se jednat o jednu iteraci smyčky či jedinou metodu) jsou umístěny v oblasti haldy nazvané příznačně young generation a objekty starší (tj. takové objekty, které již několikrát přežily běh správce paměti) jsou uloženy v oblasti nazvané tenured generation nebo též old generation.

Obrázek 1: Většina běžných objektů je vytvořena v obecně menší oblasti haldy nazvané young generation. Teprve poté, co objekt několikrát „přežije“ spuštění správce paměti nad touto oblastí haldy, může být přesunut do (opět obecně) větší oblasti haldy nazvané old generation či taktéž tenured generation. Objekty, pro jejichž alokaci je zapotřebí velké množství paměti, však mohou být přímo vytvořeny v oblasti tenured generation.

Nad oblastí young generation se relativně často spouští správce paměti optimalizovaný s ohledem na to, že velká část objektů bude z této oblasti skutečně prakticky ihned odstraněna (jedná se o fakt zjištěný měřením chování objektů v mnoha reálných i modelových aplikacích). Objekty, které přežijí několik spuštění tohoto „rychlého“ správce paměti, jsou přesunuty do oblasti old/tenured generation. I nad objekty uloženými v oblasti old/tenured generation se samozřejmě spouští správce paměti, ovšem s mnohem menší frekvencí. I samotný algoritmus správce paměti používaný pro tuto paměťovou oblast je poněkud odlišný, protože musí mj. správně vyhodnocovat reference „starých“ objektů na objekty „mladé“ a naopak. Ovšem i samotná oblast young generation je rozdělena na několik podoblastí. První z těchto podoblastí se nazývá eden a právě v této podoblasti jsou vytvářeny nové objekty.

Obrázek 2: Oblast haldy nazvaná young generation je většinou rozdělena na tři podoblasti: eden, survivor space #1 a survivor space #2. Na některých architekturách je však namísto toho použita větší kapacita haldy pro eden a jedinou podoblast survivor space (jak však uvidíme dále, není to případ architektury i386).

V případě, že je celá kapacita edenu již alokována, zavolá se správce paměti, který celým edenem projde a vyřadí ty objekty, které již nejsou aktivní (živé). Zbylé objekty, kterých je (alespoň podle statistických měření provedených nad typickými aplikacemi) mnohem méně, jsou zkopírovány do jedné z oblastí nazvaných survivor space. U architektur, v nichž se používají dvě podoblasti survivor space, je vždy jedna z těchto podoblastí prázdná a druhá podoblast obsahuje objekty zkopírované z edenu popř. z podoblasti první (kopírováním objektů se totiž survivor space kromě jiného též defragmentuje). Teprve poté, co je objekt několikrát zkopírován mezi oběma podoblastmi survivor space, může být přesunut z young generation do větší a méně často modifikované oblasti tenured generation.

2. Základní způsoby monitorování správce paměti

O nejzákladnějším a současně i nejjednodušším způsobu monitorování práce správce paměti jsme si již řekli v předchozích částech tohoto seriálu. Pokud při startu javovské aplikace použijeme volbu -verbose:gc, bude se na standardní výstup postupně vypisovat každé zavolání správce paměti – jedná se jak o správce paměti běžícího nad young generation, tak i o správce pracujícího s tenured generation. Pro připomenutí si znovu uveďme náš demonstrační příklad používající konkatenaci řetězce v programové smyčce, což ve svém důsledku vede k opakovanému volání správce paměti nad nově vytvořenými objekty typu String a StringBuilder s velmi krátkou dobou života, protože tyto objekty přestávají být platné již po proběhnutí jediné iterace (jednoho cyklu) programové smyčky. Zdrojový kód tohoto testovacího příkladu je následující:

public class ConcatTest1
{
    private static final int LOOP_COUNT = 10000;

    public static String createString()
    {
        String str = "";
        for (int i = 0; i < LOOP_COUNT; i++)
        {
            str += i + " ";
        }
        return str;
    }

    public static void main(String[] args)
    {
        String str = createString();
        System.gc(); // lze zakomentovat
        System.out.println("String length: " + str.length());
    }
}

Obrázek 3: Objekty běžné velikosti jsou vždy vytvářeny v edenu. Pouze příliš velké objekty jsou alokovány již přímo v oblasti old/tenured generation, což však může způsobovat problémy – fragmentaci této oblasti, frekventované odstraňování objektů z old/tenured generation atd.

Po přeložení výše uvedeného programu a jeho spuštění pomocí volby…

java -verbose:gc ConcatTest1

… se na standardní výstup vypíše množství údajů o spuštění správce paměti nad young generation. Každý výpis vypadá následovně:

[GC 2257K->153K(7744K), 0.0009370 secs]

Co však jednotlivé údaje znamenají? Písmena GC značí, že se spustil správce paměti nad young generation; pokud by se však spustil správce paměti i nad tenured generation, vypsal by se namísto řetězce GC řetězec Full GC. Hodnota 2257K značí celkovou velikost všech živých objektů před spuštěním správce paměti a hodnota 153K naproti tomu celkovou velikost objektů, které zůstaly živé i po proběhnutí celého cyklu správy paměti (zde můžeme vidět, že správce paměti byl při čištění edenu skutečně velmi úspěšný, protože v tomto – poněkud umělém – příkladu dokázal uvolnit více než 93% paměťové kapacity edenu). Ve skutečnosti se nemusí jednat o skutečně živé objekty, ale taktéž o objekty, na něž existuje alespoň jedna reference od objektu uloženého v oblasti tenured generation. Takový objekt sice již nemusí být živý, což však správce paměti zatím nemůže vědět, protože prozatím prochází pouze oblast young generation.

Číslo umístěné v závorkách (7744K) je celková velikost paměti využitelná pro alokaci objektů. Jedná se o maximální kapacitu haldy, od níž je odečtena velikost jedné podoblasti survivor space (to je ostatně logické, protože pouze vždy jedna z těchto dvou oblastí je používána pro defragmentaci young generation). Význam posledního čísla je zřejmý – jedná se o dobu běhu správce paměti. Na tomto místě je vhodné poznamenat, že správce paměti většinou běží v samostatném vláknu a NEmusí zamykat všechny objekty, s nimiž pracuje, což znamená, že vlákno/vlákna aplikace ve skutečnosti nemusí být pozastavena na takovou dobu, jaká je uvedena ve výpisu.

Obrázek 4: Jakmile je eden zcela zaplněn, nebo pokud v něm již není místo pro vytvoření dalšího objektu, spustí se správce paměti, který poctivě projde všechny zde uložené objekty. Aktivní objekty jsou uloženy do jedné z oblastí survivor space, objekty neaktivní jsou odstraněny.

3. Podrobnější monitorování činnosti správce paměti

Při nutnosti podrobnějšího sledování činnosti správce paměti je však výše zmíněná volba -verbose:gc nedostatečná, protože nám nedává přesnou představu o tom, jakým způsobem se jednotlivé oblasti, na něž je halda rozdělena, skutečně využívají (a vlastně ani nemusíme vědět, jak jsou jednotlivé oblasti velké). V takovém případě může být zajímavější a užitečnější použít volbu -XX:PrintGCDetails, například následujícím způsobem:

java -XX:+PrintGCDetails ConcatTest1

Způsob výpisu informací při použití této volby sice není doposud plně standardizován a může se dokonce lišit podle verze JVM, nicméně v OpenJDK 6 se při každém zavolání správce paměti nad oblastí young generation vypíše následující řádek:

[GC [DefNew: 2159K->102K(2368K), 0.0014200 secs] 2268K->211K(7744K), 0.0015800 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

První tři číslice ve výpisu 2159K->102K (2368K) mají stejný význam, jaký jsme si již řekli v předchozí kapitole, ovšem vztahují se pouze k oblasti young generation. Teprve trojice číslic 2268K->211K(7744K) se vztahuje k celé haldě (tudíž by rozdíl 2159K-102K měl být stejný jako 2268K-211K, samozřejmě za předpokladu, že není vytvořen žádný obří objekt, který by byl přímo alokován v tenured generation).

Obrázek 5: Před či po projití edenu se navíc projdou i všechny objekty uložené v jedné z podoblastí survivor space a opět je na základě trasování objektů zhodnoceno, který objekt je aktivní, který neaktivní a který dostatečně starý na to, aby byl přesunut do oblasti old generation.

Pokud dojde ke spuštění správce paměti i nad oblastí tenured generation, tj. oblastí, kde se nachází dostatečně staré a/nebo velké objekty, je výpis poněkud pozměněn:

[Full GC (System) [Tenured: 125K->110K(5376K), 0.0792740 secs] 170K->110K(7872K), [Perm : 24K->24K(12288K)], 0.0794570 secs] [Times: user=0.07 sys=0.00, real=0.08 secs]

Zde si kromě na první pohled viditelných rozdílů (odlišná slova na začátku zprávy) povšimněte taktéž toho, že celý běh správce paměti je nad oblastí tenured generation pomalejší (zde více než 50×) a taktéž toho, že vlastně v této oblasti nedošlo k významné redukci objektů alokovaných na haldě. To je ovšem v pořádku, protože to jen ukazuje na to, že metoda „rozděl a panuj“ byla vhodně použita – většinu dočasných objektů se podařilo „zlikvidovat“ již v oblasti young generation.

4. Statistika vypsaná při použití volby -XX:PrintGCDetails

Při použití volby -XX:PrintGCDetails se před ukončením aplikace vypíše zajímavá statistika (následující formát opět platí pro případ, že je tato volba použita na OpenJDK 6):

Heap
 def new generation   total 2496K, used 1288K [0x7feb0000, 0x80160000, 0x85200000)
  eden space 2240K,  57% used [0x7feb0000, 0x7fff20c0, 0x800e0000)
  from space 256K,   0% used [0x800e0000, 0x800e0000, 0x80120000)
  to   space 256K,   0% used [0x80120000, 0x80120000, 0x80160000)
 tenured generation   total 5376K, used 394K [0x85200000, 0x85740000, 0x8f8b0000)
   the space 5376K,   7% used [0x85200000, 0x85262b60, 0x85262c00, 0x85740000)
 compacting perm gen  total 12288K, used 28K [0x8f8b0000, 0x904b0000, 0x938b0000)
   the space 12288K,   0% used [0x8f8b0000, 0x8f8b72d8, 0x8f8b7400, 0x904b0000)
    ro space 10240K,  73% used [0x938b0000, 0x94006e40, 0x94007000, 0x942b0000)
    rw space 12288K,  60% used [0x942b0000, 0x949ef9f8, 0x949efa00, 0x94eb0000)

Na předchozím výpisu je nám virtuálním strojem jazyka Java prozrazeno, jak jsou vlastně jednotlivé oblasti young generation (zde je přejmenovaná na def new generation) a tenured generation velké a současně jsou vypsány i kapacity všech tří podoblastí v young generation – edenu a obou podoblastí survivor space (tyto oblasti jsou na výpisu nazvané podle své funkce from a to). Povšimněte si, že kapacita young generation je rovna 2496K, což je součet kapacity edenu (2240K) a kapacity jedné z podoblastí survivor space (256K) – důvod pro tento výpočet jsme si již vysvětlili o několik odstavců výše.

Poznámka: volbu -XX:PrintGCDetails je možné kombinovat s volbou -XX:PrintGCTimeS­tamps, čímž dosáhneme i výpisu časových značek odpovídajících volání správce paměti. Samotná volba -XX:PrintGCTimeS­tamps však nemá žádný efekt; musí být použita spolu s některou volbou povolující výpis informací o běhu správce paměti:

java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps ConcatTest1

Výpis je nepatrně pozměněn, ovšem časová značka může být v některých případech důležitá, například při sledování, v jakém okamžiku je běžící program pozastaven na delší dobu, kdy je halda nejvíce používána atd.:

27.296: [GC 27.296: [DefNew: 2289K->190K(2368K), 0.0036770 secs] 7552K->5548K(7744K), 0.0038540 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
23.893: [Full GC (System) 23.894: [Tenured: 394K->203K(5376K), 0.1400310 secs] 1638K->203K(7872K), [Perm : 26K->26K(12288K)], 0.1402740 secs] [Times: user=0.07 sys=0.00, real=0.14 secs]

5. Parametry ovlivňující funkci správce paměti

V předchozích kapitolách jsme si řekli základní informace o tom, jakým způsobem lze i za pomocí jednoduchých prostředků (tj. pouze vlastního JRE a nástrojů, které se v JRE nachází) monitorovat funkci správců paměti. Ovšem monitorování samo o sobě by nemělo velký význam tehdy, pokud by nebylo možné parametry správy paměti vhodným způsobem modifikovat. Při spouštění JRE je skutečně možné chování správce paměti ovlivnit, a to hned několika volbami. Nejprve se znovu podívejme na to, jak je rozdělena paměťová oblast haldy (pro jednoduchost nyní budeme ignorovat oblast PermGen používanou pro poněkud odlišné účely). Obě hlavní oblasti haldy – young generation a tenured generation – i všechny důležité podoblasti jsou zobrazeny na šestém a sedmém obrázku. Dva základní parametry, kterými se virtuální stroj jazyka Java při svém spouštění řídí, je parametr určující maximální velikost haldy (rezervovanou při startu virtuálního stroje) a parametr určující, jak velká paměť je skutečně virtuálnímu stroji po rezervaci přiřazena.

Obrázek 6: Dvě hlavní oblasti haldy a podrobnější pohled na podoblast young generation.

Tyto dva parametry je možné při startu virtuálního stroje Javy specifikovat pomocí známých voleb -Xmx (rezervovaná velikost paměti pro haldu) a -Xms (paměť skutečně alokovaná pro haldu při startu virtuálního stroje). Je zapotřebí si dát pozor na to, že se původní hodnoty obou parametrů liší v závislosti na typu operačního systému i na použité platformě. Hodnoty obou parametrů mohou být samozřejmě shodné, což je také v mnoha případech nejlepší varianta, zejména ve chvíli, kdy je virtuální stroj jazyka Java spuštěn na systémech s malými paměťovými stránkami, což může být i případ Linuxu, zejména tehdy, když není povolena podpora pro Large/Huge pages.

Obrázek 7: Dvě hlavní oblasti haldy a podrobnější pohled na podoblast tenured generation.

Mimochodem, velikost paměťových stránek lze zjistit příkazem getconf PAGESIZE vracejícího údaje v bajtech. Hodnoty parametrů -Xmx a -Xms ovlivňují jak velikost oblasti young generation, tak i oblasti tenured generation, což mj. znamená, že se v případě malé kapacity haldy bude správce paměti spouštět s větší frekvencí, protože se bude častěji zaplňovat eden a objekty se budou kvůli menší velikosti survivor space(s) častěji kopírovat do tenured generation i v případě, že ještě nedosáhly potřebného „stáří“.

6. Chování správce paměti při explicitním nastavení velikosti haldy

Chování správce paměti při explicitním nastavení velikosti haldy si můžeme vyzkoušet na našem demonstračním příkladu:

java -Xmx10M -Xms10M -XX:+PrintGCDetails ConcatTest1

Na dalším výpisu je zobrazeno posledních několik řádků zpráv vypisovaných správcem paměti. Povšimněte si velikostí jednotlivých oblastí a podoblastí vypočtených samotným virtuálním strojem Javy pouze na základě celkové velikosti haldy:

[GC [DefNew: 2865K->191K(3072K), 0.0022210 secs] 2974K->299K(9920K), 0.0023930 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (System) [Tenured: 108K->203K(6848K), 0.1063970 secs] 587K->203K(9920K), [Perm : 26K->26K(12288K)], 0.1066690 secs] [Times: user=0.07 sys=0.01, real=0.11 secs]
String length: 48890
Heap
 def new generation   total 3072K, used 57K [0x8eeb0000, 0x8f200000, 0x8f200000)
  eden space 2752K,   2% used [0x8eeb0000, 0x8eebe4a0, 0x8f160000)
  from space 320K,   0% used [0x8f160000, 0x8f160000, 0x8f1b0000)
  to   space 320K,   0% used [0x8f1b0000, 0x8f1b0000, 0x8f200000)
 tenured generation   total 6848K, used 203K [0x8f200000, 0x8f8b0000, 0x8f8b0000)
   the space 6848K,   2% used [0x8f200000, 0x8f232fc8, 0x8f233000, 0x8f8b0000)
 compacting perm gen  total 12288K, used 28K [0x8f8b0000, 0x904b0000, 0x938b0000)
   the space 12288K,   0% used [0x8f8b0000, 0x8f8b72f8, 0x8f8b7400, 0x904b0000)
    ro space 10240K,  73% used [0x938b0000, 0x94006e40, 0x94007000, 0x942b0000)
    rw space 12288K,  60% used [0x942b0000, 0x949ef9f8, 0x949efa00, 0x94eb0000)

Zajímavé je také zjistit, kolikrát byl v tomto případě zavolán správce paměti nad oblastí young generation:

java -Xmx10M -Xms10M -XX:+PrintGCDetails ConcatTest1 | grep DefNew | wc -l
694

A taktéž celkový čas běhu aplikace:

time java -Xmx10M -Xms10M ConcatTest1

real    0m14.300s
user    0m11.477s
sys     0m0.544s

Jak jsem se již zmínil v předchozí kapitole, má velikost paměti rezervované (popř. alokované) pro haldu velký vliv na výkonnost celého virtuálního stroje a tím i aplikace, která je ve virtuálním stroji spuštěna. Vyzkoušejme nejprve extrémní příklad, kdy je kapacita haldy snížena na velmi malé hodnoty, například na 2MB. Zde je nutné poznamenat, že u větších aplikací například při spouštění aplikačního serveru, je za „malou kapacitu haldy“ považováno i několik (desítek) megabajtů, takže se v případě výkonnostních problémů mohou programátoři a/nebo administrátoři zaměřit taktéž na volby -Xmx a -Xms. Jak se změní doba běhu aplikace i další vlastnosti JRE při snížení maximální rezervované kapacity haldy na 2MB? Nejprve se podívejme, jakým způsobem virtuální stroj rozdělí přidělenou 2MB oblast mezi různé oblasti a podoblasti haldy:

Heap
 def new generation   total 960K, used 20K [0x8f2b0000, 0x8f3b0000, 0x8f4b0000)
  eden space 896K,   2% used [0x8f2b0000, 0x8f2b5028, 0x8f390000)
  from space 64K,   0% used [0x8f3a0000, 0x8f3a0000, 0x8f3b0000)
  to   space 64K,   0% used [0x8f390000, 0x8f390000, 0x8f3a0000)
 tenured generation   total 1024K, used 201K [0x8f4b0000, 0x8f5b0000, 0x8f8b0000)
   the space 1024K,  19% used [0x8f4b0000, 0x8f4e2428, 0x8f4e2600, 0x8f5b0000)
 compacting perm gen  total 12288K, used 27K [0x8f8b0000, 0x904b0000, 0x938b0000)
   the space 12288K,   0% used [0x8f8b0000, 0x8f8b6e08, 0x8f8b7000, 0x904b0000)
    ro space 10240K,  73% used [0x938b0000, 0x94006e40, 0x94007000, 0x942b0000)
    rw space 12288K,  60% used [0x942b0000, 0x949ef9f8, 0x949efa00, 0x94eb0000)

Celková doba běhu aplikace v tomto případě pravděpodobně uživatele příliš nepotěší:

time java -Xmx2M -Xms2M ConcatTest1

real   0m45.910s
user   0m39.334s
sys    0m1.696s

(trojnásobné zpomalení)

…stejně jako celkový počet spuštění správce paměti (počet jeho spuštění koresponduje s celkovým nárůstem doby běhu programu, což u takto jednoduchého algoritmu pravděpodobně nepřekvapí)…

java -Xmx2M -Xms2M -XX:+PrintGCDetails ConcatTest1 | grep DefNew | wc -l
2241

(cca trojnásobný počet spuštění správce paměti)

Vyzkoušejme nyní druhý extrém – nastavení rezervované velikosti pro haldu na 100MB:

Heap
 def new generation   total 30720K, used 548K [0x894b0000, 0x8b600000, 0x8b600000)
  eden space 27328K,   2% used [0x894b0000, 0x895392b8, 0x8af60000)
  from space 3392K,   0% used [0x8af60000, 0x8af60000, 0x8b2b0000)
  to   space 3392K,   0% used [0x8b2b0000, 0x8b2b0000, 0x8b600000)
 tenured generation   total 68288K, used 203K [0x8b600000, 0x8f8b0000, 0x8f8b0000)
   the space 68288K,   0% used [0x8b600000, 0x8b632fc8, 0x8b633000, 0x8f8b0000)
 compacting perm gen  total 12288K, used 28K [0x8f8b0000, 0x904b0000, 0x938b0000)
   the space 12288K,   0% used [0x8f8b0000, 0x8f8b72f8, 0x8f8b7400, 0x904b0000)
    ro space 10240K,  73% used [0x938b0000, 0x94006e40, 0x94007000, 0x942b0000)
    rw space 12288K,  60% used [0x942b0000, 0x949ef9f8, 0x949efa00, 0x94eb0000)

Celkový čas běhu programu:

time java -Xmx100M -Xms100M ConcatTest1

real    0m11.673s
user    0m10.081s
sys     0m0.408s

Počet volání správce paměti:

java -Xmx100M -Xms100M -XX:+PrintGCDetails ConcatTest1 | grep DefNew | wc -l
68

(10× menší než při nastavení 10MB a 33× menší než u 2MB)

7. Změna poměru mezi velikostí jednotlivých oblastí na haldě

Po volbách -Xmx a -Xms má největší vliv na výkonnost správců paměti podíl velikostí oblastí young generation a tenured generation. Tento poměr lze nastavit pomocí volby -XX:NewRatio=x, kde x je celočíselný (!) poměr mezi velikostí tenured generation a young generation. Na tomto místě je vhodné si uvědomit, že do celkové velikosti oblasti young generation se při výpočtu poměru započítávají všechny tři podoblasti, tj. jak eden, tak i obě podoblasti survivor space(s). Celková maximální kapacita paměti vyhrazené pro haldu se sice nemění, i tak však má tento parametr v mnoha případech velký vliv na výkonnost či propustnost Javovských aplikací – právě proto je vhodné při výkonnostních problémech provést měření při změnách tohoto parametru, protože se u některých aplikací může podařit výkon aplikací zvýšit aniž by bylo nutné obětovat další operační paměť pro haldu.

Ostatně se o významu tohoto parametru můžeme přesvědčit sami, například pomocí jednoduchého skriptu, který do souboru gc_count.txt uloží počet volání správce paměti nad young generation, přičemž celková velikost haldy zůstává stále nastavena na 10MB:

#!/bin/bash

for i in `seq 1 10`;
do
    java -Xmx10M -Xms10M -XX:NewRatio=${i} \
        -XX:+PrintGCDetails ConcatTest1 | grep DefNew | wc -l >> gc_count.txt
done

Naměřené výsledky jsem pro větší přehlednost uspořádal do tabulky, do níž je navíc přidán i sloupec s dobou běhu aplikace. Z tabulky je patrné, že i při omezené velikosti haldy (tu může limitovat jak kapacita operační paměti, tak i například kapacita alokovaná pro virtuální stroj, což začíná být v době cloudů čím dál tím více důležitější veličina), je možné s vhodnými spouštěcími parametry dosáhnout zajímavých zlepšení:

Velikost haldy Poměr tenured:young Volání GC pro young Čas běhu
10 M 1:1 463 10.86 s
10 M 2:1 694 11.37 s
10 M 3:1 939 12.01 s
10 M 4:1 1163 12.41 s
10 M 5:1 1378 13.38 s
10 M 6:1 1700 14.33 s
10 M 7:1 1926 14.91 s
10 M 8:1 2052 16.29 s
10 M 9:1 2239 16.87 s
10 M 10:1 2238 16.80 s

V mnoha případech nám však nebude nastavení celočíselného poměru mezi young generation a tenured generation dostačovat. Namísto této hodnoty lze taktéž použít přepínače -XX:NewSize=ka­pacita a -XX:MaxNewSize=ka­pacita, pomocí nichž je možné nastavit minimální a maximální velikost oblasti young generation v bajtech, kilobajtech, megabajtech atd. Velikost tenured generation se v tomto případě dopočítá automaticky na základě maximální povolené velikosti haldy. Podívejme se na konkrétní příklad, kde je velikost young generation nastavena na hodnotu 8192+1024+102­4=10240 kB a velikost tenured generation na 11M-10M=1 MB (1024 kB):

java -Xmx11M -XX:NewSize=10M -XX:+PrintGCDetails

... informace o možných přepínačích ...

Heap
 def new generation   total 9216K, used 491K [0x8eab0000, 0x8f4b0000, 0x8f4b0000)
  eden space 8192K,   6% used [0x8eab0000, 0x8eb2af00, 0x8f2b0000)
  from space 1024K,   0% used [0x8f2b0000, 0x8f2b0000, 0x8f3b0000)
  to   space 1024K,   0% used [0x8f3b0000, 0x8f3b0000, 0x8f4b0000)
 tenured generation   total 1024K, used 0K [0x8f4b0000, 0x8f5b0000, 0x8f8b0000)
   the space 1024K,   0% used [0x8f4b0000, 0x8f4b0000, 0x8f4b0200, 0x8f5b0000)
 compacting perm gen  total 12288K, used 43K [0x8f8b0000, 0x904b0000, 0x938b0000)
   the space 12288K,   0% used [0x8f8b0000, 0x8f8baf88, 0x8f8bb000, 0x904b0000)
    ro space 10240K,  73% used [0x938b0000, 0x94006e40, 0x94007000, 0x942b0000)
    rw space 12288K,  60% used [0x942b0000, 0x949ef9f8, 0x949efa00, 0x94eb0000)

Nyní se opět na chvíli vrátíme k našemu testovacímu příkladu. Nejprve ho spustíme s velikostí haldy nastavenou na 5 MB, ovšem s tím, že způsob rozdělení této kapacity mezi young generation a tenured generation ponecháme plně na virtuálním stroji Javy:

time java -Xmx5M -Xms5M -XX:+PrintGCDetails ConcatTest1 | grep DefNew | wc -l
1311    - počet volání GC

real    0m15.715s
user    0m14.529s
sys 0m0.984s

Ovšem vzhledem k tomu, že víme, že náš příklad vytváří velké množství objektů s malou životností, které tudíž nemusí (a nemají!) skončit v oblasti tenured generation, můžeme velikost této oblasti snížit na pouhý jeden megabajt, čímž se jak drasticky sníží počet volání správce paměti, tak se i zmenší celková doba běhu aplikace:

time java -Xmx5M -Xms5M -XX:NewSize=4M -XX:+PrintGCDetails ConcatTest1 | grep DefNew | wc -l
571     - počet volání GC

real    0m11.994s
user    0m11.221s
sys 0m0.612s

Poznámka: opět pro jistotu poznamenávám, že pro reálné aplikace budou naměřené hodnoty i vhodné nastavované parametry s velkou pravděpodobností zcela odlišné, protože náš demonstrační příklad je v určitém ohledu extrémem.

8. Výsledky měření

Nikdy nezaškodí taktéž uvést nějakou (samozřejmě vhodně zfalšovanou :-) statistiku, ideálně podepřenou grafy. S generováním dat tvořících základ pro grafy nám může pomoci jednoduchý skript, pomocí něhož zjistíme jak dobu běhu testovací aplikace při použití různé velikosti haldy, tak i celkový počet volání správce paměti:

#!/bin/bash

# vysledek: dvojice textovych souboru
# obsahujicich pocet spusteni GC a celkovou
# dobu behu testovaciho programu

for i in `seq 2 100`;
do
    echo "Spoustim JVM s nastavenou velikosti heapu na ${i}MB"
    # POZOR! nechceme volat interni prikaz time z BASHe
    /usr/bin/time -o times.txt -a -f "%U" \
        java -Xmx${i}M -Xms${i}M -XX:+PrintGCDetails \
        ConcatTest1 | grep DefNew | wc -l >> gc_count.txt
done

Z vygenerovaných dat lze jednoduše vytvořit grafy, na kterých je patrná nelineární závislost mezi celkovou velikostí haldy a časem běhu aplikace. Z grafů je patrné také to, že příliš malá velikost haldy vede k mnohem delší době trvání běhu aplikace (popř. i ke vzniku výjimek typu java.lang.OutOf­MemoryError), ovšem na druhou stranu nemá moc smyslu nastavovat ani příliš velkou kapacitu haldy, protože od určitého okamžiku (zcela závislého na povaze běžící aplikace i zpracovávaných dat!) se již další zvyšování velikosti haldy projeví na celkovém času běhu aplikace (popř. na průchodnosti aplikace) jen zcela nepatrně.

root_podpora

Obrázek 8: Celková doba běhu testovací aplikace (vyjádřená v sekundách) v závislosti na maximální velikosti haldy (vyjádřené v MB)

Obrázek 9: Počet spuštění správce paměti v závislosti na maximální velikosti haldy (vyjádřené v MB).

9. Odkazy na Internetu

  1. Amdahl's law
    http://en.wiki­pedia.org/wiki/Am­dahl_law
  2. Garbage collection (computer science)
    http://en.wiki­pedia.org/wiki/Gar­bage_collecti­on_(computer_sci­ence)
  3. Dr. Dobb's | G1: Java's Garbage First Garbage Collector
    http://www.drdob­bs.com/article/prin­tableArticle.jhtml?ar­ticleId=219401061­&dept_url=/ja­va/
  4. Java's garbage-collected heap
    http://www.ja­vaworld.com/ja­vaworld/jw-08–1996/jw-08-gc.html
  5. Compressed oops in the Hotspot JVM
    http://wikis.sun­.com/display/Hot­SpotInternals/Com­pressedOops
  6. 32-bit or 64-bit JVM? How about a Hybrid?
    http://blog.ju­ma.me.uk/2008/10/­14/32-bit-or-64-bit-jvm-how-about-a-hybrid/
  7. Compressed object pointers in Hotspot VM
    http://blogs.sun­.com/nike/entry/com­pressed_objec­t_pointers_in_hot­spot
  8. Java HotSpot™ Virtual Machine Performance Enhancements
    http://downlo­ad.oracle.com/ja­vase/7/docs/techno­tes/guides/vm/per­formance-enhancements-7.html
  9. Using jconsole
    http://downlo­ad.oracle.com/ja­vase/1.5.0/doc­s/guide/manage­ment/jconsole­.html
  10. jconsole – Java Monitoring and Management Console
    http://downlo­ad.oracle.com/ja­vase/1.5.0/doc­s/tooldocs/sha­re/jconsole.html
  11. Great Computer Language Shootout
    http://c2.com/cgi/wi­ki?GreatCompu­terLanguageSho­otout
  12. x86–64
    http://en.wiki­pedia.org/wiki/X86–64
  13. Physical Address Extension
    http://en.wiki­pedia.org/wiki/Phy­sical_Address_Ex­tension
  14. Java performance
    http://en.wiki­pedia.org/wiki/Ja­va_performance
  15. 1.6.0_14 (6u14)
    http://www.ora­cle.com/technet­work/java/java­se/6u14–137039.html?ssSou­rceSiteId=otncn
  16. Update Release Notes
    http://www.ora­cle.com/technet­work/java/java­se/releasenotes-136954.html
  17. 4.10 Limitations of the Java Virtual Machine
    http://java.sun­.com/docs/book­s/jvms/second_e­dition/html/Clas­sFile.doc.html#88659
  18. Java™ Platform, Standard Edition 7 Binary Snapshot Releases
    http://dlc.sun­.com.edgesuite­.net/jdk7/bina­ries/index.html
  19. Trying the prototype
    http://mail.o­penjdk.java.net/pi­permail/lambda-dev/2010-August/002179.html
  20. Better closures (for Java)
    http://blogs.sun­.com/jrose/en­try/better_clo­sures
  21. Lambdas in Java: An In-Depth Analysis
    http://www.in­foq.com/articles/lam­bdas-java-analysis
  22. Class ReflectiveOpe­rationExcepti­on
    http://downlo­ad.java.net/jdk7/doc­s/api/java/lan­g/ReflectiveO­perationExcep­tion.html
  23. Proposal: Indexing access syntax for Lists and Maps
    http://mail.o­penjdk.java.net/pi­permail/coin-dev/2009-March/001108.html
  24. Proposal: Elvis and Other Null-Safe Operators
    http://mail.o­penjdk.java.net/pi­permail/coin-dev/2009-March/000047.html
  25. Java 7 : Oracle pushes a first version of closures
    http://www.bap­tiste-wicht.com/2010/05­/oracle-pushes-a-first-version-of-closures/
  26. Groovy: An agile dynamic language for the Java Platform
    http://groovy­.codehaus.org/O­perators
  27. Better Strategies for Null Handling in Java
    http://www.sli­deshare.net/Step­han.Schmidt/bet­ter-strategies-for-null-handling-in-java
  28. Control Flow in the Java Virtual Machine
    http://www.ar­tima.com/under­thehood/flowP­.html
  29. Java Virtual Machine
    http://en.wiki­pedia.org/wiki/Ja­va_virtual_machi­ne
  30. ==, .equals(), compareTo(), and compare()
    http://leepoin­t.net/notes-java/data/expres­sions/22compa­reobjects.html
  31. New JDK7 features
    http://openjdk­.java.net/pro­jects/jdk7/fe­atures/
  32. Project Coin: Bringing it to a Close(able)
    http://blogs.sun­.com/darcy/en­try/project_co­in_bring_close
  33. ClosableFinder source code
    http://blogs.sun­.com/darcy/re­source/Projec­tCoin/Closeable­Finder.java
  34. Joe Darcy blog about JDK
    http://blogs.sun­.com/darcy
  35. Java 7 – more dynamics
    http://www.bap­tiste-wicht.com/2010/04­/java-7-more-dynamics/
  36. ArrayList (JDK 1.4)
    http://downlo­ad.oracle.com/ja­vase/1.4.2/doc­s/api/java/util/A­rrayList.html

Byl pro vás článek přínosný?