Hlavní navigace

Pohled pod kapotu JVM - jak efektivně jsou uložena pole a řetězce na haldě?

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

Sdílet

Dnes si řekneme, jak získat informace o polích (primitivních typů) uložených na haldě a následně se budeme zabývat uložením řetězců. Jedná se o poměrně důležité téma, jelikož způsob práce JVM s řetězci může mnohdy významným způsobem ovlivnit jak výkonnost javovských aplikací, tak i jejich paměťovou náročnost.

Obsah

1. Dokončení předchozí části seriálu – zjištění pořadí atributu v rámci třídy

2. Přečtení pořadí atributu ve třídě z unie jvmtiHeapReferenceInfo

3. Demonstrační agent číslo 23 – výpis pořadí všech atributů nalezených na haldě

4. Přečtení informací o polích primitivních datových typů uložených na haldě

5. Demonstrační agent číslo 24 – výpis informací o polích primitivních datových typů

6. Proč velikost paměti obsazené polem neodpovídá objemu paměti vyhrazené pro prvky pole?

7. Řetězce na haldě – aneb specifický typ objektů

8. Struktura objektu představujícího řetězec

9. Efektivita uložení řetězců na haldě

10. Výpočet efektivity uložení řetězců na haldě

11. Zdrojové kódy všech tří agentů a k nim příslušných testovacích příkladů

12. Odkazy na Internetu

1. Dokončení předchozí části seriálu – zjištění pořadí atributu v rámci třídy

V dnešní části seriálu o programovacím jazyce Java i o vlastnostech virtuálního stroje Javy nejprve dokončíme téma, kterému jsme se již začali věnovat minule. Jedná se o výpis typů a hodnot atributů uložených na haldě. Posléze si řekneme, jakým způsobem je možné získat informace o polích (primitivních typů) uložených na haldě a následně se budeme zabývat způsobem uložení řetězců, protože se jedná o poměrně důležité téma, jelikož způsob práce s řetězci může mnohdy velmi významným způsobem ovlivnit jak výkonnost javovských aplikací, tak i jejich paměťovou náročnost. Vraťme se nejdříve ke dvěma demonstračním JVM TI agentům popsaným minule. V těchto agentech se používala callback funkce nazvaná callback_for_primitive_values(), která se volala pro všechny atributy objektů uložených na haldě. Přitom se jednalo o atributy, jejichž typ patřil mezi primitivní typy Javy – boolean, byte, short, int, long, char, float či double. Hlavička zmíněné callback funkce vypadá následovně:

/*
 * Callback funkce volana pro kazdou hodnotu primitivniho datoveho typu
 * ulozenou na halde.
 */
static jint JNICALL callback_for_primitive_values(
        jvmtiHeapReferenceKind        kind,
        const jvmtiHeapReferenceInfo* info,
        jlong                         object_class_tag,
        jlong *                       object_tag_ptr,
        jvalue                        value,
        jvmtiPrimitiveType            value_type,
        void *                        user_data)

Již víme, jak je možné z údajů předaných této callback funkci získat jméno příslušné třídy (jejíž instance jsou zkoumány), typ atributu i hodnotu, která je v tomto atributu uložena. Prozatím však ještě nevíme, jakým způsobem je možné zjistit pořadí atributu v rámci třídy (či její instance). To je další a v podstatě již poslední informace, kterou můžeme relativně snadno získat, tentokrát s využitím prvního parametru kind a především pak parametru druhého info. Podrobnosti si řekneme v navazující kapitole.

2. Přečtení pořadí atributu ve třídě z unie jvmtiHeapReferenceInfo

Při volání callback funkce callback_for_primitive_values() je v prvním parametru nazvaném kind předána informace o tom, jaký prvek uložený na haldě je právě prozkoumáván. Datový typ parametru kind je jvmtiHeapReferenceKind a pohledem do hlavičkového souboru jvmti.h (či do dokumentace, ale zdrojový kód bývá často tou nejlepší dokumentací :-) lze velmi snadno zjistit, že se jedná o výčet s celkem sedmnácti deklarovanými hodnotami:

typedef enum {
    JVMTI_HEAP_REFERENCE_CLASS = 1,
    JVMTI_HEAP_REFERENCE_FIELD = 2,
    JVMTI_HEAP_REFERENCE_ARRAY_ELEMENT = 3,
    JVMTI_HEAP_REFERENCE_CLASS_LOADER = 4,
    JVMTI_HEAP_REFERENCE_SIGNERS = 5,
    JVMTI_HEAP_REFERENCE_PROTECTION_DOMAIN = 6,
    JVMTI_HEAP_REFERENCE_INTERFACE = 7,
    JVMTI_HEAP_REFERENCE_STATIC_FIELD = 8,
    JVMTI_HEAP_REFERENCE_CONSTANT_POOL = 9,
    JVMTI_HEAP_REFERENCE_SUPERCLASS = 10,
    JVMTI_HEAP_REFERENCE_JNI_GLOBAL = 21,
    JVMTI_HEAP_REFERENCE_SYSTEM_CLASS = 22,
    JVMTI_HEAP_REFERENCE_MONITOR = 23,
    JVMTI_HEAP_REFERENCE_STACK_LOCAL = 24,
    JVMTI_HEAP_REFERENCE_JNI_LOCAL = 25,
    JVMTI_HEAP_REFERENCE_THREAD = 26,
    JVMTI_HEAP_REFERENCE_OTHER = 27
} jvmtiHeapReferenceKind;

Nás však v tento okamžik zajímají pouze hodnoty primitivních datových typů uložených na haldě, pro něž se při volání callback funkce v parametru kind vždy předává hodnota JVMTI_HEAP_REFERENCE_FIELD, čemuž ostatně odpovídá i podmínka zapsaná uvnitř těla callback funkce:

    /* zajimaji nas pouze nestaticke atributy objektu */
    if (kind != JVMTI_HEAP_REFERENCE_FIELD) return JVMTI_VISIT_OBJECTS;
 

Důležitější, zajímavější a současně i poněkud komplikovanější je však druhý parametr naší callback funkce nazvaný info, jehož typ je jvmtiHeapReferenceInfo. Pro zjištění, o jaký datový typ se ve skutečnosti jedná, budeme muset opět prozkoumat hlavičkový soubor jvmti.h, z něhož lze vyčíst, že jde o unii, která může v daný okamžik obsahovat jednu ze šesti (ve skutečnosti však jen pěti) datových struktur:

union jvmtiHeapReferenceInfo {
    jvmtiHeapReferenceInfoField field;
    jvmtiHeapReferenceInfoArray array;
    jvmtiHeapReferenceInfoConstantPool constant_pool;
    jvmtiHeapReferenceInfoStackLocal stack_local;
    jvmtiHeapReferenceInfoJniLocal jni_local;
    jvmtiHeapReferenceInfoReserved other;
};

Pro úplnost si uveďme deklarace všech šesti datových struktur ukládaných do unie, i když nás ve skutečnosti bude zajímat pouze struktura jediná – jvmtiHeapReferenceInfoField:

struct jvmtiHeapReferenceInfoField {
    jint index;  /* zde je uloženo pořadí atributu v rámci třídy/objektu */
};
struct jvmtiHeapReferenceInfoArray {
    jint index;
};
struct jvmtiHeapReferenceInfoConstantPool {
    jint index;
};
struct jvmtiHeapReferenceInfoStackLocal {
    jlong thread_tag;
    jlong thread_id;
    jint depth;
    jmethodID method;
    jlocation location;
    jint slot;
};
struct jvmtiHeapReferenceInfoJniLocal {
    jlong thread_tag;
    jlong thread_id;
    jint depth;
    jmethodID method;
};
struct jvmtiHeapReferenceInfoReserved {
    jlong reserved1;
    jlong reserved2;
    jlong reserved3;
    jlong reserved4;
    jlong reserved5;
    jlong reserved6;
    jlong reserved7;
    jlong reserved8;
};

3. Demonstrační agent číslo 23 – výpis pořadí všech atributů nalezených na haldě

Další postup při vylepšování demonstračního JVM TI agenta je již poměrně snadný; stačí jen vědět, že v případě, že je v prvním parametru callback funkce callback_for_primitive_values() předána hodnota JVMTI_HEAP_REFERENCE_FIELD, pak je v unii info vždy uložena datová struktura jvmtiHeapReferenceInfoField, v níž je umístěn jediný prvek – kýžený index atributu v rámci třídy či objektu (instance třídy). Odkaz na celý zdrojový kód nové verze demonstračního JVM TI agenta je uveden v jedenácté kapitole, zde si pouze ukážeme upravenou formu callback funkce volané pro všechny hodnoty primitivních datových typů uložených na haldě:

/*
 * Callback funkce volana pro kazdou hodnotu primitivniho datoveho typu
 * ulozenou na halde.
 */
static jint JNICALL callback_for_primitive_values(
        jvmtiHeapReferenceKind kind,
        const                  jvmtiHeapReferenceInfo* info,
        jlong                  object_class_tag,
        jlong *                object_tag_ptr,
        jvalue                 value,
        jvmtiPrimitiveType     value_type,
        void *                 user_data)
{
    int index;
    int size;
    int field_no;
    char *primitive_type_str;
    char *class_name;
 
    /* struktura obsahujici podrobnejsi informace o atributu */
    jvmtiHeapReferenceInfoField *reference_info_field;
 
    /* zajimaji nas pouze nestaticke atributy objektu */
    if (kind != JVMTI_HEAP_REFERENCE_FIELD) return JVMTI_VISIT_OBJECTS;
 
    /* na zaklade predaneho typu ziskat jmeno typu jako retezec */
    primitive_type_str = get_primitive_type_str(value_type);
 
    /* na zaklade predaneho typu ziskat velikost v bajtech */
    size = get_primitive_type_size(value_type);
 
    /* ziskat poradi atributu v tride */
    /* nutno pretypovat, protoze jvmtiHeapReferenceInfo je ve skutecnosti unie */
    reference_info_field = (jvmtiHeapReferenceInfoField*)info;
    field_no = reference_info_field->index;
 
    /* jmeno tridy lze ziskat na zaklade tagu z globalniho pole */
    index = object_class_tag - 1;
    class_name = class_names[index];
 
    /* vypiseme informace pouze pro dve testovaci tridy "Foo" a "Bar" */
    if (strcmp("Foo ", class_name)==0 || strcmp("Bar ", class_name)==0)
    {
        printf("Field #%2d of type %-7s in instance of class %shas size %1d byte%c and value ",
                field_no, primitive_type_str, class_name, size, size > 1 ? 's' : ' ');
        print_value(value_type, value);
    }
 
    /* spocitani statistickych informaci o obsazeni haldy */
    t_heap_stat *heap_stat = (t_heap_stat*)user_data;
    heap_stat->primitive_count++;
    heap_stat->total_size += size;
 
    /* pokracovat dal v prochazeni haldy */
    return JVMTI_VISIT_OBJECTS;
}

Po překladu a spuštění tohoto demonstračního příkladu skripty uvedenými opět v jedenácté kapitole získáme následující výstup:

Agent23: Agent_OnLoad
Agent23: JVM TI version is correct
Agent23: Got VM init event
Agent23: Got VM Death event
Agent23: 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 sun.misc.Version  has tag 4
class java.util.Collections$EmptyList  has tag 5
class sun.misc.ExtensionDependency  has tag 6
class java.nio.HeapByteBuffer  has tag 7
...
...
...
class [Z has tag 373
class [B has tag 374
class [C has tag 375
class [I has tag 376
class [S has tag 377
class [J has tag 378
class [F has tag 379
class [D has tag 380
Field # 2 of type float   in instance of class Foo has size 4 bytes and value 3.141500
Field # 1 of type int     in instance of class Foo has size 4 bytes and value 42
Field # 0 of type boolean in instance of class Foo has size 1 byte  and value false
Field # 2 of type float   in instance of class Foo has size 4 bytes and value 2.717200
Field # 1 of type int     in instance of class Foo has size 4 bytes and value 6502
Field # 0 of type boolean in instance of class Foo has size 1 byte  and value true
Field # 3 of type long    in instance of class Bar has size 8 bytes and value 4
Field # 2 of type int     in instance of class Bar has size 4 bytes and value 3
Field # 1 of type short   in instance of class Bar has size 2 bytes and value 2
Field # 0 of type byte    in instance of class Bar has size 1 byte  and value 1
Field # 3 of type long    in instance of class Bar has size 8 bytes and value 4
Field # 2 of type int     in instance of class Bar has size 4 bytes and value 3
Field # 1 of type short   in instance of class Bar has size 2 bytes and value 2
Field # 0 of type byte    in instance of class Bar has size 1 byte  and value 1
Primitive values count: 5522 (fields only)
Total size:   21422 bytes
Agent23: Agent_OnUnload

Nyní si můžete porovnat zdrojový kód testovacích tříd Foo a Bar s informacemi vypsanými výše…

4. Přečtení informací o polích primitivních datových typů uložených na haldě

Nyní již víme, jakým způsobem je možné vypsat všechny objekty uložené na haldě i všechny „jednoduché“ atributy objektů – ať již se jedná o atributy objektového typu (=reference) či o atributy primitivního datového typu. Ovšem ještě nám zbývá si říci, jak lze získat informace o polích uložených na haldě, a to včetně polí znaků, protože právě s využitím polí znaků (pevné délky) jsou implementovány řetězce, které hrají v mnoha typech javovských aplikací téměř klíčovou roli. Nejdříve si popíšeme způsob čtení informací o polích, jejichž prvky jsou primitivního datového typu, tj. polí vytvořených například takto:

byte[]    b = new byte[length];
short[]   s = new short[length];
int[]     i = new int[length];
long[]    l = new long[length];
char[]    c = new char[length];
float[]   f = new float[length];
double[]  d = new double[length];
boolean[] z = new boolean[length];

Poznámka: vícerozměrná pole se v Javě zpracovávají jako pole polí, podobně jako je tomu například v céčku a mnoha dalších programovacích jazycích.

Při průchodu poli uloženými na haldě se používá speciální forma callback funkce, která je ve struktuře jvmtiHeapCallbacks představována ukazatelem array_primitive_value_callback:

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;

Registrace této callback funkce se provádí naprosto stejným způsobem, jako registrace callback funkcí volaných pro objekty i pro atributy primitivních datových typů; bude se však lišit počet i typ parametrů předávaných této callback funkci (tu si ukážeme v navazující kapitole). V kódu níže je ukázán způsob registrace této callback funkce s následným zahájením průchodu haldou:

/*
 * Registrace callback funkce pro prochazeni heapem 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, 0};
 
    /* naplnit strukturu heap_callbacks jednou callback funkci */
    (void)memset(&heap_callbacks, 0, sizeof(heap_callbacks));
    heap_callbacks.array_primitive_value_callback = &callback_for_array_values;
 
    /* 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 */
    printf("Arrays count: %d\n", heap_stat.arrays_count);
    printf("Data size:    %ld bytes\n", heap_stat.data_size);
    printf("Total size:   %ld bytes\n", heap_stat.total_size);
}

5. Demonstrační agent číslo 24 – výpis informací o polích primitivních datových typů

Callback funkci zavolané při nalezení pole s prvky primitivního datového typu se předává celkem sedm parametrů, z nichž některé již známe z jiných typů callback funkcí, ovšem další parametry budou zcela nové:

# Typ parametru Jméno parametru Význam
1 jlong class_tag tag přiřazený ke třídě, které je objekt instancí
2 jlong size celková velikost alokovaná pro pole na haldě
3 jlong* tag_ptr ukazatel na tag přiřazený k objektu
4 jint element_count počet prvků pole
5 jvmtiPrimitiveType element_type typ hodnoty (byte, int, float…) prvků pole
6 void* element obsah pole (hodnoty prvků pole)
7 void* user_data ukazatel na uživatelská data (má stejný význam, jaký již známe z minula)

Při průchodu haldou nás bude zajímat především počet prvků pole, typ prvků pole a velikost paměti alokované pro pole (proč je tato paměť větší, než by odpovídalo počtu prvků pole, si řekneme v dalším textu):

/*
 * Callback funkce volana pro kazde pole primitivnich
 * datovych typu ulozene na halde.
 */
static jint JNICALL callback_for_array_values(
        jlong              class_tag,
        jlong              size,
        jlong*             tag_ptr,
        jint               element_count,
        jvmtiPrimitiveType element_type,
        const void*        elements,
        void*              user_data)
{
    char *primitive_type_str;
    char *class_name;
    int   index;
    long  data_size;
 
    /* na zaklade predaneho typu ziskat jmeno typu jako retezec */
    primitive_type_str = get_primitive_type_str(element_type);
 
    /* jmeno tridy lze ziskat na zaklade tagu z globalniho pole */
    index = class_tag - 1;
    class_name = class_names[index];
 
    /* ziskat objem zabrany jen daty, nikoli vyplni */
    data_size = element_count * get_primitive_type_size(element_type);
 
    /* "magicke" delky pouzite v testovaci tride */
    if (element_count == 1 || element_count == 1234 || element_count == 10000) {
        printf("%s  %s[%d] with size %ld(%ld) bytes\n",
            class_name, primitive_type_str, element_count, data_size, (long int)size);
    }
 
    /* 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;
    heap_stat->data_size += data_size;
 
    /* pokracovat dal v prochazeni haldy */
    return JVMTI_VISIT_OBJECTS;
}

Kompletní zdrojový kód demonstračního JVM TI agenta je opět uveden v kapitole číslo 11.

6. Proč velikost paměti obsazené polem neodpovídá objemu paměti vyhrazené pro prvky pole?

Po spuštění demonstračního agenta číslo 24 získáme následující (poněkud zkrácený) výpis:

Agent24: Agent_OnLoad
Agent24: JVM TI version is correct
Agent24: Got VM init event
Agent24: Got VM Death event
Agent24: 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 sun.misc.Version  has tag 4
class java.util.Collections$EmptyMap  has tag 5
class sun.misc.ExtensionDependency  has tag 6
class java.nio.HeapByteBuffer  has tag 7
class java.util.Arrays  has tag 8
class sun.misc.JavaIOAccess  has tag 9
class java.util.IdentityHashMap  has tag 10
class java.net.URLStreamHandlerFactory  has tag 11
class Foo  has tag 12
...
...
...
class [Z has tag 372
class [B has tag 373
class [C has tag 374
class [I has tag 375
class [S has tag 376
class [J has tag 377
class [F has tag 378
class [D has tag 379
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
[B  byte[1] with size 1(16) bytes
[C  char[1] with size 2(16) bytes
[B  byte[1] with size 1(16) bytes
[B  byte[1] with size 1(16) bytes
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
[B  byte[1] with size 1(16) bytes
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
[B  byte[1] with size 1(16) bytes
[S  short[1] with size 2(16) bytes
[I  int[1] with size 4(16) bytes
[J  long[1] with size 8(24) bytes
[C  char[1] with size 2(16) bytes
[F  float[1] with size 4(16) bytes
[D  double[1] with size 8(24) bytes
[Z  boolean[1] with size 1(16) bytes
[B  byte[1234] with size 1234(1248) bytes
[S  short[1234] with size 2468(2480) bytes
[I  int[1234] with size 4936(4952) bytes
[J  long[1234] with size 9872(9888) bytes
[C  char[1234] with size 2468(2480) bytes
[F  float[1234] with size 4936(4952) bytes
[Z  boolean[1234] with size 1234(1248) bytes
[D  double[1234] with size 9872(9888) bytes
[B  byte[10000] with size 10000(10016) bytes
[S  short[10000] with size 20000(20016) bytes
[I  int[10000] with size 40000(40016) bytes
[J  long[10000] with size 80000(80016) bytes
[C  char[10000] with size 20000(20016) bytes
[F  float[10000] with size 40000(40016) bytes
[D  double[10000] with size 80000(80016) bytes
[Z  boolean[10000] with size 10000(10016) bytes
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
[C  char[1] with size 2(16) bytes
Arrays count: 2494
Data size:    610459 bytes
Total size:   648752 bytes
Agent24: Agent_OnUnload

Zajímavé je, že paměť alokovaná pro pole je vždy větší, než délka zjištěná prostým vynásobením počtu prvků pole s počtem bajtů pro jeden prvek. V céčku totiž skutečně platí:

alokovaná_paměť = délka_pole * sizeof(typ)

V Javě je však situace poněkud komplikovanější, protože pole jsou chápána jako objekty a tudíž se při jejich ukládání na haldu kromě vlastních prvků ukládají i další informace. Především se jedná o hlavičku, jejíž délka je většinou rovna osmi bajtům. Hlavička je používána pro identifikaci objektu, využívá ji správce paměti atd. atd. U 64bitových virtuálních strojů a velkých kapacit haldy může být hlavička i větší, setkat se lze i s délkou 16 bajtů! Taktéž je nutné uložit délku pole, což je (prozatím) čtyřbajtová hodnota (délka pole je jeho nedílnou součástí, na rozdíl od céčka). Navíc se podle architektury, na níž JVM běží, na konci pole nachází výplň (padding) tak velká, aby celková velikost objektu byla dělitelná 8 či 16 bajty. To je i důvod, proč například jednoprvkové pole boolean[1] ve skutečnosti na haldě obsadí šestnáct bajtů.

7. Řetězce na haldě – aneb specifický typ objektů

Dnes se ještě budeme zabývat popisem způsobu uložení řetězců na haldě, získáním těchto řetězců a jejich (i když poněkud omezeným) výpisem. Připomeňme si, co se vlastně v Javě pod pojmem „řetězec“ (string) skrývá. Na jednu stranu se jedná o pouhou třídu, jejíž instance lze vytvářet s využitím operátoru new, na stranu druhou však pro práci s řetězci existuje podpora v samotné syntaxi a sémantice tohoto programovacího jazyka, protože do programů je možné zapisovat řetězcové literály (konstantní řetězce) a navíc pro řetězce existují přetížené verze operátorů + a += (nikoli však již operátoru ==, což stále vede ke vzniku mnohdy těžko objevitelných sémantických chyb). To však není vše, protože řetězcové literály jsou ukládány v constant poolu (http://www.root.cz/clanky/pohled-pod-kapotu-jvm-2-cast-podrobnejsi-analyza-obsahu-constant-poolu), takže řetězce jsou skutečně na platformě Java zpracovávány jiným způsobem, než další třídy a jejich instance (navíc s JDK7 existuje pro řetězce speciální forma rozeskoku switch – viz též úvodní článek tohoto seriálu http://www.root.cz/clanky/novinky-v-nbsp-jdk-7-aneb-mirny-pokrok-v-nbsp-mezich-zakona-1/).

Z hlediska vývojáře je důležitý taktéž fakt, že řetězce jsou neměnitelné (immutable), což s sebou přináší jak některé výhody, tak i komplikace, především ve chvíli, kdy se mají s řetězci provádět složitější operace. Interně řetězce vypadají poměrně jednoduše, alespoň při prvním pohledu, protože jsou tvořeny polem char[], což znamená, že každý prvek tohoto pole má velikost šestnáct bitů. Pro většinu abeced platí, že každý znak uložený v tomto poli skutečně odpovídá jednomu znaku dané abecedy (latinky, azbuky, alfabety …), ovšem ve skutečnosti není možné v rozsahu 0..216 reprezentovat libovolný znak definovaný v normě Unicode. To platí pouze pro znaky s kódem 0×0000 až 0×FFFF, které se někdy označují názvem Basic Multilingual Plane (BMP), které jsou v řetězci uloženy stylem jeden_znak_abecedy == 1 char. Pro znaky s vyšším kódem (až 0×10FFFF) je nutné v řetězci vyhradit pár hodnot typu char, tento pár se jmenuje surrogate pair.

8. Struktura objektu představujícího řetězec

V předchozích kapitolách jsme si řekli, že pole jsou na haldě uloženy takovým způsobem, že je pro ně nutné alokovat větší objem paměti, než by to odpovídalo počtu prvků pole vynásobeného velikostí jednoho prvku. V případě řetězců je poněkud paradoxně situace ještě horší, protože řetězec je na haldě představován dvěma objekty – instancí třídy String a instancí pole char[] – toto pole je sice atributem třídy String, ovšem pole jsou, jak již víme, chápána jako objekty, tudíž se namísto pole ukládá pouze jeho reference. To s sebou nese určitou (osobně bych řekl, že velmi velkou) režii, která vede k tomu, že řetězce používané v typických javovských aplikacích spotřebují daleko větší množství operační paměti, než je na první pohled patrné.

Nejprve se podívejme na to, jak je uložena každá instance třídy String:

# 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ů

Celková velikost paměti pro uložení instance třídy String je tedy minimálně 24 bajtů, a to pouze v případě, že reference na objekty má šířku 32 bitů a nikoli 64 bitů (i to je možné v případě větších kapacit hald). Oněch 24 bajtů se označuje shallow size („mělká velikost“ značící, že nebereme v úvahu velikosti objektových atributů) a je stejná pro všechny instance třídy String.

To však samozřejmě není vše, protože instance třídy String se pouze odkazuje na pole char[], které je na haldě uloženo následovně:

# 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 x length*2 vlastní obsah řetězce
4 0 nebo 2 PADDING výplň, aby celková délka objektu byla dělitelná čtyřmi (někdy osmi)

I pro pole char[], které bude obsahovat pouze jeden znak, tedy bude na haldě alokována paměť o velikosti šestnácti bajtů!

9. Efektivita uložení řetězců na haldě

Z textu uvedeného v předchozí kapitole plyne, že uložení především kratších řetězců nemusí být vůbec efektivní z hlediska využití operační paměti. Pro úplnost si řekněme, že celková kapacita paměti alokovaná na haldě pro určitý objekt se nazývá retained size a v případě řetězců se jedná o součet shallow size s velikostí paměti alokované pro pole char[], tedy:

Retained size = Shallow size + char[].HEADER + char[].length + (chars * 2 + padding)

Když si uvědomíme, že poměr mezi pamětí vyhrazenou POUZE pro vlastní obsah řetězce (znaky zde uložené) a hodnotou retained size nelineárně roste se zvětšující se délkou řetězce, vyjde nám, že zejména při práci s krátkými řetězci je halda využívána dosti neefektivním způsobem (jak neefektivním, si ukážeme níže). Krátké řetězce se však v Javovských aplikacích vyskytují velmi často; používají se jako klíče do hešovacích map, jména souborů bývají krátká, jména a příjmení lidí taktéž, nehledě na jména tříd a balíčků (řetězce využívané interně samotnou JVM)… Navíc se k tomu přidává fakt, že například při používání aplikací v anglicky mluvících prostředích (ale i v dalších zemích se základní či rozšířenou latinkou) se prakticky vždy využije pouze 8 bitů z 16 bitů alokovaných na každou hodnotu typu char, takže je využití ve skutečnosti ještě poloviční oproti prostému poměru chars*2/retained_size.

Mimochodem – i při spuštění toho nejjednoduššího javovského programu typu Hello world se na haldě vytvoří přes 900 převážně krátkých řetězců.

10. Výpočet efektivity uložení řetězců na haldě

Abychom si ukázali, že uložení krátkých řetězců na haldě je skutečně v mnoha případech dosti neefektivní, můžeme si vyzkoušet vygenerovat tabulku a z ní odvozený graf, na němž bude vykreslen poměr chars*2/retained_size, tj. poměr mezi počtem znaků uložených v řetězci (dvojka je zde z toho důvodu, že šířka datového typu char je v Javě 16 bitů). Tabulka bude vygenerována pro všechny délky řetězce, pro něž je efektivita uložení menší než 90% (znovu je důležité zopakovat, že v případě programů pracujících pouze s ASCII sadou je toto číslo ve skutečnosti vlastně poloviční). Program pro výpočet této tabulky je jednoduchý (zdrojový kód si můžete stáhnout z adresy http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/f61c1eead131/to­ols/string_storage.c):

/* Vypocet efektivity ulozeni retezcu na halde. */
/* Autor: Pavel Tisnovsky */
 
#include <stdio.h>
 
/* Hlavicka objektu ulozenych na halde */
const int HEADER_SIZE = 8;
 
/* Atributy kazde instance tridy String */
const int STRING_CLASS_OFFSET_FIELD = 4;
const int STRING_CLASS_COUNT_FIELD  = 4;
const int STRING_CLASS_HASH_FIELD   = 4;
const int STRING_CLASS_ARRAY_REF    = 4;
 
/* Atribut ulozeny spolecne s kazdym polem. */
const int ARRAY_LENGTH = 4;
 
int main(int argc, char **argv)
{
    int str_length = 0;
    float effectivity;
 
    /* Hlavicka tabulky */
    printf("j_chars\tBytes\tPadding\tBytes+Padding\tShallow size\tRetained size\tEffectivity\n");
 
    /* Vypocet a zobrazeni efektivity ulozeni retezcu pro ruzne delky */
    do {
        /* char v Jave ma delku 2 bajty */
        int bytes = str_length << 1;
 
        /* dorovnani delky na velikost delitelnou ctyrmi */
        int padding = (str_length % 2) << 1;
 
        /* na zacatku je vetsinou 24 bajtu "rozsirene hlavicky" */
        int shallow_size = HEADER_SIZE
                         + STRING_CLASS_OFFSET_FIELD
                         + STRING_CLASS_COUNT_FIELD
                         + STRING_CLASS_HASH_FIELD
                         + STRING_CLASS_ARRAY_REF;
        int retained_size = shallow_size + HEADER_SIZE + ARRAY_LENGTH + bytes + padding;
        /* 0..100 % */
        effectivity = 100.0 * bytes / retained_size;
 
        /* tisk jednoho radku tabulky */
        printf("%d\t%d\t%d\t%d\t%d\t%d\t%5.1f%%\n",
                str_length, bytes, padding, bytes + padding, shallow_size, retained_size,
                effectivity);
        str_length++;
    } while (effectivity < 90.0);
 
    return 0;
}

Nejprve bude ukázán numerický výpis:

CS24_early

j_chars Bytes Padding Bytes+Padding Shallow size Retained size Effectivity
0 0 0 0 24 36 0.0%
1 2 2 4 24 40 5.0%
2 4 0 4 24 40 10.0%
3 6 2 8 24 44 13.6%
4 8 0 8 24 44 18.2%
5 10 2 12 24 48 20.8%
6 12 0 12 24 48 25.0%
7 14 2 16 24 52 26.9%
8 16 0 16 24 52 30.8%
9 18 2 20 24 56 32.1%
10 20 0 20 24 56 35.7%
11 22 2 24 24 60 36.7%
12 24 0 24 24 60 40.0%
13 26 2 28 24 64 40.6%
14 28 0 28 24 64 43.8%
15 30 2 32 24 68 44.1%
16 32 0 32 24 68 47.1%
17 34 2 36 24 72 47.2%
18 36 0 36 24 72 50.0%
19 38 2 40 24 76 50.0%
20 40 0 40 24 76 52.6%
21 42 2 44 24 80 52.5%
22 44 0 44 24 80 55.0%
23 46 2 48 24 84 54.8%
24 48 0 48 24 84 57.1%
25 50 2 52 24 88 56.8%
26 52 0 52 24 88 59.1%
27 54 2 56 24 92 58.7%
28 56 0 56 24 92 60.9%
29 58 2 60 24 96 60.4%
30 60 0 60 24 96 62.5%
31 62 2 64 24 100 62.0%
32 64 0 64 24 100 64.0%
33 66 2 68 24 104 63.5%
34 68 0 68 24 104 65.4%
35 70 2 72 24 108 64.8%
36 72 0 72 24 108 66.7%
37 74 2 76 24 112 66.1%
38 76 0 76 24 112 67.9%
39 78 2 80 24 116 67.2%
40 80 0 80 24 116 69.0%
41 82 2 84 24 120 68.3%
42 84 0 84 24 120 70.0%
43 86 2 88 24 124 69.4%
44 88 0 88 24 124 71.0%
45 90 2 92 24 128 70.3%
46 92 0 92 24 128 71.9%
47 94 2 96 24 132 71.2%
48 96 0 96 24 132 72.7%
49 98 2 100 24 136 72.1%
50 100 0 100 24 136 73.5%
51 102 2 104 24 140 72.9%
52 104 0 104 24 140 74.3%
53 106 2 108 24 144 73.6%
54 108 0 108 24 144 75.0%
55 110 2 112 24 148 74.3%
56 112 0 112 24 148 75.7%
57 114 2 116 24 152 75.0%
58 116 0 116 24 152 76.3%
59 118 2 120 24 156 75.6%
60 120 0 120 24 156 76.9%
61 122 2 124 24 160 76.3%
62 124 0 124 24 160 77.5%
63 126 2 128 24 164 76.8%
64 128 0 128 24 164 78.0%
65 130 2 132 24 168 77.4%
66 132 0 132 24 168 78.6%
67 134 2 136 24 172 77.9%
68 136 0 136 24 172 79.1%
69 138 2 140 24 176 78.4%
70 140 0 140 24 176 79.5%
71 142 2 144 24 180 78.9%
72 144 0 144 24 180 80.0%
73 146 2 148 24 184 79.3%
74 148 0 148 24 184 80.4%
75 150 2 152 24 188 79.8%
76 152 0 152 24 188 80.9%
77 154 2 156 24 192 80.2%
78 156 0 156 24 192 81.3%
79 158 2 160 24 196 80.6%
80 160 0 160 24 196 81.6%
81 162 2 164 24 200 81.0%
82 164 0 164 24 200 82.0%
83 166 2 168 24 204 81.4%
84 168 0 168 24 204 82.4%
85 170 2 172 24 208 81.7%
86 172 0 172 24 208 82.7%
87 174 2 176 24 212 82.1%
88 176 0 176 24 212 83.0%
89 178 2 180 24 216 82.4%
90 180 0 180 24 216 83.3%
91 182 2 184 24 220 82.7%
92 184 0 184 24 220 83.6%
93 186 2 188 24 224 83.0%
94 188 0 188 24 224 83.9%
95 190 2 192 24 228 83.3%
96 192 0 192 24 228 84.2%
97 194 2 196 24 232 83.6%
98 196 0 196 24 232 84.5%
99 198 2 200 24 236 83.9%
100 200 0 200 24 236 84.7%
101 202 2 204 24 240 84.2%
102 204 0 204 24 240 85.0%
103 206 2 208 24 244 84.4%
104 208 0 208 24 244 85.2%
105 210 2 212 24 248 84.7%
106 212 0 212 24 248 85.5%
107 214 2 216 24 252 84.9%
108 216 0 216 24 252 85.7%
109 218 2 220 24 256 85.2%
110 220 0 220 24 256 85.9%
111 222 2 224 24 260 85.4%
112 224 0 224 24 260 86.2%
113 226 2 228 24 264 85.6%
114 228 0 228 24 264 86.4%
115 230 2 232 24 268 85.8%
116 232 0 232 24 268 86.6%
117 234 2 236 24 272 86.0%
118 236 0 236 24 272 86.8%
119 238 2 240 24 276 86.2%
120 240 0 240 24 276 87.0%
121 242 2 244 24 280 86.4%
122 244 0 244 24 280 87.1%
123 246 2 248 24 284 86.6%
124 248 0 248 24 284 87.3%
125 250 2 252 24 288 86.8%
126 252 0 252 24 288 87.5%
127 254 2 256 24 292 87.0%
128 256 0 256 24 292 87.7%
129 258 2 260 24 296 87.2%
130 260 0 260 24 296 87.8%
131 262 2 264 24 300 87.3%
132 264 0 264 24 300 88.0%
133 266 2 268 24 304 87.5%
134 268 0 268 24 304 88.2%
135 270 2 272 24 308 87.7%
136 272 0 272 24 308 88.3%
137 274 2 276 24 312 87.8%
138 276 0 276 24 312 88.5%
139 278 2 280 24 316 88.0%
140 280 0 280 24 316 88.6%
141 282 2 284 24 320 88.1%
142 284 0 284 24 320 88.8%
143 286 2 288 24 324 88.3%
144 288 0 288 24 324 88.9%
145 290 2 292 24 328 88.4%
146 292 0 292 24 328 89.0%
147 294 2 296 24 332 88.6%
148 296 0 296 24 332 89.2%
149 298 2 300 24 336 88.7%
150 300 0 300 24 336 89.3%
151 302 2 304 24 340 88.8%
152 304 0 304 24 340 89.4%
153 306 2 308 24 344 89.0%
154 308 0 308 24 344 89.5%
155 310 2 312 24 348 89.1%
156 312 0 312 24 348 89.7%
157 314 2 316 24 352 89.2%
158 316 0 316 24 352 89.8%
159 318 2 320 24 356 89.3%
160 320 0 320 24 356 89.9%
161 322 2 324 24 360 89.4%
162 324 0 324 24 360 90.0%

Zajímavější je však graf, který ukazuje (pravá Y-ová osa a průběh vykreslený hnědou barvou), jak efektivita uložení nelineárně roste a teprve pro řetězce o délce 161 znaků překračuje 90 % (z 360 alokovaných bajtů se 324 bajtů použije pro uložení znaků, zbytek je vlastně režie).

string_storage_graph

11. Zdrojové kódy všech tří agentů a k nim příslušných testovacích příkladů

Podobně jako ve čtyřech předcházejících částech tohoto seriálu byly i dnešní tři demonstrační příklady kvůli snazšímu udržování všech zdrojových kódů uloženy 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:

12. 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.