Hlavní navigace

Pohled pod kapotu JVM - řetězce na haldě (dokončení) a využití JVM TI pro ladění

22. 1. 2013
Doba čtení: 24 minut

Sdílet

V dnešním článku o jazyce Java i o vlastnostech JVM nejprve dokončíme téma, kterým jsme se začali zabývat v předchozím díle – ukážeme si, jak jsou využívány (krátké) řetězce ve vybrané reálné javovské aplikaci. Ve druhé polovině článku si řekneme, jak lze využít rozhraní JVM TI pro ladění (debugging).

Obsah

1. Průchod haldou a výpis všech řetězců uložených na haldě

2. Callback funkce jvmtiStringPrimitiveValueCallback()

3. Demonstrační agent číslo 25 – výpis řetězců uložených na haldě

4. Statistika využití řetězců v jednoduchém testovacím příkladu (Test25)

5. Statistika využití řetězců v reálné aplikaci (Freemind)

6. Využití rozhraní JVM TI pro ladění (debugging) javovských aplikací

7. Zjištění všech atributů vybrané třídy

8. Demonstrační agent číslo 26 – výpis informací o všech atributech vybrané třídy

9. Zdrojové kódy obou demonstračních agentů a k nim příslušných testovacích příkladů

10. Odkazy na Internetu

1. Průchod haldou a výpis všech řetězců uložených na haldě

V předchozí části seriálu o programovacím jazyce Java i o vlastnostech virtuálního stroje Javy jsme se seznámili s tím, jakým způsobem jsou uloženy řetězce na haldě (heapu) virtuálního stroje (JVM). Dnes popis této poměrně důležité problematiky dokončíme, protože si ukážeme demonstračního JVM TI agenta, který dokáže vypsat všechny řetězce uložené na haldě – z těchto informací posléze budeme moci získat alespoň rámcovou představu o tom, jak paměťově náročná je práce s řetězci v jednoduchém demonstračním příkladu (kapitola 4) i v reálné desktopové aplikaci Freemind (kapitola 5). Toto měření však samozřejmě lze provést i pro ostatní javovské aplikace, například i pro aplikační servery a na nich nasazené (deploy) aplikace. Připomeňme si však nejdříve, jakým způsobem jsou řetězce na haldě uloženy. Řetězec je představován dvojicí objektů – instancí třídy String a instancí pole char[], které je atributem třídy String.

Pole char[] mohou být ve skutečnosti sdílená mezi větším množstvím řetězců, protože každá instance třídy String obsahuje další dva důležité atributy – délku řetězce (ve znacích) a offset od začátku pole char[]. Díky tomu je například možné implementovat metodu String.substring() takovým způsobem, že se neprovádí časově i paměťově náročné vytváření nového pole se znaky, ale prostě se vrátí nová instance třídy String se stejným atributem char[], ovšem s rozdílnými hodnotami atributů count a offset a samozřejmě též posledního atributu hash (proč je možné String.substring() takto implementovat je zřejmé – řetězce jsou v Javě neměnné neboli immutable). Každá instance třídy String zabere na haldě většinou 24 bajtů (platí pro 32bitové JVM i 64bitové JVM s relativně malou velikostí haldy), jak je to ostatně patrné i při pohledu na následující tabulku:

# Velikost (B) Struktura Popis
1 8 HEADER hlavička objektu (přiřazena každému objektu na haldě)
2 4 int offset atribut offset instance třídy String
3 4 int count atribut count instance třídy String
4 4 int hash atribut hash instance třídy String
5 4 char[] reference na pole znaků

(Potenciálně) sdílené pole char[] má na haldě samozřejmě proměnnou délku v závislosti na tom, kolik znaků řetězec skutečně obsahuje:

# Velikost (B) Struktura Popis
1 8 HEADER hlavička objektu (přiřazena každému objektu na haldě)
2 4 int length délka pole = délka řetězce ve znacích
3 length*2 char[] vlastní obsah řetězce (char má dva bajty)
4 0 nebo 2 PADDING výplň, aby celková délka objektu byla dělitelná čtyřmi (někdy osmi)

2. Callback funkce jvmtiStringPrimitiveValueCallback()

Nyní tedy již víme, jak jsou řetězce na haldě uloženy. Teoreticky již dokonce umíme všechny řetězce na haldě přečíst, protože by postačovalo projít instancemi všech tříd s využitím callback funkce jvmtiHeapReferenceCallback(), vybrat z nich všechny instance třídy String (což není těžké) a potom s využitím callback funkce jvmtiArrayPrimitiveValueCallback() projít všemi poli znaků (char[]) a z nich nějakým způsobem vyfiltrovat pouze ta pole, která jsou atributy instancí tříd String. Ovšem vzhledem k tomu, že se s řetězci pracuje skutečně velmi často a navíc i proto, že řetězce mají (jako objekty) v Javě poněkud zvláštní postavení, existuje v rozhraní JVM TI pro průchod řetězci speciální typ callback funkce jvmtiStringPrimitiveValueCallback(). Ukazatel na tuto callback funkci lze uložit do struktury jvmtiHeapCallbacks, kterou si v tomto seriálu již ukážeme skutečně naposledy:

typedef struct {
    jvmtiHeapIterationCallback        heap_iteration_callback;
    jvmtiHeapReferenceCallback        heap_reference_callback;
    jvmtiPrimitiveFieldCallback       primitive_field_callback;
    jvmtiArrayPrimitiveValueCallback  array_primitive_value_callback;
    jvmtiStringPrimitiveValueCallback string_primitive_value_callback;
    jvmtiReservedCallback             reserved5;
    jvmtiReservedCallback             reserved6;
    jvmtiReservedCallback             reserved7;
    jvmtiReservedCallback             reserved8;
    jvmtiReservedCallback             reserved9;
    jvmtiReservedCallback             reserved10;
    jvmtiReservedCallback             reserved11;
    jvmtiReservedCallback             reserved12;
    jvmtiReservedCallback             reserved13;
    jvmtiReservedCallback             reserved14;
    jvmtiReservedCallback             reserved15;
} jvmtiHeapCallbacks;

Ukazatel zapsaný do položky datové struktury jvmtiHeapCallbacks.jvmtiS­tringPrimitiveValueCallback musí obsahovat adresu callback funkce, jejíž hlavička má následující tvar:

jint JNICALL jvmtiStringPrimitiveValueCallback(
     jlong        class_tag,
     jlong        size,
     jlong*       tag_ptr,
     const jchar* value,
     jint         value_length,
     void*        user_data);

Vidíme, že této callback funkci, která může být zavolána pro každý řetězec uložený na haldě, se při jejím zavolání předá šestice parametrů, z nichž některé již známe z dalších callback funkcí, ale některé parametry jsou zde zcela nové:

# Typ parametru Jméno parametru Význam
1 jlong class_tag tag přiřazený ke třídě, které je objekt instancí (bude to vždy třída String)
2 jlong size celková velikost alokovaná pro objekt na haldě (typicky 24 bajtů)
3 jlong* tag_ptr ukazatel na tag přiřazený k objektu
4 char* value obsah řetězce v Unicode (modifikovaný UTF-8)
5 jint value_length délka řetězce (odpovídá počtu 16bitových znaků)
6 void* user_data ukazatel na uživatelská data (má stejný význam, jaký již známe z minula)

V prvním parametru se předává tag přiřazený ke třídě, jejíž instancí se právě na haldě prochází. V této callback funkci by se vždy mělo jednat o tag přiřazený ke třídě String, což si ostatně můžeme otestovat na demonstračním JVM TI agentovi. Ve druhém parametru je předávána velikost instance této třídy, což je (možná poněkud překvapivě) konstantní hodnota – většinou 24 bajtů. Proč tomu tak je bylo vysvětleno v předchozí kapitole. Užitečnější je pátý parametr obsahující skutečnou délku řetězce a samozřejmé též čtvrtý parametr s vlastním obsahem řetězce. Z toho vyplývá, že JVM TI dokáže automaticky spárovat instanci třídy String s příslušnou instancí pole char[], takže tuto relativně složitou činnost nemusíme dělat ručně.

3. Demonstrační agent číslo 25 – výpis řetězců uložených na haldě

Informace získané v předchozí kapitole využijeme ve dvacátém pátém demonstračním JVM TI agentovi, v němž je implementována speciální callback funkce pojmenovaná callback_for_each_string(), která musí být zaregistrována přes heap_callbacks.string_pri­mitive_value_callback. Způsob registrace této callback funkce je ukázán na následujícím úryvku kódu, kde nejprve dojde k vlastní registraci callback funkce a následně se zahájí procházení haldou:

/*
 * Registrace callback funkce pro prochazeni haldou a zacatek prochazeni.
 */
static void register_heap_callback_and_run_dump(jvmtiEnv *jvmti_env)
{
    jvmtiError error;
    jvmtiHeapCallbacks heap_callbacks;
    t_heap_stat heap_stat = {0,0};
 
    /* naplnit strukturu heap_callbacks jednou callback funkci */
    (void)memset(&heap_callbacks, 0, sizeof(heap_callbacks));
    heap_callbacks.string_primitive_value_callback = &callback_for_each_string;
 
    /* zahajit prochazeni haldou */
    error = (*jvmti_env)->IterateThroughHeap(jvmti_env, 0, NULL,
            &heap_callbacks, (const void *)&heap_stat);
    check_jvmti_error(jvmti_env, error, "read heap content");
 
    /* vypis statistiky o obsazeni haldy */
    printf("String count: %d\n", heap_stat.arrays_count);
    printf("Total size:   %ld bytes\n", heap_stat.total_size);
}

Pro každý řetězec nalezený na haldě se vypíše délka tohoto řetězce (uvedená ve znacích), velikost objektu představujícího řetězec (jak jsme se již zmínili, bude zde většinou konstantní hodnota 24) a následně se řetězec vypíše, ale pro jistotu se budou vypisovat jen znaky s kódy 32 až 127, protože řetězec (resp. přesněji řečeno jednotlivé znaky v něm uložené) jsou reprezentovány v modifikovaném kódu UTF-8, s nímž se v čistém céčku pracuje dosti obtížně. V modifikovaném UTF-8 je znak uložen v jednom až třech bajtech, tj. délka je proměnná a oproti normálnímu kódování UTF-8 zde existuje omezení na maximálně tříbajtové sekvence. Navíc je zaručeno, že ASCII znaky 0×00 až 0×7f jsou reprezentovány jedním bajtem s ASCII kódem, zatímco všechny znaky s kódem 0×80 až 0×7ff jsou uloženy ve dvou bajtech a znaky s kódem 0×800 až 0×ffff ve třech bajtech – v tomto případě však mají všechny bajty nastaven nejvyšší bit na jedničku, tj. jejich hodnota je větší než 127.

Pokud tedy budeme v řetězci vyhledávat a následně tisknout pouze bajty s hodnotou menší než 128, je zaručeno, že se provede tisk pouze ASCII znaků a nikoli například „rozsypaného čaje“:

/*
 * Callback funkce volana pro kazdy retezec.
 */
static jint JNICALL callback_for_each_string(
        jlong  class_tag,
        jlong  size,
        jlong* tag_ptr,
        const  jchar* value,
        jint   value_length,
        void*  user_data)
{
    int i;
    printf("length: %4d chars,  size: %ld bytes    ",
        value_length, (long int)size);
 
    /* velmi zjednoduseny tisk *podmnoziny* Unicode znaku */
    putchar('"');
    for (i=0; i<value[i]; i++)
    {
        if (value[i] >= 32 && value[i] <= 127)
        {
            putchar(value[i]);
        }
    }
    putchar('"');
    putchar('\n');
 
    /* spocitani statistickych informaci o obsazeni haldy */
    t_heap_stat *heap_stat = (t_heap_stat*)user_data;
    heap_stat->arrays_count++;
    heap_stat->total_size += size;
 
    /* pokracovat dale v prochazeni haldy */
    return JVMTI_VISIT_OBJECTS;
}

4. Statistika využití řetězců v jednoduchém testovacím příkladu (Test25)

Celý zdrojový kód demonstračního JVM TI agenta číslo 25 je, podobně jako i předchozí demonstrační agenti, dostupný v Mercurial repositáři. Zajímavé bude zjistit, kolik řetězců je uloženo na haldě virtuálního stroje Javy v případě, že je v JVM spuštěn pouze velmi jednoduchý a krátký příklad, jehož zdrojový kód je vypsán pod tímto odstavcem:

/**
  * Testovaci trida pouzita pro test dvacateho
  * pateho demonstracniho JVM TI agenta.
  */
public class Test25 {
    /**
      * Spusteni testu.
      */
    public static void main(String[] args) {
        String s1 = "Hello world!";
        String s2 = "http://www.root.cz";
        String s3 = "abcdefghijklmnopqrstuvwxyz";
        String s4 = "Příliš žluťoučký kůň úpěl ďábelské ódy";
    }
}

Vidíme, že v této testovací třídě jsou použity pouze čtyři řetězce, ovšem ve skutečnosti bude na haldě běžící JVM uloženo řetězců mnohem více. Po překladu JVM TI agenta i testovací třídy Test25 se virtuální stroj Javy spustí následujícím příkazem:

gcc -Wall -ansi -I/usr/lib/jvm/java-1.6.0-openjdk/include/ -shared -o libagent25.so agent25.c
javac -g Test25.java

I když všichni očekáváme, že na haldě bude uloženo větší množství řetězců, může být výsledek i tak poměrně překvapivý:

Agent25: Agent_OnLoad
Agent25: JVM TI version is correct
Agent25: Got VM init event
Agent25: Got VM Death event
Agent25: Got data dump request
class java.io.BufferedOutputStream  has tag 1
class java.lang.Class$3  has tag 2
class java.util.Enumeration  has tag 3
...
...
...
class [Z has tag 371
class [B has tag 372
class [C has tag 373
class [I has tag 374
class [S has tag 375
class [J has tag 376
class [F has tag 377
class [D has tag 378
length:   15 chars,  size: 24 bytes    "Java heap space"
length:   13 chars,  size: 24 bytes    "PermGen space"
length:   37 chars,  size: 24 bytes    "Requested array size exceeds VM limit"
length:   26 chars,  size: 24 bytes    "GC overhead limit exceeded"
length:    9 chars,  size: 24 bytes    "/ by zero"
length:    4 chars,  size: 24 bytes    "main"
length:    4 chars,  size: 24 bytes    "main"
length:   90 chars,  size: 24 bytes    "Method sun.misc.Unsafe.prefetchRead(Ljava/lang/Object;J"
length:  108 chars,  size: 24 bytes    "Method sun.misc.Unsafe.copyMemory(Ljava/lang/Object;JLjava"
...
...
...
length:    5 chars,  size: 24 bytes    "java."
length:    4 chars,  size: 24 bytes    "main"
length:   12 chars,  size: 24 bytes    "Hello world!"
length:   18 chars,  size: 24 bytes    "http://www.root.cz"
length:   26 chars,  size: 24 bytes    "abcdefghijklmnopqrstuvwxyz"
length:   38 chars,  size: 24 bytes    "Pli luouk k pl belsk"
Strings count: 927
Total size:   22248 bytes
Agent25: Agent_OnUnload

Na haldě je tedy uloženo celkem 927 řetězců (toto číslo se může na vaší JVM o několik procent měnit) a velikost paměti obsazené pouze instancemi třídy String, nikoli tedy vlastními řetězci char[], dosahuje přibližné hodnoty dvacet jedna kilobajtů. V následující tabulce jsou vypsány další zajímavé statistické informace:

Počet řetězců 927
Minimální délka řetězce 0 znaků
Maximální délka řetězce 517 znaků
Průměrná délka řetězce 15,6 znaků
Medián 10 znaků
Průměrná efektivita uložení 44%

Průměrná délka řetězce ani medián nám ovšem nedají zcela přesnou informaci o tom, jak dlouhé řetězce ve skutečnosti na haldě najdeme. Proto si navíc ještě ukážeme histogram, na jehož horizontální osu je nanesena délka řetězce (ve znacích) a na osu vertikální pak počet řetězců majících tuto délku. Histogram je zobrazen pro dva různé rozsahy horizontální osy: první zobrazuje frekvenci výskytu řetězců s délkou 0–600 znaků a druhý pouze řetězce s délkou 0–100 znaků:

Obrázek 1: Histogram s frekvencí výskytu řetězců s délkou 0–600 znaků.

Obrázek 2: Histogram s frekvencí výskytu řetězců s délkou 0–100 znaků.

5. Statistika využití řetězců v reálné aplikaci (Freemind)

Pro malé javovské programy lze předpokládat, že se na haldě budou nacházet především objekty vytvořené a využívané přímo virtuálním strojem Javy, což bude platit i o řetězcích. O tom jsme se ostatně mohli přesvědčit v předchozím případě, kdy javovský program obsahoval pouze čtyři explicitně zadané řetězce, zatímco na haldě jich bylo uloženo celkem 927. Přitom se většinou jednalo o velmi krátké řetězce s průměrnou délkou 15,6 znaků a s mediánem dokonce pouhých 10 znaků. Zkusme tedy využít našeho JVM TI agenta pro získání statistiky o řetězcích pro reálnou aplikaci. Bude se jednat o poměrně známý desktopový program Freemind sloužící pro tvorbu takzvaných myšlenkových map. Jedná se o středně velkou aplikaci, takže získané informace by mohly být poměrně zajímavé.

Obrázek 3: Screenshot programu Freemind ve chvíli, kdy je virtuální stroj monitorován JVM TI agentem číslo 25.

Freemind se spouští pomocí skriptu, v němž se nastavují cesty k různým Java archivům. Tento skript bylo pro potřeby měření JVM TI agentem nutné upravit do následující podoby (která je ovšem závislá na konkrétních adresářích, kde jsou uloženy Java archivy):

CLASSPATH=\
/usr/share/java/SimplyHTML.jar:\
/usr/share/java/gnu-regexp.jar:\
/usr/share/java/jibx-run.jar:\
/usr/share/java/xpp3.jar:\
/usr/share/freemind/lib/bindings.jar:\
/usr/share/freemind/lib/freemind.jar:\
/usr/share/java/commons-lang.jar:\
/usr/share/java/forms.jar
 
java -agentpath:./libagent25.so -Xmx256M -Dfreemind.base.dir="/usr/share/freemind" -cp "${CLASSPATH}" freemind.main.FreeMindStarter

Po spuštění tohoto skriptu dojde k zobrazení grafického uživatelského rozhraní Freemindu a je možné s ním normálně pracovat. Až teprve při ukončování činnosti virtuálního stroje se zavolá callback funkce v JVM TI agentovi, která nastartuje procházení všemi řetězci na haldě:

Checking Java Version...
Agent25: Agent_OnLoad
Agent25: JVM TI version is correct
Agent25: Got VM init event
Agent25: Got VM Death event
Agent25: Got data dump request
class freemind.modes.common.GotoLinkNodeAction  has tag 1
class sun.awt.image.IntegerComponentRaster  has tag 2
class javax.swing.plaf.PanelUI  has tag 3
class sun.awt.MostRecentKeyValue  has tag 4
class javax.swing.plaf.basic.BasicHTML$BasicHTMLViewFactory  has tag 5
class java.util.TaskQueue  has tag 6
class java.util.Currency  has tag 7
class sun.awt.X11.XNETProtocol  has tag 8
class sun.awt.X11.XProtocolConstants  has tag 9
...
...
...
class [[S has tag 3405
class [[[S has tag 3406
class [J has tag 3407
class [[J has tag 3408
class [F has tag 3409
class [[F has tag 3410
class [D has tag 3411
class [[D has tag 3412
length:   31 chars,  size: 24 bytes    "End paint in 213. Mean time:247"
length:    0 chars,  size: 24 bytes    " "
length:    9 chars,  size: 24 bytes    "18.1.2013"
length:    8 chars,  size: 24 bytes    "23:38:41"
length:    0 chars,  size: 24 bytes    " "
length:    4 chars,  size: 24 bytes    "cs__"
length:   97 chars,  size: 24 bytes    "18.1.2013 23:38:41 freemind.view.mindmapview.MapView"
length:   38 chars,  size: 24 bytes    "Java_java_awt_AWTEvent_nativeSetSource"
length:   58 chars,  size: 24 bytes    "Java_sun_awt_X11_XWindow_haveCurrentX11InputMethodInstance"
length:   51 chars,  size: 24 bytes    "Java_sun_awt_X11_XWindow_x11inputMethodLookupString"
length:   20 chars,  size: 24 bytes    "The X.Org Foundation"
length:   40 chars,  size: 24 bytes    "Java_sun_awt_X11_XlibWrapper_IsKeypadKey"
length:   17 chars,  size: 24 bytes    "type = FocusOut, "
length:   31 chars,  size: 24 bytes    "type = FocusOut, serial = 695, "
...
...
...
length:   10 chars,  size: 24 bytes    "US/Central"
length:   10 chars,  size: 24 bytes    "US/Eastern"
length:    9 chars,  size: 24 bytes    "US/Hawaii"
length:   17 chars,  size: 24 bytes    "US/Indiana-Starke"
length:   15 chars,  size: 24 bytes    "US/East-Indiana"
length:   11 chars,  size: 24 bytes    "US/Michigan"
length:   11 chars,  size: 24 bytes    "US/Mountain"
length:   10 chars,  size: 24 bytes    "US/Pacific"
length:   14 chars,  size: 24 bytes    "US/Pacific-New"
length:    8 chars,  size: 24 bytes    "US/Samoa"
length:    3 chars,  size: 24 bytes    "VST"
length:    4 chars,  size: 24 bytes    "W-SU"
length:    4 chars,  size: 24 bytes    "Zulu"
length:   17 chars,  size: 24 bytes    "filter_conditions"
Strings count: 29737
Total size:   713688 bytes
Agent25: Agent_OnUnload

Pro tuto průměrnou desktopovou aplikaci se tedy na haldě vytvořilo necelých třicet tisíc(!) řetězců a pouze instance třídy String zabraly téměř 700 kilobajtů (do této velikosti nejsou počítána pole char[]). V následující tabulce jsou zobrazeny další zajímavé statistické informace:

Počet řetězců 29737
Minimální délka řetězce 0 znaků
Maximální délka řetězce 10387 znaků
Průměrná délka řetězce 23,7 znaků
Medián 16 znaků
Průměrná efektivita uložení 55 %

Průměrná délka řetězce i medián se oproti první testovací aplikaci zvýšily, což znamená, že se zlepšila i efektivita uložení řetězců (poměr mezi obsazením haldy znaky dělený celkovým obsazením instancemi tříd String a jejími atributy char[]). Podívejme se taktéž na histogramy. První histogram opět ukazuje frekvenci řetězců s délkami 0 až 600 znaků a druhý pouze frekvence řetězců s délkou 0 až 100 znaků.

Obrázek 4: Histogram s frekvencí výskytu řetězců s délkou 0–600 znaků.

Obrázek 5: Histogram s frekvencí výskytu řetězců s délkou 0–100 znaků.

Jen pro doplnění je ještě vhodné k oběma histogramům přidat minule vysvětlený graf, na jehož horizontální osu je vynesena délka řetězce a na osu vertikální (pravá osa) pak efektivita uložení takto dlouhého řetězce v paměti:

string_storage_graph

Obrázek 6: Efektivita uložení řetězců s různou délkou.

6. Využití rozhraní JVM TI pro ladění (debugging) javovských aplikací

Rozhraní JVM TI je možné využít i pro ladění javovských aplikací – jinými slovy je možné s využitím JVM TI naprogramovat i poměrně složitý debugger, i když je na tomto místě nutné říci, že výhodnější a především jednodušší bývá v tomto případě použití rozhraní JDWP, které je přímo určené právě pro implementaci debuggerů. Pokud se smíříme s tím, že JVM TI je nízkoúrovňové rozhraní, nabízí nám v oblasti ladění ve skutečnosti poměrně širokou funkcionalitu, například:

  1. Zjištění vzniku zachycované či nezachycované výjimky (to již umíme).
  2. Zjištění zachycení výjimky (taktéž již umíme).
  3. Získání seznamu všech načtených tříd (-//-).
  4. Přečtení všech objektů, polí i primitivních hodnot na haldě (-//-).
  5. Zjištění zápisu do atributu.
  6. Zjištění pouhého čtení hodnoty atributu.
  7. Zachycení vstupu do metody.
  8. Zachycení výstupu z metody.
  9. Nastavení breakpointu.
  10. Vynucení výskoku z funkce s vybranou návratovou hodnotou.
  11. Krokování po jednotlivých instrukcích.

7. Zjištění všech atributů vybrané třídy

V následujícím textu se budeme zabývat především body číslo 5 a 6, tj. způsobem detekce zápisu do atributu vybrané třídy/objektu a způsobem detekce čtení tohoto atributu. Tuto užitečnou funkci nám nabízí prakticky všechny moderní debuggery (příkaz watch…). S využitím funkcionality nabízené rozhraním JVM TI je možné k libovolnému atributu třídy či objektu zaregistrovat callback funkci zavolanou ve chvíli, kdy je hodnota tohoto atributu změněna (je proveden zápis) či kdy je naopak jen přečtena aktuální hodnota tohoto atributu. Nicméně při registraci těchto callback funkcí je zapotřebí znát identifikátor tohoto atributu reprezentovaný hodnotou typu jfieldID, kterou musíme nějakým způsobem získat. Nejprve si ukážeme, jak je možné získat informace o všech atributech vybrané javovské třídy, protože přesně tato činnost se provádí v debuggeru – tam je taktéž nutné vybrat třídu/objekt a její/jeho atribut, který se má sledovat.

Demonstrační agent číslo 26 získá a vypíše jména, signatury a modifikátory všech atributů třídy Test26 (a samozřejmě i všech případných instancí této třídy). Základem funkcionality tohoto agenta je funkce callback_on_info_request() zavolaná ve chvíli ukončování práce JVM. Ve skutečnosti je však možné tuto funkci zavolat naopak při inicializaci JVM, v této chvíli je to jedno. V callback funkci callback_on_info_request() se získá seznam všech načtených tříd a ve smyčce se tímto seznamem prochází. Ve chvíli, kdy jméno třídy odpovídá zadanému jménu „Test26“ je zavolána funkce print_attributes_info(), které se předá identifikátor příslušné třídy:

/*
 * Callback funkce zavolana pri cteni informaci o atributech vybrane tridy.
 */
static void JNICALL callback_on_info_request(jvmtiEnv *jvmti_env)
{
    jvmtiError error;
    jint       class_count;
    jclass    *class_array;
    int        i;
 
    MSG("Got data dump request");
 
    error = (*jvmti_env)->GetLoadedClasses(jvmti_env, &class_count, &class_array);
    check_jvmti_error(jvmti_env, error, "get loaded classes");
 
    for (i=0; i < class_count; i++)
    {
        char *class_name_ptr;
        char *updated_class_name_ptr;
 
        /* ziskat jmeno tridy */
        error = (*jvmti_env)->GetClassSignature(jvmti_env, class_array[i], &class_name_ptr, NULL);
        check_jvmti_error(jvmti_env, error, "get class signature");
        if (class_name_ptr == NULL)
        {
            puts("Error: class has no signature");
        }
        /* upravit jmeno tridy */
        updated_class_name_ptr = update_class_name(class_name_ptr, ';');
 
        /* nasli jsme nasi testovaci tridu? */
        if (strcmp(TEST_CLASS_NAME, updated_class_name_ptr) == 0)
        {
            printf("Field\t%-20s\t%-25s\tModifiers\n", "Name", "Signature");
            print_attributes_info(jvmti_env, class_array[i]);
        }
 
        /* dealokace pameti po GetClassSignature() */
        error = (*jvmti_env)->Deallocate(jvmti_env, (unsigned char *)class_name_ptr);
        check_jvmti_error(jvmti_env, error, "deallocate class name");
    }
}

Ve funkci print_attributes_info() se s využitím JVM TI funkce GetClassFields() získá pole obsahující identifikátory všech atributů, které jsou v dané třídě deklarovány. Toto pole obsahuje prvky typu jfieldID a JVM TI funkce GetClassFields() kromě tohoto pole vrátí i jeho délku, čehož se využije v programové smyčce, která všemi hodnotami jfieldID prochází a pro každou získanou hodnotu zavolá funkci print_info_for_attribute():

/*
 * Zjisteni a nasledny tisk informaci o atributech tridy.
 */
static void JNICALL print_attributes_info(jvmtiEnv *jvmti_env, jclass class)
{
    jvmtiError error;
    int        field_count;
    jfieldID  *fields_array;
 
    /* precist vsechny atributy tridy */
    error = (*jvmti_env)->GetClassFields(jvmti_env, class, &field_count, &fields_array);
    check_jvmti_error(jvmti_env, error, "get class fields");
 
    /* pole atributu bylo inicializovano */
    if (error == JVMTI_ERROR_NONE)
    {
        int i;
        /* projit vsemi atributy a vypsat o nich informace */
        for (i = 0; i < field_count; i++)
        {
            print_info_for_attribute(jvmti_env, class, fields_array[i], i);
        }
    }
 
    /* dealokace pole ziskaneho pres GetClassFields() */
    error = (*jvmti_env)->Deallocate(jvmti_env, (unsigned char*)fields_array);
    check_jvmti_error(jvmti_env, error, "deallocate class fields array");
}

Do funkce print_info_for_attribute() jsou předány dvě důležité informace – hodnoty jclass a jfieldID, které jednoznačně určují příslušný atribut třídy. Pro získání jména a signatury atributu se použije JVM TI funkce nazvaná GetFieldName() a pro přečtení modifikátorů (viz další text) pak funkce nazvaná GetFieldModifiers(). Použití obou zmíněných JVM TI funkcí je jednoduché, pouze nesmíme zapomenout na dealokaci řetězců vrácených z GetFieldName():

/*
 * Tisk informaci o vybranem atributu tridy.
 */
static void print_info_for_attribute(jvmtiEnv *jvmti_env, jclass class, jfieldID field, int i)
{
    jvmtiError error;
    char *name;
    char *signature;
    int  modifiers;
 
    error = (*jvmti_env)->GetFieldName(jvmti_env, class, field, &name, &signature, NULL);
    check_jvmti_error(jvmti_env, error, "get field name");
    error = (*jvmti_env)->GetFieldModifiers(jvmti_env, class, field, &modifiers);
    check_jvmti_error(jvmti_env, error, "get field modifiers");
 
    /* zakladni informace */
    printf("%d\t%-20s\t%-25s\t", i, name, signature);
    /* tisk pripadnych modifikatoru */
    print_modifiers(modifiers);
    putchar('\n');
 
    /* dealokace pameti */
    error = (*jvmti_env)->Deallocate(jvmti_env, (unsigned char*)name);
    check_jvmti_error(jvmti_env, error, "deallocate field name");
    error = (*jvmti_env)->Deallocate(jvmti_env, (unsigned char*)signature);
    check_jvmti_error(jvmti_env, error, "deallocate field signature");
}

Nyní nám již zbývá pouze vytisknout všechny případné modifikátory atributu. Tyto modifikátory jsou uloženy v jediné celočíselné hodnotě jako bitové příznaky. My již vlastně hodnoty těchto příznaků známe, protože jsou stejné, jako příznaky používané v bajtkódu JVM (části 18 až 31 tohoto seriálu):

/*
 * Priznaky tridy, rozhrani ci datove polozky
 */
enum
{
    ACC_PUBLIC       = 0x0001,   /* verejna trida/rozhrani/datova polozka */
    ACC_PRIVATE      = 0x0002,   /* privatni datova polozka dostupna pouze uvnitr tridy */
    ACC_PROTECTED    = 0x0004,   /* chranena datova polozka dostupna uvnitr tridy a z pripadne odvozene tridy */
    ACC_STATIC       = 0x0008,   /* staticka datova polozka */
    ACC_FINAL        = 0x0010,   /* finalni trida ci datova polozka */
    ACC_VOLATILE     = 0x0040,   /* priznak volatile - hodnota se vzdy precte ci zapise do pameti */
    ACC_TRANSIENT    = 0x0080,   /* priznak transient - nebude se zpracovavat pri (de)serializaci */
    ACC_INTERFACE    = 0x0200,   /* rozliseni trida/rozhrani */
    ACC_ABSTRACT     = 0x0400,   /* abstraktni trida */
    ACC_SYNTHETIC    = 0x1000,   /* synteticka trida ci datova polozka, nema svuj protejsek ve zdrojovem kodu */
    ACC_ANNOTATION   = 0x2000,   /* anotace */
    ACC_ENUM         = 0x4000,   /* vycet */
};

Se znalostí číselného kódu vráceného JVM TI funkcí GetFieldModifiers() a výše vypsaného výčtu s bitovými maskami je možné vypsat všechny modifikátory příslušného atributu či jejich různé kombinace například následujícím způsobem:

/*
 * Tisk informaci o modifikatorech atributu.
 */
static void print_modifiers(int modifiers)
{
    if (modifiers & ACC_PUBLIC)     fputs(" public", stdout);
    if (modifiers & ACC_PRIVATE)    fputs(" private", stdout);
    if (modifiers & ACC_PROTECTED)  fputs(" protected", stdout);
    if (modifiers & ACC_STATIC)     fputs(" static", stdout);
    if (modifiers & ACC_FINAL)      fputs(" final", stdout);
    if (modifiers & ACC_ABSTRACT)   fputs(" abstract", stdout);
    if (modifiers & ACC_SYNTHETIC)  fputs(" synthetic", stdout);
    if (modifiers & ACC_TRANSIENT)  fputs(" transient", stdout);
    if (modifiers & ACC_INTERFACE)  fputs(" interface", stdout);
    if (modifiers & ACC_ANNOTATION) fputs(" annotation", stdout);
    if (modifiers & ACC_ENUM)       fputs(" enum", stdout);
    if (modifiers & ACC_VOLATILE)   fputs(" volatile", stdout);
}

Poznámka: funkce fputs() je zde použita pouze z toho důvodu, že nevkládá do standardního výstupu znak pro konec řádku, jiný důvod pro provádění této „magie“ neexistuje :-)

8. Demonstrační agent číslo 26 – výpis informací o všech atributech vybrané třídy

Pro vyzkoušení činnosti dvacátého šestého demonstračního JVM TI agenta, jehož zdrojový kód je opět uložen v Mercurial repositáři (viz též kapitola číslo 9) bude tento agent spuštěn s testovací třídou Test26, v níž je deklarováno poměrně velké množství různých atributů. Jedná se o atributy primitivního datového typu, pole primitivních datových typů, reference na objekty i pole referencí na objekty. Navíc jsou u některých atributů použity různé modifikátory, ať již se jedná o modifikátory přístupu (public/protected/private), tak i modifikátory static (třídní atribut), transient (atribut nebude serializován), volatile (nebude se provádět žádná optimalizace při čtení a zápisu hodnoty atributu) a final (jedná se o konstantní hodnotu). Tyto atributy lze samozřejmě různě kombinovat, což je taktéž v testovací třídě použito:

/**
  * Testovaci trida pouzita pro test dvacateho
  * sesteho demonstracniho JVM TI agenta.
  */
public class Test26 {
    /* Zakladni datove typy a obalove tridy */
    int         i;
    Integer     I;
    float       f;
    Float       F;
 
    /* Pole, tridy a rozhrani */
    int[]       pole;
    boolean[][] matice;
    String      retezec;
    String[]    moc_retezcu;
    Test26      instance;
    Comparable  comparable;
 
    /* Ruzne modifikatory */
    public int    public_int;
    private int   private_int;
    protected int protected_int;
    int           proste_jen_int;
    transient int transient_int;
    volatile int  volatile_int;
    final int     final_int = 0;
    static int    static_int;
 
    /* Kombinace modifikatoru */
    public static final int public_static_final_int = 42;
    private transient java.awt.Color color;
    protected volatile static java.util.Date date;
 
    /**
      * Spusteni testu.
      */
    public static void main(String[] args) {
    }
}

Po spuštění virtuálního stroje Javy společně s JVM TI agentem:

java -agentpath:./libagent26.so Test26 2> /dev/null

Získáme následující výpis, který přesně odpovídá deklaraci atributů v testovací třídě Test26:

CS24_early

Agent26: Agent_OnLoad
Agent26: JVM TI version is correct
Agent26: Got VM init event
Agent26: Got VM Death event
Agent26: Got data dump request
Field   Name                    Signature                       Modifiers
0       i                       I
1       I                       Ljava/lang/Integer;
2       f                       F
3       F                       Ljava/lang/Float;
4       pole                    [I
5       matice                  [[Z
6       retezec                 Ljava/lang/String;
7       moc_retezcu             [Ljava/lang/String;
8       instance                LTest26;
9       comparable              Ljava/lang/Comparable;
10      public_int              I                                public
11      private_int             I                                private
12      protected_int           I                                protected
13      proste_jen_int          I
14      transient_int           I                                transient
15      volatile_int            I                                volatile
16      final_int               I                                final
17      static_int              I                                static
18      public_static_final_int I                                public static final
19      color                   Ljava/awt/Color;                 private transient
20      date                    Ljava/util/Date;                 protected static volatile
Agent26: Agent_OnUnload

V následující části tohoto seriálu si ukážeme, jak lze takto získané informace dále využít při tvorbě jednoduchého „debuggeru“.

9. Zdrojové kódy obou demonstračních agentů a k nim příslušných testovacích příkladů

Podobně jako v pěti předcházejících částech tohoto seriálu byly i dnešní dva demonstrační JVM TI agenti kvůli snazšímu udržování všech zdrojových kódů uloženi do Mercurial repositáře, který je dostupný na adrese http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/. Prozatím nejnovější verze dnes popisovaných JVM TI agentů i dalších potřebných skriptů a testovacích javovských tříd jsou dostupné na následujících adresách:

Demonstrační příklad/podpůrný soubor Umístění
Agent #25 http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/f258f0f485b5/jvmti-agents/agent25/agent25.c
Skript pro překlad http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/f258f0f485b5/jvmti-agents/agent25/compile.sh
Skript pro spuštění http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/f258f0f485b5/jvmti-agents/agent25/test.sh
Skript pro spuštění Freemindu http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/f258f0f485b5/jvmti-agents/agent25/freemind.sh
Testovací třída Test25.java http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/f258f0f485b5/jvmti-agents/agent25/Test25.java
   
Výsledek pro třídu Test26 http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/f258f0f485b5/jvmti-agents/agent25/Test25.out
Výsledek pro aplikaci Freemind http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/f258f0f485b5/jvmti-agents/agent25/freemind.out
   
Agent #26 http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/f258f0f485b5/jvmti-agents/agent26/agent26.c
Skript pro překlad http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/f258f0f485b5/jvmti-agents/agent26/compile.sh
Skript pro spuštění http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/f258f0f485b5/jvmti-agents/agent26/test.sh
Testovací třída Test26.java http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/f258f0f485b5/jvmti-agents/agent26/Test26.java

10. Odkazy na Internetu

  1. The JVM Tool Interface (JVM TI): How VM Agents Work
    http://www.oracle.com/technet­work/articles/javase/jvm-ti-141370.html
  2. JVM Tool Interface Version 1.2
    http://docs.oracle.com/ja­vase/7/docs/platform/jvmti/jvmti­.html
  3. Creating a Debugging and Profiling Agent with JVMTI
    http://www.oracle.com/technet­work/articles/javase/jvmti-136367.html
  4. JVM TI (Wikipedia)
    http://en.wikipedia.org/wiki/JVM_TI
  5. IBM JVMTI extensions
    http://publib.boulder.ibm­.com/infocenter/realtime/v2r0/in­dex.jsp?topic=%2Fcom.ibm.sof­trt.doc%2Fdiag%2Ftools%2Fjvmti_ex­tensions.html
  6. An empirical study of Java bytecode programs
    http://www.mendeley.com/research/an-empirical-study-of-java-bytecode-programs/
  7. Java quick guide: JVM Instruction Set (tabulka všech instrukcí JVM)
    http://www.mobilefish.com/tu­torials/java/java_quickgu­ide_jvm_instruction_set.html
  8. The JVM Instruction Set
    http://mpdeboer.home.xs4a­ll.nl/scriptie/node14.html
  9. Control Flow in the Java Virtual Machine
    http://www.artima.com/under­thehood/flowP.html
  10. The class File Format
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/Clas­sFile.doc.html
  11. javap – The Java Class File Disassembler
    http://docs.oracle.com/ja­vase/1.4.2/docs/tooldocs/win­dows/javap.html
  12. javap-java-1.6.0-openjdk(1) – Linux man page
    http://linux.die.net/man/1/javap-java-1.6.0-openjdk
  13. Using javap
    http://www.idevelopment.in­fo/data/Programming/java/mis­cellaneous_java/Using_javap­.html
  14. Examine class files with the javap command
    http://www.techrepublic.com/ar­ticle/examine-class-files-with-the-javap-command/5815354
  15. Bytecode Engineering
    http://book.chinaunix.net/spe­cial/ebook/Core_Java2_Volu­me2AF/0131118269/ch13lev1sec6­.html

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

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.