Hlavní navigace

Monitorování procesů a správa paměti v JDK6 a JDK7 (1)

6. 1. 2011
Doba čtení: 21 minut

Sdílet

V sedmé části seriálu o vlastnostech JDK (samozřejmě včetně OpenJDK) si řekneme základní informace o správě paměti ve virtuálním stroji jazyka Java. Taktéž se seznámíme se základními způsoby monitorování procesů běžících v JVM – zaměříme se přitom (alespoň prozatím) pouze na nástroje, které jsou dostupné přímo v JDK6 a JDK7.

Obsah

1. Automatická správa paměti ve virtuálním stroji jazyka Java

2. Optimalizace související se správou paměti

3. Vyvolání správce paměti z běžící aplikace a metoda finalize()

4. Demonstrační příklady

5. Základní způsoby monitorování operací prováděných správcem paměti

6. Alokace paměti pro objekty v JVM

7. Jak nákladné je vytváření a rušení objektů?

8. „Skryté“ vytváření objektů

9. Odkazy na Internetu

1. Automatická správa paměti ve virtuálním stroji jazyka Java

Každý vývojář, který začíná psát své první aplikace v programovacím jazyku Java, se poměrně brzy dozví, že tento jazyk (samozřejmě ve spolupráci s jeho běhovým prostředím – JRE) podporuje automatickou správu paměti. To mj. znamená, že se programátoři ve většině případů nemusí explicitně starat o to, jakým způsobem se již nepotřebné objekty z paměti uvolní a jakým způsobem je vlastně paměť organizovaná, popř. zda a jak se tato paměť defragmentuje. Podpora pro automatickou správu paměti je zajisté velmi užitečná vlastnost a vývojáři mohou po určité době dokonce dojít k názoru, že se o přidělovanou paměť vůbec nemusí starat. To však není v některých případech pravda, protože vytváření a rušení objektů (a s ním související alokace a dealokace bloků na takzvané haldě – heapu) je operace poměrně náročná na výpočetní výkon mikroprocesoru, takže správné zacházení s pamětí – tj. především hlídání toho, zda se například zbytečně (a mnohdy poněkud skrytě) nevytváří dočasné objekty – může někdy i velmi významně ovlivnit rychlost běhu aplikace popř. eliminovat prodlevy způsobené automatickou správou paměti.

U desktopových aplikací se někdy celá problematika správy paměti ignoruje nebo zlehčuje se slovy „to je prostě vlastnost pomalé Javy“, ovšem u serverových aplikací, které například musejí současně vyřizovat několik desítek nebo i stovek požadavků za sekundu, je možné vhodnými a cílenými zásahy do programového kódu a nastavením některých parametrů JVM zvýšit propustnost systému, a to v některých případech i několikanásobně (je to ostatně poněkud podobné situaci, s níž se často setkávají programátoři píšící SQL příkazy – pro jednoduché a malé databáze postačuje použít jakýkoli SQL dotaz, který „nějak“ provede požadovanou operaci, ovšem u složitých a rozsáhlých databází je zapotřebí zjišťovat složitosti dotazů a provádět jejich optimalizace nebo dokonce změnit celou strukturu databáze). Samozřejmě ještě větší dopad má optimalizovaný program v případě použití správců paměti orientovaných na systémy běžící v reálném čase (RT-systems). Ovšem jaké optimalizace lze v souvislosti se správou paměti provádět v Javě, když jsme si o odstavec výše řekli, že se paměť spravuje automaticky?

2. Optimalizace související se správou paměti

Mezi vhodné zásahy ovlivňující funkci a časové i paměťové nároky automatické správy paměti patří především změna či optimalizace vlastního algoritmu (což ovšem není předmětem tohoto článku, ale analýzy konkrétního problému). S tím souvisí i použití vhodných datových struktur, například lineárně vázaných seznamů namísto seznamů implementovaných polem, kdy se i přes snahu automatického alokátoru pole systém mnohdy nevyhne jeho realokacím – nebezpečnost častých realokací stoupá například při nasazení aplikace do ostrého provozu, v němž musí pracovat s jinými a především rozsáhlejšími daty, než v provozu testovacím (teoreticky by testovací provoz měl simulovat provoz reálný, ale jak všichni víme, teorie se od praxe mnohdy liší, například při častých změnách ve specifikaci projektu, kdy se i poměrně závažné úpravy implementují na poslední chvíli…). Dále pak je možné provést úpravu stávajícího algoritmu takovým způsobem, aby se eliminovalo zbytečné vytváření dočasných objektů – typickým příkladem je náhrada objektů typu String za objekty typu StringBuffer nebo StringBuilder v případě, že se často provádí spojování řetězců.

V případě, kdy není možné změnit použitý algoritmus ani ho jiným způsobem upravit, je ještě v záloze další strategie spočívající v nastavení vhodných parametrů paměti spravované JVM (haldy) a konečně nastavení parametrů automatického správce paměti (garbage collectoru), popř. volba, který ze správců paměti má být použit (v současných JVM je jich dostupných více). Důležité je samozřejmě také zjistit, jak se automatický správce paměti chová při běhu aplikace, popř. které části aplikace je nutné upravit (to vůbec nemusí být při studiu statického programového kódu zřejmé!). K tomuto účelu slouží různé monitorovací a logovací nástroje, které si popíšeme v následujících kapitolách. Příště na tuto problematiku navážeme, protože si popíšeme konkrétní způsob práce dostupných správců paměti (garbage collectorů) v JVM, včetně prozatím nejnovějšího typu správce paměti, který se nazývá Garbage First Garbage Collector nebo též zkráceně G1.

3. Vyvolání správce paměti z běžící aplikace a metoda finalize()

Před popisem základních monitorovacích nástrojů, které jsou dostupné v každé instalaci JDK 6 a JDK 7 si připomeňme, že správce paměti (garbage collector) je spouštěn v samostatném vláknu či více vláknech, takže pracuje paralelně s běžící aplikací. Operace prováděné správcem paměti samozřejmě nemohou probíhat zcela nezávisle na aplikaci, protože se například provádí defragmentace haldy (heapu), s čímž souvisí potřeba některé objekty uzamykat – právě v těchto chvílích je běh aplikace pozastavován, a to i na systémech, které jsou vybaveny mikroprocesory s více jádry. Správce paměti si sám určuje, v jakém okamžiku provede nějakou operaci, ovšem v některých případech je vhodné ho spustit explicitně – tím se například může uvolnit část haldy před provedením nějaké náročné operace. K tomuto účelu slouží metoda System.gc(). Některé objekty navíc mohou implementovat metodu protected void finalize() (její prázdnou deklaraci nalezneme už v kořenové třídě Object) zavolanou těsně před tím, než je objekt skutečně z paměti odstraněn. Takzvanou finalizaci lze vynutit zavoláním metody System.runFina­lization().

Obě zmíněné metody se mohou volat (a mnohdy se také volají) těsně po sobě. Metoda System.gc() totiž většinou spustí první část správce paměti (takzvanou fázi mark – označování), během níž je zjištěno, které objekty je možné zrušit a tyto objekty jsou vhodným způsobem označeny, popř. jsou objekty bez překryté metody finalize() přímo odstraněny v závislosti na tom, který správce paměti je použit. Metoda System.runFina­lization() zavolá (obecně asynchronně!) metody finalize() u zbývajících objektů, které mají být zrušeny a následně tyto objekty skutečně z paměti odstraní. Přitom je nutné si uvědomit, že správce paměti stále běží v samostatném vláknu/vláknech, tj. paralelně s vláknem/vlákny aplikace. Navíc není zaručeno pořadí, v jakém jsou objekty rušeny ani pořadí volání jejich metody finalize() (pokud jsou samozřejmě tyto metody překryty). Jediná zaručená vlastnost je, že se metoda finalize() pro jeden objekt vyvolá pouze jedenkrát. Použití popsaných metod si ukážeme na dvou jednoduchých demonstračních příkladech, jejichž zdrojové kódy jsou uvedeny v následující kapitole.

4. Demonstrační příklady

V prvním demonstračním příkladu je nejprve do seznamu uloženo dvacet instancí třídy Item a následně je celý seznam označen za neaktivní objekt (list=null) s následným zavoláním metod System.gc() a System.runFina­lization(). Povšimněte si, že třída Item má překrytou metodu finalize a taktéž toho, že pro označení objektu jako neaktivního postačuje, aby na něj v běžící aplikaci neexistovala žádná (aktivní) reference. Současní správci paměti totiž při hledání již nepotřebných objektů zjišťují právě to, zda na objekt existuje alespoň jedna přímá či nepřímá reference:

import java.util.ArrayList;
import java.util.List;
 
class Item
{
    private int i;
 
    public Item(int i)
    {
        this.i = i;
    }
 
    @Override
    protected void finalize()
    {
        System.out.println( "finalize object #" + this.i );
    }
}
 
public class GcTest1
{
    public static void main(String[] args)
    {
        List<Item> list = new ArrayList<Item>();
        for ( int i = 0; i < 20; i++ )
        {
            list.add( new Item( i ) );
        }
        System.out.println( "Press any key to run gc" );
        System.console().readLine();
        list = null;
        System.gc();
        System.runFinalization();
        System.out.println( "Press any key to quit" );
        System.console().readLine();
    }
}

Druhý demonstrační příklad je podobný příkladu prvnímu, ovšem namísto označení celého seznamu za neaktivní objekt jsou postupně z tohoto seznamu odstraňovány jednotlivé objekty, které do něj byly předtím vloženy. Podoba třídy Item se přitom nezměnila – stále obsahuje překrytou metodu protected void finalize(), která při svém zavolání vypíše (obecně asynchronně!) hlášení na standardní výstup:

import java.util.ArrayList;
import java.util.List;
 
class Item
{
    private int i;
 
    public Item(int i)
    {
        this.i = i;
    }
 
    @Override
    protected void finalize()
    {
        System.out.println("finalize object #" + this.i);
    }
}
 
public class GcTest2
{
    public static void main(String[] args)
    {
        List<Item> list = new ArrayList<Item>();
        for ( int i = 0; i < 20; i++ )
        {
            list.add( new Item( i ) );
        }
        System.out.println("Press any key to run gc");
        System.console().readLine();
        while (!list.isEmpty())
        {
            list.remove(0);
            System.gc();
            System.runFinalization();
        }
        System.out.println("Press any key to quit");
        System.console().readLine();
    }
}

5. Základní způsoby monitorování operací prováděných správcem paměti

Nejjednodušší způsob, jakým lze monitorovat práci správce paměti (garbage collectoru) spočívá v použití volby -verbose:gc při spouštění libovolné javovské aplikace v JVM. Pokud je tato volba použita, vypisuje správce paměti průběžně základní informace o své práci. Vzhledem k tomu, že správce paměti běží v samostatných vláknech (viz předchozí text), je jeho výstup promíchán s výstupem aplikace, která může používat metody System.out.prin­t|println|for­mat atd. Standardní správce paměti vypisuje na jednom řádku trojici údajů umístěných mezi hranaté závorky: operaci, která byla provedena (GC/Full GC/…), stav haldy (heapu) před a pro provedení operace a taktéž čas trvání operace. Při použití volby -verbose:gc je samozřejmě nutné dbát na to, aby byla uvedena ještě před jménem třídy s metodou main(), která má být spuštěná (popř. kde má být spuštěn pouze blok static, i když toto jeho použití je poněkud neobvyklé), popř. před specifikací Java archivu. Je tomu tak z toho důvodu, aby se tato volba nepředala jako parametr aplikace, ale zpracovala se přímo v JVM.

To znamená, že následující příkaz nebude pracovat korektně:

java -jar freemind.jar -verbose:gc

Zatímco tento příkaz bude pracovat takovým způsobem, jak je očekáváno:

java -verbose:gc -jar freemind.jar

Podívejme se nyní, jaké údaje jsou poslány na standardní výstup při spuštění prvního demonstračního příkladu s výše uvedenou volbou -verbose:gc. Vidíme, že po explicitním spuštění správce paměti a finalizátoru jsou volány jednotlivé metody finalize() objektů umístěných v seznamu. Konkrétní pořadí řádků na standardním výstupu může být odlišné podle toho, jaký správce paměti je použitý a jak jsou v daném okamžiku synchronizována jednotlivá vlákna, v nichž běží aplikace a správce paměti:

java -verbose:gc GcTest1
Press any key to run gc
 
[Full GC 350K->166K(5056K), 0.0263670 secs]
finalize object #19
finalize object #18
finalize object #17
finalize object #16
finalize object #15
finalize object #14
finalize object #13
finalize object #12
finalize object #11
finalize object #10
finalize object #9
finalize object #8
finalize object #7
finalize object #6
finalize object #5
finalize object #4
finalize object #3
finalize object #2
finalize object #1
finalize object #0
Press any key to quit

V případě druhého demonstračního příkladu je standardní výstup odlišný, což je ostatně logické, protože jednotlivé objekty jsou ze seznamu odstraňovány postupně a ihned po jejich odstranění je navíc vynuceno spuštění správce paměti. Povšimněte si taktéž toho, že i když správce paměti nemá (zdánlivě) co na práci, protože se odstranil (opět zdánlivě) jen jeden zcela minimalistický objekt s jediným atributem, trvá celá operace správci paměti poměrně dlouho. Pro představu – zhruba stejný časový interval by trvalo vyrenderování průměrně složité trojrozměrné scény na grafickém akcelerátoru:

java -verbose:gc GcTest2
Press any key to run gc
 
[Full GC 350K->166K(5056K), 0.0223676 secs]
finalize object #0
[Full GC 202K->167K(5056K), 0.0180439 secs]
finalize object #1
[Full GC 203K->167K(5056K), 0.0207814 secs]
finalize object #2
[Full GC 203K->166K(5056K), 0.0221718 secs]
finalize object #3
[Full GC 202K->166K(5056K), 0.0218341 secs]
finalize object #4
[Full GC 202K->166K(5056K), 0.0222224 secs]
finalize object #5
[Full GC 202K->166K(5056K), 0.0215910 secs]
finalize object #6
[Full GC 202K->166K(5056K), 0.0216421 secs]
finalize object #7
[Full GC 202K->166K(5056K), 0.0215164 secs]
finalize object #8
[Full GC 202K->166K(5056K), 0.0216905 secs]
finalize object #9
[Full GC 202K->166K(5056K), 0.0209960 secs]
finalize object #10
[Full GC 202K->165K(5056K), 0.0180615 secs]
finalize object #11
[Full GC 201K->166K(5056K), 0.0179299 secs]
finalize object #12
[Full GC 202K->166K(5056K), 0.0179850 secs]
finalize object #13
[Full GC 202K->166K(5056K), 0.0179671 secs]
finalize object #14
[Full GC 202K->165K(5056K), 0.0179981 secs]
finalize object #15
[Full GC 201K->165K(5056K), 0.0179042 secs]
finalize object #16
[Full GC 201K->165K(5056K), 0.0178833 secs]
finalize object #17
[Full GC 202K->166K(5056K), 0.0178539 secs]
finalize object #18
[Full GC 202K->165K(5056K), 0.0180746 secs]
finalize object #19
Press any key to quit

V některých případech nemusí být vypisování informací o práci správce paměti na standardní výstup tou nejvhodnější metodou. Alternativně lze pomocí volby -Xloggc:jméno_sou­boru přesměrovat pouze informace vypisované správcem paměti do zvoleného souboru. Oproti informacím zapisovaným na standardní výstup je ještě na začátku každého řádku doplněna informace o časovém okamžiku (měřeném v sekundách od startu aplikace), kdy byla daná operace zahájena. Logovací soubor obsahuje v případě prvního demonstračního příkladu pouze jeden řádek:

1.654: [Full GC 349K->166K(5056K), 0.0267048 secs]

U druhého demonstračního příkladu je do logovacího souboru vypsáno dvacet řádků:

1.077: [Full GC 349K->166K(5056K), 0.0232898 secs]
1.102: [Full GC 202K->166K(5056K), 0.0172860 secs]
1.121: [Full GC 202K->167K(5056K), 0.0172659 secs]
1.139: [Full GC 203K->165K(5056K), 0.0175709 secs]
1.158: [Full GC 201K->165K(5056K), 0.0171773 secs]
1.176: [Full GC 202K->166K(5056K), 0.0171315 secs]
1.194: [Full GC 202K->166K(5056K), 0.0171946 secs]
1.213: [Full GC 202K->165K(5056K), 0.0172673 secs]
1.231: [Full GC 201K->165K(5056K), 0.0171748 secs]
1.249: [Full GC 202K->166K(5056K), 0.0172159 secs]
1.268: [Full GC 202K->166K(5056K), 0.0171620 secs]
1.286: [Full GC 202K->165K(5056K), 0.0172600 secs]
1.305: [Full GC 201K->165K(5056K), 0.0171807 secs]
1.323: [Full GC 201K->165K(5056K), 0.0171617 secs]
1.341: [Full GC 201K->165K(5056K), 0.0172134 secs]
1.360: [Full GC 202K->165K(5056K), 0.0172419 secs]
1.378: [Full GC 201K->165K(5056K), 0.0171321 secs]
1.396: [Full GC 201K->165K(5056K), 0.0171343 secs]
1.415: [Full GC 201K->165K(5056K), 0.0175201 secs]
1.434: [Full GC 202K->165K(5056K), 0.0175939 secs]

6. Alokace paměti pro objekty v JVM

Při psaní aplikací v Javě se nevyhneme častému vytváření objektů. Může se přitom jednat jak o běžné objekty (obsahující většinou jako své atributy reference na další objekty) nebo o pole a řetězce, což jsou taktéž objekty, které se od „běžných“ objektů odlišují pouze tím, že pro ně existuje podpora v syntaxi jazyka Java. Programovací jazyk Java se navíc od některých dalších programovacích jazyků odlišuje v tom, že objekty jsou vždy vytvářeny na haldě a nikoli na zásobníku (tam jsou ukládány pouze jejich reference). To například znamená, že překladač při vytváření nějakého objektu vůbec nemusí zkoumat, zda je objekt používán pouze v rámci některého bloku (metody, podmínky, smyčky) nebo zda je reference na něj předána jako návratový kód metody popř. je uložena například do atributu objektu. Tato vlastnost – spolu s tím, že se všechny objekty vytváří dynamicky – zabraňuje vznikům chyb způsobených například tím, že se z metody/funkce vrátí ukazatel na objekt vytvořený na zásobníku (který může být kdykoli později přepsán).

Můžeme se ostatně podívat na to, jakým způsobem se odlišuje bajtkód následujících čtyř metod, které vytváří instanci třídy Integer a následně s ní pracují různým způsobem:

public class Test
{
    Integer attribute;
 
    // reference na vytvořený objekt se vrací
    // jako návratová hodnota metody
    Integer method1()
    {
        Integer i = new Integer(6502);
        return i;
    }
 
    // objekt je pouze použit v rámci metody
    void method2()
    {
        Integer i = new Integer(6502);
        System.out.println(i);
    }
 
    // uložení reference na vytvořený objekt
    // do atributu objektu (= instance třídy Test)
    void method3()
    {
        Integer i = new Integer(6502);
        this.attribute = i;
    }
 
    // vrácení instance třídy Integer
    // bez vytváření lokální reference
    Integer method4()
    {
        return new Integer(6502);
    }
}

Po překladu zdrojového kódu třídy Test s volbou -g (konkrétně javac -g Test.java) si můžeme příkazem javap -c Test nechat vypsat vygenerovaný bajtkód. Shodné řádky všech čtyř metod jsou označeny hvězdičkou, první tři metody mají navíc společnou instrukci, která ukládá referenci na nový objekt do lokální proměnné. Tato instrukce je označena vykřičníkem:

java.lang.Integer method1();
  Code:
*   0:  new     #2; //class java/lang/Integer
*   3:  dup
*   4:  sipush  6502
*   7:  invokespecial   #3; //Method java/lang/Integer."<init>":(I)V
!  10:  astore_1
   11:  aload_1
   12:  areturn
 
void method2();
  Code:
*   0:  new     #2; //class java/lang/Integer
*   3:  dup
*   4:  sipush  6502
*   7:  invokespecial   #3; //Method java/lang/Integer."<init>":(I)V
!  10:  astore_1
   11:  getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
   14:  aload_1
   15:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
   18:  return
 
void method3();
  Code:
*   0:  new     #2; //class java/lang/Integer
*   3:  dup
*   4:  sipush  6502
*   7:  invokespecial   #3; //Method java/lang/Integer."<init>":(I)V
!  10:  astore_1
   11:  aload_0
   12:  aload_1
   13:  putfield        #6; //Field attribute:Ljava/lang/Integer;
   16:  return
 
java.lang.Integer method4();
  Code:
*   0:  new     #2; //class java/lang/Integer
*   3:  dup
*   4:  sipush  6502
*   7:  invokespecial   #3; //Method java/lang/Integer."<init>":(I)V
   10:  areturn

7. Jak nákladné je vytváření a rušení objektů?

Vraťme se nyní k problematice vytváření a rušení objektů. Tyto dvě operace, z nichž jedna je většinou prováděna explicitně v aplikaci (většinou, ale zdaleka ne vždy příkazem new) a druhá skrytě správcem paměti, samozřejmě nějaký čas trvají, tj. je zapotřebí na ně vyhradit určitý a nikoli zanedbatelný výpočetní výkon mikroprocesoru. I když je například vytvoření, tj. konstrukce objektu zdánlivě jednoduchá (typicky z toho důvodu, že je použit bezparametrický konstruktor, v němž se neprovádí žádné složité inicializace), jsou s ní spojeny další skryté operce, například alokace bloku/bloků na haldě, požadavek na přidělení dalšího bloku paměti poslaný operačnímu systému, v tom horším případě dokonce odložení části virtuální paměti na disk (swapování) atd.

Odstraňování již nepotřebného objektu z haldy však může být ještě složitější, protože se v rámci správy paměti v určitých intervalech (a většinou po poměrně malých částech) provádí defragmentace této paměti. Jaký vliv má relativně malá změna v programovém kódu (která eliminuje vytváření dočasných objektů) na celkový běh aplikace, a jak lze náročnost aplikace na výpočetní výkon mikroprocesoru změřit, si ukážeme v navazujících textech.

8. „Skryté“ vytváření objektů

Před popisem různých způsobů monitorování aplikací běžících v JVM si ukažme trojici demonstračních příkladů, které provádí stejnou činnost – do řetězce uloží textové reprezentace čísel od nuly do zadané mezní hodnoty (zde 10000), která jsou od sebe oddělená mezerou. Jedná se sice o umělý příklad, ovšem s podobným problémem se programátoři mohou setkat poměrně často – může se jednat o načítání obsahu nějakého logovacího souboru do řetězce, vytváření podoby HTML tabulky v řetězci před jejím posláním ke klientovi, programová serializace objektů do formátu XML, formátování nějaké tiskové sestavy na základě údajů načítaných z databáze atd. (většinou se namísto přidávání mezery do řetězce přidává znak pro konec řádku, ale to je již detail).

První demonstrační příklad používá ten nejjednodušší a – jak uvidíme dále – i z časového hlediska nejhorší způsob: jednotlivá čísla jsou převáděna na řetězec, poté je tento řetězec připojen k řetězci představujícím mezeru (právě díky této operaci dojde ke konverzi čísla na řetězec) a následně se výsledek připojí k již vytvořenému textu. Problém skrytý v tomto přístupu spočívá v tom, že řetězce jsou neměnitelné objekty, což znamená, že operace str1 += str2 vytvoří zcela nový objekt (typu String) a předchozí dva objekty (tj. původní str1 a str2) musí být dříve či později zrušeny správcem paměti. Tato náročná operace je v mezním případě (dlouhé řetězce) provedena celkem LOOP_COUNT-krát, navíc je zde ještě problém související s výrazem i + " " popsaným dále:

public class ConcatTest1
{
    private static final int LOOP_COUNT = 10000;
 
    private 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.out.println("String length: " + str.length());
        System.out.println("*** Running full gc ***");
        System.gc();
        System.runFinalization();
        System.out.println("*** Quitting ***");
    }
}

Obrázek 1: Sledování běhu příkladu ConcatTest1 v nástroji jconsole. Povšimněte si vytížení procesoru a taktéž obsazení haldy (heapu).

Ve druhém demonstračním příkladu je celá problematika řešena poněkud lépe – namísto (neměnných) objektů typu String je použit objekt typu StringBuffer nebo StringBuilder, k němuž je možné pomocí několika přetížených metod append() přidávat další znaky. I zde však není situace zcela dokonalá, i když je mnohem lepší, než v předchozím příkladu. I přesto, že se ve smyčce nenachází žádné klíčové slovo new, dochází zde poněkud skrytě k neustálému vytváření dočasných objektů, protože zápis i + " " se do bajtkódu přeloží jako sekvence instrukcí pro vytvoření dočasného objektu StringBuilder, do něhož se postupně uloží původní řetězec, textová podoba proměnné i a mezera. Následně je tento dočasný objekt převeden na řetězec, jenž je přiřazen lokální proměnné str (přesněji řečeno je do proměnné přiřazena reference na vytvořený řetězec) a poté se dočasný StringBuilder již stane neaktivním objektem. Jak uvidíme dále, není zde situace tak závažná, jako v prvním demonstračním příkladu, protože dočasný objekt StringBuilder obsahuje pouze krátký text, takže správce paměti není vyvoláván s tak velkou frekvencí:

public class ConcatTest2
{
    private static final int LOOP_COUNT = 10000;
 
    private static String createString()
    {
        StringBuffer str = new StringBuffer();
        for (int i = 0; i < LOOP_COUNT; i++)
        {
            str.append(i + " ");
        }
        return str.toString();
    }
 
    public static void main(String[] args)
    {
        String str = createString();
        System.out.println("String length: " + str.length());
        System.out.println("*** Running full gc ***");
        System.gc();
        System.runFinalization();
        System.out.println("*** Quitting ***");
    }
}

Obrázek 2: Sledování běhu příkladu ConcatTest2 v nástroji jconsole. Zde je již vytížení procesoru minimální a taktéž halda je obsazena z méně než 50%.

Třetí demonstrační příklad je přepsán takovým způsobem, že se ve smyčce žádný další pomocný dočasný objekt nevytváří, o čemž se můžeme snadno přesvědčit například pohledem do vygenerovaného bajtkódu (ovšem ke skrytému vytvoření nového objektu dojít ve skutečnosti může a skutečně i několikrát dojde – přijdete na to, kde a proč?). Jedná se sice o příklad (pravděpodobně) nejméně přehledný, ovšem v případě, že by se podobná konstrukce – postupné vytváření dlouhého řetězce z malých částí – měla vyskytovat v kritické části programu, je vhodné a mnohdy i nutné se přiklonit k tomuto řešení:

public class ConcatTest3
{
    private static final int LOOP_COUNT = 10000;
 
    private static String createString()
    {
        StringBuffer str = new StringBuffer();
        for (int i = 0; i < LOOP_COUNT; i++)
        {
            str.append(i);
            str.append(' ');
        }
        return str.toString();
    }
 
    public static void main(String[] args)
    {
        String str = createString();
        System.out.println("String length: " + str.length());
        System.out.println("*** Running full gc ***");
        System.gc();
        System.runFinalization();
        System.out.println("*** Quitting ***");
    }
}

Obrázek 3: Sledování běhu příkladu ConcatTest3 v nástroji jconsole. Výsledek je v tomto případě podobný jako u příkladu předchozího.

Jak vypadá výpis práce správce paměti v prvním příkladu, je ukázáno níže. Z tohoto výpisu je zřejmé, že při běhu programu došlo automaticky k vyvolání správce paměti cca 1100×(!) a program běžel na testovacím počítači celých 2,5 sekundy:

0.109: [GC 896K->118K(5056K), 0.0026554 secs]
0.115: [GC 1014K->123K(5056K), 0.0010161 secs]
0.119: [GC 1016K->120K(5056K), 0.0004892 secs]
...                    ...
... smazáno 1100 řádků ...
...                    ...
2.491: [GC 2208K->1540K(5056K), 0.0003350 secs]
2.493: [GC 2399K->1635K(5056K), 0.0002609 secs]
2.495: [GC 2495K->1826K(5056K), 0.0003830 secs]
2.497: [GC 2686K->1921K(5056K), 0.0002724 secs]
2.499: [GC 2782K->2112K(5056K), 0.0003674 secs]
2.501: [GC 2973K->2208K(5056K), 0.0002763 secs]
2.502: [GC 3069K->2398K(5056K), 0.0003746 secs]
2.504: [GC 3260K->2494K(5056K), 0.0002911 secs]
2.506: [Full GC 2687K->211K(5056K), 0.0163401 secs]

Druhý demonstrační příklad běžel znatelně rychleji (0,135s) a navíc se správce paměti automaticky zavolal pouze dvakrát (tato hodnota se může mírně odlišovat, nicméně by neměla přesáhnout zhruba pět zavolání GC):

root_podpora

0.122: [GC 896K->152K(5056K), 0.0025894 secs]
0.130: [GC 906K->188K(5056K), 0.0009370 secs]
0.135: [Full GC 825K->283K(5056K), 0.0199106 secs]

Ve třetím příkladu nedocházelo k vytváření žádných dočasných objektů ve smyčce, proto se správce paměti zavolal až po explicitním volání System.gc():

0.114: [Full GC 615K->211K(5056K), 0.0240047 secs]

Podrobnější analýzu chování všech tří příkladů si ukážeme příště.

9. Odkazy na Internetu

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