Pohled pod kapotu JVM – základy optimalizace aplikací naprogramovaných v Javě (4)

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

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
V dnešní části seriálu o programovacím jazyce Java i o virtuálním stroji Javy se budeme zabývat problematikou synchronizace a atomicity operací v javovských aplikacích. Mimo jiné si řekneme, jaké problémy může způsobit použití atributů s modifikátorem „volatile“ i způsob využití takzvaných safe-pointů.

Obsah

1. Pohled pod kapotu JVM – základy optimalizace aplikací naprogramovaných v Javě (4)

2. Atributy s modifikátorem volatile

3. Ukázkový příklad číslo 1 – použití atributů typu volatile int

4. Výsledky běhu prvního demonstračního příkladu

5. Ukázkový příklad číslo 2 – použití atributů typu volatile long

6. Vnitřní synchronizace: technologie safe-pointů

7. Princip práce safe-pointů v současných virtuálních strojích

8. Obsah následující části seriálu

9. Repositář se zdrojovými kódy všech demonstračních i testovacích příkladů

10. Odkazy na Internetu

1. Pohled pod kapotu JVM – základy optimalizace aplikací naprogramovaných v Javě (4)

V dnešní části seriálu o programovacím jazyce Java i o virtuálním stroji Javy si řekneme, jakými způsoby je zajištěna atomicita některých operací a taktéž se zmíníme o tom, jak je implementována synchronizace v javovských aplikacích. Nebudeme se však prozatím do podrobností zabývat „explicitní“ synchronizací s využitím synchronizovaných metod a synchronizovaných metod, ale poněkud méně viditelnou, ovšem velmi významnou technologií – jedná se o takzvané safe-pointy, které jsou používány jak interpretrem Javy, tak i JIT překladači typu klient a server.

Nejprve si pro úplnost řekněme, jakými způsoby je vůbec možné zajistit synchronizaci v aplikacích naprogramovaných v Javě. Pravděpodobně nejznámější způsob spočívá ve využití již zmíněných synchronizovaných metod a synchronizovaných bloků. V případě synchronizovaných bloků se při překladu do bajtkódu generují instrukce monitorentermonitorexit, jejichž význam a implementaci si popíšeme příště. Pro synchronizované metody připravuje a obhospodařuje příslušný zámek (lock) samotný JVM (to, že je metoda synchronizována, je označeno jednobitovým příznakem v bajtkódu).

Další možnost zajištění synchronizace spočívá ve využití rozhraní a tříd z balíčku java.util.concurrent.locks, zatímco o zajištění atomicity vybraných operací (což se synchronizací velmi úzce souvisí) se starají rozhraní a třídy, které lze nalézt v balíčku java.util.concurrent.atomic. Programovací jazyk Java navíc obsahuje i podporu pro modifikátor volatile sloužící k deklaraci volatilních atributů (ne však již parametrů ani lokálních proměnných), s nimiž se interně pracuje značně rozdílným způsobem, než s nevolatilními (tj. vlastně „normálními“) atributy. S použitím atributů s modifikátorem volatile souvisí i několik na první pohled možná ne zcela zřejmých problémů, které si popíšeme v následujících pěti kapitolách.

2. Atributy s modifikátorem volatile

Práce s volatilními atributy je problematická z toho důvodu, že čtení i zápis těchto atributů musí být proveden atomicky, a to za všech okolností. To znamená, že při zápisu nové hodnoty do volatilního atributu by tuto hodnotu měly posléze načíst všechna ostatní vlákna. Zdánlivě se tedy o žádný problém nejedná, ve skutečnosti zde však můžeme nalézt hned dvě problematická místa.

Prvním z nich je zajištění atomicity čtení a zápisu atributů s bitovou šířkou 64 bitů na 32bitových platformách. Zatímco běžný atribut typu long či double může být velmi snadno přečten ve dvou krocích, u atributů volatilních je nutné zajistit použití 64bitových registrů – na současných mikroprocesorech se jedná o instrukce SSE2 pracující s registry XMM0 až XMM7 (konkrétně se pro přenos dat z paměti do XMMx či naopak používá instrukce movsd, s níž jsme se ostatně již seznámili minule). Na starších mikroprocesorech se používají registry matematického koprocesoru a instrukce fild*fist* (platí pro uniprocesorové počítače, zatímco u počítačů s více jádry je již nutné použít lock).

Druhý problém související s volatilními atributy spočívá v tom, že změna hodnoty těchto atributů musí být viditelná i pro další vlákna, což znamená nutnost synchronizace vyrovnávacích pamětí (cache) jednotlivých mikroprocesorů či procesorových jader. Navíc je nutné, aby ostatní vlákna viděla postupnou změnu hodnoty volatilního atributu přesně v tom pořadí, v jakém ke změně došlo. To poněkud omezuje možnosti optimalizací při JIT překladu; nové hodnoty volatilních atributů se ihned ukládají do paměti a na mnoha architekturách (včetně x86) se používají paměťové bariéry (implementované například instrukcí pro čtení paměti, u níž je uveden prefix lock.

Důležitá poznámka: u volatilních atributů je zajištěna pouze atomicita čtení a zápisu, nikoli již dalších složitějších operací. Například u atributu volatile long x není a nemůže být zajištěno, že operace x++ proběhne atomicky – naopak, dvě souběžná vlákna si mohou navzájem přepsat své výsledky. Nicméně by se nikdy nemělo stát, že by při operaci x=0×0000000000000000; x=0×ffffffffffffffff nějaké další vlákno přečetlo hodnotu 0×00000000ffffffff či 0×ffffffff00000000, a to i na 32bitových architekturách (u nevolatilních atributů to však velmi pravděpodobně nastane :-).

Druhá poznámka: atomicita čtení a zápisu volatilních atributů je teoreticky zajištěna od Javy 5.0, ve skutečnosti však některé verze Javy v tomto ohledu pracovaly chybně. Nicméně v Javě 7 by již vše mělo být opraveno podle specifikace.

3. Ukázkový příklad číslo 1 – použití atributů typu volatile int

Podívejme se nyní na velmi jednoduchý ukázkový příklad, v němž je použita třída obsahující nevolatilní celočíselný atribut x a volatilní celočíselný atribut y. První atribut je postupně zvyšován v programové smyčce implementované v metodě VolatileTest1.testX(), druhý atribut je postupně zvyšován v metodě VolatileTest2.testY(). Zaznamenává se celkový čas běhu první i druhé testovací metody tohoto benchmarku:

public class VolatileTest1 {
    private final static int ITERATIONS = 1000000;
 
    public int x;
    public volatile int y;
 
    // test s nevolatilnim atributem
    private void testX() {
        x = 0;
        for (int i=0; i<ITERATIONS; i++) {
            x += 1;
        }
    }
 
    // test s volatilnim atributem
    private void testY() {
        y = 0;
        for (int i=0; i<ITERATIONS; i++) {
            y += 1;
        }
    }
 
    public static void main(String[] args) {
        VolatileTest1 test = new VolatileTest1();
        long t1, t2, delta_t;
        long sumTestX=0, sumTestY=0;
 
        // provest zadany pocet testu
        for (int i = 0; i < 10; i++) {
            // provest test a zmerit cas behu testu
            t1 = System.nanoTime();
            test.testX();
            t2 = System.nanoTime();
            delta_t = t2 - t1;
            sumTestX += delta_t;
 
            // vypis casu pro jeden test
            System.out.format("Round #%2d testX() time: %,12d ns\n", i, delta_t);
 
            // provest test a zmerit cas behu testu
            t1 = System.nanoTime();
            test.testY();
            t2 = System.nanoTime();
            delta_t = t2 - t1;
            sumTestY += delta_t;
 
            // vypis casu pro jeden test
            System.out.format("Round #%2d testY() time: %,12d ns\n", i, delta_t);
        }
 
        // vypis kumulativniho casu
        System.out.format("Cumulative time for testX(): %,12d ns\n", sumTestX);
        System.out.format("Cumulative time for testY(): %,12d ns\n", sumTestY);
    }
 
}

4. Výsledky běhu prvního demonstračního příkladu

Nyní si zkusíme demonstrační příklad popsaný v předchozí kapitole spustit na počítači s jedním mikroprocesorem (Pentium M) a přitom použijeme přepínač -client:

Round # 0 testX() time:    7 304 001 ns
Round # 0 testY() time:    9 380 523 ns
Round # 1 testX() time:    7 408 764 ns
Round # 1 testY() time:    9 173 512 ns
Round # 2 testX() time:    7 637 004 ns
Round # 2 testY() time:    7 637 005 ns
Round # 3 testX() time:    7 635 886 ns
Round # 3 testY() time:    7 645 665 ns
Round # 4 testX() time:    7 671 925 ns
Round # 4 testY() time:    7 644 827 ns
Round # 5 testX() time:    7 552 915 ns
Round # 5 testY() time:    7 637 004 ns
Round # 6 testX() time:    7 793 729 ns
Round # 6 testY() time:    7 647 899 ns
Round # 7 testX() time:    5 906 337 ns
Round # 7 testY() time:    7 645 385 ns
Round # 8 testX() time:    7 637 004 ns
Round # 8 testY() time:    7 645 106 ns
Round # 9 testX() time:    4 800 890 ns
Round # 9 testY() time:    7 666 897 ns
Cumulative time for testX():   71 348 455 ns
Cumulative time for testY():   79 723 823 ns

Vidíme, že práce s volatilním atributem je nepatrně pomalejší.

Co se ovšem stane, pokud na stejném počítači povolíme použití JIT překladače typu server? Výsledky budou mnohem zajímavější:

Round # 0 testX() time:   18 140 294 ns
Round # 0 testY() time:   15 192 155 ns
Round # 1 testX() time:    3 523 632 ns
Round # 1 testY() time:   11 826 084 ns
Round # 2 testX() time:      158 959 ns
Round # 2 testY() time:    7 845 970 ns
Round # 3 testX() time:      158 679 ns
Round # 3 testY() time:    7 847 087 ns
Round # 4 testX() time:      158 680 ns
Round # 4 testY() time:    7 887 315 ns
Round # 5 testX() time:      158 958 ns
Round # 5 testY() time:    7 571 912 ns
Round # 6 testX() time:      158 959 ns
Round # 6 testY() time:    7 852 675 ns
Round # 7 testX() time:      158 959 ns
Round # 7 testY() time:    7 852 675 ns
Round # 8 testX() time:      158 959 ns
Round # 8 testY() time:    8 026 720 ns
Round # 9 testX() time:      158 959 ns
Round # 9 testY() time:    7 897 094 ns
Cumulative time for testX():   22 935 038 ns
Cumulative time for testY():   89 799 687 ns

Vidíme, že JIT překladač typu server sice dokázal dobře optimalizovat programovou smyčku, v níž se měnil nevolatilní atribut, ovšem u atributu volatilního již úspěšnost nebyla nijak velká. Je tomu tak z toho důvodu, že nedošlo k tak dokonalému rozbalení smyčky a taky z důvodu použití paměťové bariéry. I v takto jednoduchém benchmarku je rozdíl v rychlosti běhu čtyřnásobný!

5. Ukázkový příklad číslo 2 – použití atributů typu volatile long

Nyní demonstrační příklad nepatrně upravíme, a to tak, aby byly oba celočíselné atributy (nevolatilní i volatilní) typu long a nikoli typu int. Druhý demonstrační příklad bude mít následující tvar:

public class VolatileTest2 {
    private final static int ITERATIONS = 1000000;
 
    public long x;
    public volatile long y;
 
    // test s nevolatilnim atributem
    private void testX() {
        x = 0;
        for (int i=0; i<ITERATIONS; i++) {
            x += 1;
        }
    }
 
    // test s volatilnim atributem
    private void testY() {
        y = 0;
        for (int i=0; i<ITERATIONS; i++) {
            y += 1;
        }
    }
 
    public static void main(String[] args) {
        VolatileTest2 test = new VolatileTest2();
        long t1, t2, delta_t;
        long sumTestX=0, sumTestY=0;
 
        // provest zadany pocet testu
        for (int i = 0; i < 10; i++) {
            // provest test a zmerit cas behu testu
            t1 = System.nanoTime();
            test.testX();
            t2 = System.nanoTime();
            delta_t = t2 - t1;
            sumTestX += delta_t;
 
            // vypis casu pro jeden test
            System.out.format("Round #%2d testX() time: %,12d ns\n", i, delta_t);
 
            // provest test a zmerit cas behu testu
            t1 = System.nanoTime();
            test.testY();
            t2 = System.nanoTime();
            delta_t = t2 - t1;
            sumTestY += delta_t;
 
            // vypis casu pro jeden test
            System.out.format("Round #%2d testY() time: %,12d ns\n", i, delta_t);
        }
 
        // vypis kumulativniho casu
        System.out.format("Cumulative time for testX(): %,12d ns\n", sumTestX);
        System.out.format("Cumulative time for testY(): %,12d ns\n", sumTestY);
    }
 
}

Opět zkusíme tento demonstrační benchmark spustit na počítači s mikroprocesorem Pentium M. Jedná se o 32bitovou architekturu, což znamená, že se JIT překladač bude muset ještě více snažit o dosažení atomicity čtení a zápisu volatilního atributu typu long (64 bitů). Nejdříve se podívejme, co se stane v případě použití JIT překladače typu klient:

Round # 0 testX() time:   11 833 347 ns
Round # 0 testY() time:   27 286 150 ns
Round # 1 testX() time:   11 482 745 ns
Round # 1 testY() time:   27 220 499 ns
Round # 2 testX() time:    9 595 353 ns
Round # 2 testY() time:   25 362 721 ns
Round # 3 testX() time:    9 788 675 ns
Round # 3 testY() time:   25 414 123 ns
Round # 4 testX() time:    9 662 122 ns
Round # 4 testY() time:   25 300 702 ns
Round # 5 testX() time:   10 094 858 ns
Round # 5 testY() time:   25 423 622 ns
Round # 6 testX() time:    9 599 544 ns
Round # 6 testY() time:   25 476 701 ns
Round # 7 testX() time:    9 612 953 ns
Round # 7 testY() time:   25 164 092 ns
Round # 8 testX() time:    9 801 525 ns
Round # 8 testY() time:   25 608 282 ns
Round # 9 testX() time:    9 592 001 ns
Round # 9 testY() time:   25 394 569 ns
Cumulative time for testX():  101 063 123 ns
Cumulative time for testY():  257 651 461 ns

Vidíme, že je zde změna volatilního atributu cca 2,5× pomalejší, než u atributu nevolatilního.

V případě použití JIT překladače typu server je rozdíl ještě mnohem větší!

Round # 0 testX() time:   18 521 070 ns
Round # 0 testY() time:   28 023 393 ns
Round # 1 testX() time:    4 280 152 ns
Round # 1 testY() time:   24 841 706 ns
Round # 2 testX() time:      458 717 ns
Round # 2 testY() time:   19 393 247 ns
Round # 3 testX() time:      458 717 ns
Round # 3 testY() time:   19 441 019 ns
Round # 4 testX() time:      458 996 ns
Round # 4 testY() time:   21 765 336 ns
Round # 5 testX() time:      458 717 ns
Round # 5 testY() time:   19 263 342 ns
Round # 6 testX() time:      458 718 ns
Round # 6 testY() time:   19 510 580 ns
Round # 7 testX() time:      464 863 ns
Round # 7 testY() time:   19 269 209 ns
Round # 8 testX() time:      458 717 ns
Round # 8 testY() time:   19 280 104 ns
Round # 9 testX() time:      458 718 ns
Round # 9 testY() time:   19 446 885 ns
Cumulative time for testX():   26 477 385 ns
Cumulative time for testY():  210 234 821 ns

Zde se již jedná o skoro řádový rozdíl – změna volatilního atributu je 8× pomalejší, než u atributu nevolatilního.

Na tomto místě si možná čtenáři říkají, proč je vlastně sáhodlouze popisována problematika práce s 64bitovými volatilními atributy (long, double) na 32bitových architekturách, když se dnes můžeme spíše setkat s 64bitovými servery či osobními počítači. Nesmíme však zapomínat na smartphony a další mobilní zařízení, v nichž se typicky setkáváme s 32bitovou architekturou ARM (a problematika volatilních atributů je prakticky stejná, bez ohledu na to, zda virtuálnímu stroji říkáme Java Virtual Machine či Dalvik ;-).

6. Vnitřní synchronizace: technologie safe-pointů

Všechny synchronizační mechanismy zmíněné v předchozích kapitolách jsou explicitně vyžadovány programátorem, ať již se jedná o použití synchronizovaných metod a bloků či o využití tříd, v nichž se synchronizace vnitřně provádí (typickým příkladem je zmíněná třída java.util.Vector či java.util.StringBuffer). Ovšem kromě těchto na první či druhý pohled rozpoznatelných synchronizačních mechanismů se synchronizace jednotlivých vláken běžících ve virtuálním stroji provádí i na poněkud nižší úrovni, která již nemá přímý obraz ve zdrojovém kódu. To mimochodem znamená, že při pohledu na zdrojový kód již není zcela jednoduché říci, ve kterém okamžiku a ve kterém místě ve zdrojovém kódu k této synchronizaci může docházet. Tento skrytý a přitom velmi důležitý synchronizační mechanismus použitý v HotSpotu i v některých dalších virtuálních strojích se nazývá safe-point(s) (nebudu se zde snažit zavádět novou českou terminologii, význam anglického názvu je asi zřejmý).

Obecně řečeno se pod označením safe-point skrývají určitá místa v bajtkódu či v přeloženém strojovém kódu, v nichž může dojít k pozastavení běhu vláken, aby mohl virtuální stroj Javy provést nějakou operaci, která vyžaduje, aby všechna aplikační vlákna byla nejenom pozastavena, ale aby byla navíc pozastavena v přesně definovaném stavu. Nejtypičtějším důvodem pro použití safe-pointů je nutnost spouštět správce paměti (GC – Garbage Collector), protože se při úklidu paměti mohou měnit adresy jednotlivých objektů a všechna běžící vlákna musí po proběhnutí GC již pracovat s novými adresami (na úrovni programovacího jazyka Java samozřejmě nemáme k přímým adresám přístup).

Důvodů je ovšem ve skutečnosti více, například nutnost pozastavit vlákna při takzvaném hot-swapu (vložení modifikovaného bajtkódu do běžící JVM), použití instrumentace či při některých ladicích operacích (detekce deadlocků, požadavek na vytvoření výpisu obsahu zásobníkových rámců apod.)

7. Princip práce safe-pointů v současných virtuálních strojích

Safe-pointy jsou použity jak při pouhé interpretaci bajtkódu, tak i ve strojovém (nativním) kódu vygenerovaném JIT překladačem typu client (C1) i server (C2). Použitím interpretru se dnes nebudeme příliš zabývat (už jen z praktických důvodů), o to zajímavější je však způsob implementace safe-pointů v současných verzích JIT překladačů virtuálního stroje Javy. Při určování těch oblastí, v nichž mají být safe-pointy použity v generovaném zdrojovém kódu, je nutné brát ohled na dvě navzájem protichůdná hlediska:

  1. Vzhledem k tomu, že safe-pointy jsou mj. používány i těmi správci paměti, které před svým během pozastavují všechna aplikační vlákna (takzvaní stop-the-world či jen STW správci paměti), je z tohoto hlediska vhodné, aby se v kódu vyskytovalo co největší množství safe-pointů. Pokud by totiž safe-point nebyl například vložen do dlouhotrvající programové smyčky, znamenalo by to, že se před vlastním spuštěním GC pozastaví všechna ostatní vlákna aplikace a tato vlákna by jen nečinně čekala na dokončení zmíněné smyčky v posledním nepozastaveném vláknu, což je zcela jistě neakceptovatelná situace (i zde tedy můžeme poměrně názorně vidět, že použití GC není v žádném případě „zadarmo“). Velké množství safe-pointů taktéž napomáhá mnohem lépe lokalizovat ta místa, v nichž došlo k deadlocku.
  2. Na druhou stranu je však vkládání safe-pointů do generovaného strojového kódu náročné jak z paměťového hlediska, tak i z hlediska času běhu všech aplikačních vláken. Teoreticky je sice možné safe-point vložit mezi každou instrukci (což by bylo zcela ideální, pokud by se vzalo do úvahy pouze první hledisko), ovšem to by znamenalo přibližně dvojnásobný nárůst paměťových nároků na velikost oblasti paměti, do níž se generují „JITované“ metody. Ve skutečnosti je ovšem nutné si u každého safe-pointu zapamatovat i další údaje, zejména takzvanou mapu referencí na objekty, takže by spotřeba paměti byla ještě mnohonásobně větší.

8. Obsah následující části seriálu

V následující části tohoto seriálu si nejdříve řekneme, jakým způsobem vyřešili autoři HotSpotu problematiku obou protichůdných hledisek zmíněných v předchozí kapitole. Posléze se seznámíme s principem generování safe-pointů na platformě x86_64; protože se jedná (alespoň podle mého názoru) o dosti elegantní řešení celé problematiky, které je „optimistické“ v tom smyslu, že se generuje poměrně velké množství safe-pointů, ovšem na druhou stranu se optimisticky předpokládá, že se těmito safe-pointy bude ve většině případů pouze procházet bez pozastavení vlákna.

linux_sprava_tip

Takže se opět můžeme těšit na assembler! :-)

9. Repositář se zdrojovými kódy všech demonstračních i testovacích příkladů

Následuje – v tomto seriálu již tradiční – kapitola s odkazy na zdrojové kódy. V následující tabulce najdete odkazy na prozatím nejnovější verze obou dnes použitých jednoduchých benchmarků:

10. Odkazy na Internetu

  1. GC safe-point (or safepoint) and safe-region
    http://xiao-feng.blogspot.cz/2008/01/gc-safe-point-and-safe-region.html
  2. Safepoints in HotSpot JVM
    http://blog.ragozin.info/2012/10/sa­fepoints-in-hotspot-jvm.html
  3. Java theory and practice: Synchronization optimizations in Mustang
    http://www.ibm.com/develo­perworks/java/library/j-jtp10185/
  4. How to build hsdis
    http://hg.openjdk.java.net/jdk7/hot­spot/hotspot/file/tip/src/sha­re/tools/hsdis/README
  5. Java SE 6 Performance White Paper
    http://www.oracle.com/technet­work/java/6-performance-137236.html
  6. Lukas Stadler's Blog
    http://classparser.blogspot­.cz/2010/03/hsdis-i386dll.html
  7. How to build hsdis-amd64.dll and hsdis-i386.dll on Windows
    http://dropzone.nfshost.com/hsdis.htm
  8. PrintAssembly
    https://wikis.oracle.com/dis­play/HotSpotInternals/Prin­tAssembly
  9. The Java Virtual Machine Specification: 3.14. Synchronization
    http://docs.oracle.com/ja­vase/specs/jvms/se7/html/jvms-3.html#jvms-3.14
  10. The Java Virtual Machine Specification: 8.3.1.4. volatile Fields
    http://docs.oracle.com/ja­vase/specs/jls/se7/html/jls-8.html#jls-8.3.1.4
  11. The Java Virtual Machine Specification: 17.4. Memory Model
    http://docs.oracle.com/ja­vase/specs/jls/se7/html/jls-17.html#jls-17.4
  12. The Java Virtual Machine Specification: 17.7. Non-atomic Treatment of double and long
    http://docs.oracle.com/ja­vase/specs/jls/se7/html/jls-17.html#jls-17.7
  13. Open Source ByteCode Libraries in Java
    http://java-source.net/open-source/bytecode-libraries
  14. ASM Home page
    http://asm.ow2.org/
  15. Seznam nástrojů využívajících projekt ASM
    http://asm.ow2.org/users.html
  16. ObjectWeb ASM (Wikipedia)
    http://en.wikipedia.org/wi­ki/ObjectWeb_ASM
  17. Java Bytecode BCEL vs ASM
    http://james.onegoodcooki­e.com/2005/10/26/java-bytecode-bcel-vs-asm/
  18. BCEL Home page
    http://commons.apache.org/bcel/
  19. Byte Code Engineering Library (před verzí 5.0)
    http://bcel.sourceforge.net/
  20. Byte Code Engineering Library (verze >= 5.0)
    http://commons.apache.org/pro­per/commons-bcel/
  21. BCEL Manual
    http://commons.apache.org/bcel/ma­nual.html
  22. Byte Code Engineering Library (Wikipedia)
    http://en.wikipedia.org/wiki/BCEL
  23. BCEL Tutorial
    http://www.smfsupport.com/sup­port/java/bcel-tutorial!/
  24. Bytecode Engineering
    http://book.chinaunix.net/spe­cial/ebook/Core_Java2_Volu­me2AF/0131118269/ch13lev1sec6­.html
  25. Bytecode Outline plugin for Eclipse (screenshoty + info)
    http://asm.ow2.org/eclipse/index.html
  26. Javassist
    http://www.jboss.org/javassist/
  27. Byteman
    http://www.jboss.org/byteman
  28. Java programming dynamics, Part 7: Bytecode engineering with BCEL
    http://www.ibm.com/develo­perworks/java/library/j-dyn0414/
  29. The JavaTM Virtual Machine Specification, Second Edition
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/VMSpec­TOC.doc.html
  30. The class File Format
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/Clas­sFile.doc.html
  31. javap – The Java Class File Disassembler
    http://docs.oracle.com/ja­vase/1.4.2/docs/tooldocs/win­dows/javap.html
  32. javap-java-1.6.0-openjdk(1) – Linux man page
    http://linux.die.net/man/1/javap-java-1.6.0-openjdk
  33. Using javap
    http://www.idevelopment.in­fo/data/Programming/java/mis­cellaneous_java/Using_javap­.html
  34. Examine class files with the javap command
    http://www.techrepublic.com/ar­ticle/examine-class-files-with-the-javap-command/5815354
  35. aspectj (Eclipse)
    http://www.eclipse.org/aspectj/
  36. Aspect-oriented programming (Wikipedia)
    http://en.wikipedia.org/wi­ki/Aspect_oriented_program­ming
  37. AspectJ (Wikipedia)
    http://en.wikipedia.org/wiki/AspectJ
  38. EMMA: a free Java code coverage tool
    http://emma.sourceforge.net/
  39. Cobertura
    http://cobertura.sourceforge.net/
  40. jclasslib bytecode viewer
    http://www.ej-technologies.com/products/jclas­slib/overview.html

Autor článku

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