Hlavní navigace

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

10. 9. 2013
Doba čtení: 16 minut

Sdílet

V dnešní části seriálu o programovacím jazyce Java i o virtuálním stroji Javy se seznámíme se základními technikami optimalizace aplikací naprogramovaných v Javě. Postupně si ukážeme jak vysokoúrovňové optimalizace, tak i nastavování a ladění samotného virtuálního stroje Javy (což se týká především GC a JITu).

Obsah

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

2. Optimalizace na úrovni implementovaných algoritmů

3. Vliv JRE a JVM na chování aplikací naprogramovaných v Javě

4. JIT překladač typu client a server

5. Použití vhodných typů kolekcí

6. Pole a další možnosti náhrady kolekcí

7. Nízkoúrovňové optimalizace – mají vůbec v Javě smysl?

8. Ladění parametrů JVM (aneb JIT ani GC nejsou samospasitelné)

9. Odkazy na Internetu

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

Seriál o programovacím jazyku Java i o virtuálním stroji Javy již dosáhl své devadesáté páté části, přičemž jsme si v předchozích dílech mj. vysvětlili i problematiku monitorování JVM, základy fungování správců paměti i vlastnosti bajtkódu JVM. Prozatím jsme se však hlouběji a systematičtěji nezabývali způsoby optimalizace aplikací naprogramovaných v Javě, i když právě optimalizace na základě různých hledisek by měla být (alespoň v ideálním světě :-) součástí vývoje a testování. Poměrně velké množství vývojářů se testováním výkonnosti a optimalizací svých aplikací příliš nezabývá, a to z mnoha důvodů – ať již se jedná o nedostatek času či o pocit, že vyššího výkonu lze snadno dosáhnout výkonnějším hardwarem (což si ukážeme, že nemusí být vždy pravda) či že se o „vyladění“ aplikace nějak postará překladač a/nebo samotný virtuální stroj Javy (to je také mnohdy dosti vzdáleno od skutečnosti).

Při pohledu do různých diskuzních fór je patrné, že pohled na JVM a Javovské aplikace běžně osciluje mezi dvěma názorovými extrémy. Na jedné straně stojí názor (bohužel někdy potvrzený špatně naprogramovanou aplikací či mizernou JVM), že Javovské aplikace vyžadují neúměrné množství operační paměti a že jejich rychlost daleko zaostává za aplikacemi napsanými v C/C++/D či v dalších kompilovaných programovacích jazycích. Na druhé straně názorového spektra se tvrdí, že díky existenci JIT (Just-In-Time) překladače javovské aplikace dosahují stejného výkonu jako aplikace nativní. Pravda ve skutečnosti leží někde uprostřed; my si především v dalších dílech tohoto seriálu řekneme podrobnosti o práci JIT překladače, protože teprve s pochopením jeho funkcionality je možné si uvědomit, za jakých okolností vlastně JIT začíná provádět překlad a jaké programové konstrukce mu mohou pomoci či naopak znesnadnit jeho práci.

Upozornění: dnešní článek sice nebude obsahovat mnoho konkrétních informací o JIT překladačích ani o GC, to se však v dalších částech napraví.

2. Optimalizace na úrovni implementovaných algoritmů

Způsoby optimalizace (nejenom) javovských aplikací lze rozdělit na několik úrovní. Na úrovni nejvyšší a většinou také nejdůležitější stojí dobrá implementace algoritmu, který má řešit daný problém. Typickým školním příkladem bývá předvedení algoritmů pro setřídění prvků nějaké kolekce či pole. Naivní algoritmy typu Bubble Sort mají složitost O(n2), ovšem lze použít také algoritmy typu Merge Sort se složitostí O(n*log(n)) – tento algoritmus mimochodem není nutné implementovat, protože ho najdeme (dokonce v několika různých modifikacích) v java.util.Arrays.sort()java.util.Collections.sort(). Dalším příkladem, tentokrát odpozorovaným z reálných aplikací, byla nevhodně navržená struktura programů takovým způsobem, že se několikrát načítal a zbytečně zpracovával stejný soubor – konkrétně se jednalo o XML soubor obsahující konfiguraci programu, jenž byl v jedné aplikaci načítán a zpracováván hned několikrát, pokaždé se přitom získávala odlišná informace (uložená v jiném uzlu).

Do této úrovně optimalizace je taktéž možné zařadit implementaci daného algoritmu takovým způsobem, aby se dobře využily možnosti mikroprocesorů s více jádry. Samotný překladač ani virtuální stroj Javy totiž nejsou v současnosti na takové úrovni, aby dokázaly výpočty dobře a automaticky paralelizovat – přitom právě zde by mohl vysokoúrovňový jazyk s vlastní VM excelovat a překonat tak tradiční kompilované jazyky, jak je to ostatně dobře patrné na příkladu Clojure. Paralelizace výpočtů je tak většinou nutné řešit přímo při tvorbě zdrojového kódu programu, což je v Javě složité a může vést k mnoha těžko odhalitelným chybám (typické deadlocky či naopak nesynchronizovaný přístup k nějakému objektu). Určité vylepšení v této oblasti přinesla až Java 8, což je však problematika, které se budeme podrobněji věnovat v další části tohoto seriálu.

3. Vliv JRE a JVM na chování aplikací naprogramovaných v Javě

Na běh aplikací naprogramovaných v Javě má výrazný vliv i vybrané JRE a JVM (samotný virtuální stroj). Připomeňme si, že virtuální stroj Javy se skládá ze tří hlavních subsystémů. Jedná se o JIT překladač, správce pamětiběhové prostředí (runtime – interpret, standardní class loader, synchronizační mechanismy, správce vláken). Nejprve se budeme zabývat JIT překladači, kvůli jejich velkému vlivu na výkon aplikací. V současnosti se můžeme setkat s dvěma typy JIT překladačů – clientserver, které se v nových JVM kombinují do takzvaného vícevrstvého překladače tiered compiler (principem jeho práce se opět budeme zabývat později).

Velký vliv na výkon javovských aplikací má i použitý správce paměti. Podobně jako existují dva typy JIT překladačů, máme dnes k dispozici i větší množství správců paměti, z nichž každý je vhodný pouze pro určitý účel a pro určitou velikost haldy, na níž jsou ukládány všechny objekty. Správce paměti (garbage collector) je spouštěn v samostatném vláknu či více vláknech, takže pracuje paralelně s běžící aplikací. Operace prováděné správcem paměti samozřejmě nemohou probíhat zcela nezávisle na aplikaci, protože se například provádí defragmentace haldy (heapu), s čímž souvisí potřeba některé objekty uzamykat – právě v těchto chvílích může být běh aplikace pozastavován, a to i na systémech, které jsou vybaveny mikroprocesory s více jádry (u některých správců paměti se proto setkáme s termínem stop-the-world).

Nesmíme zapomínat ani na běhové prostředí (runtime). Vliv interpretru si částečně osvětlíme v navazující kapitole, důležitý je však i systém synchronizace. V současnosti je v JVM implementován takzvaný optimistický mechanismus zámků, který předpokládá, že je velmi pravděpodobné, že o zámek bude žádat vždy stále to stejné vlákno, které tento zámek ihned získá. Pokud tomu tak není (objekt je zamčen jiným vláknem, …), je synchronizace obecně časově náročnější, s čímž je zapotřebí počítat i při návrhu či optimalizacích aplikace.

4. JIT překladač typu client a server

V předchozí kapitole jsme si řekli, že v současných JVM se setkáme s dvojicí JIT překladačů – client a server.

Oba typy překladačů se spouští až na základě požadavku interpretru – bajtkód každé metody je tedy nejdříve relativně pomalu interpretován a teprve poté, pokud se daná metoda volá často nebo pokud se některá smyčka v metodě opakuje po zadanou mez, se metoda či jen její často opakovaná smyčka přeloží. Přitom platí, že překladač typu client je navržen takovým způsobem, že velmi jednoduše a tudíž i rychle danou sekvenci instrukcí bajtkódu přeloží na základě „šablony“ obsahující (velmi zjednodušeně řečeno) sekvenci strojových instrukcí pro každou instrukci bajtkódu. Provádí se přitom jen minimální množství optimalizací, takže od tohoto typu překladače nelze čekat žádné zázraky.

Naproti tomu překladač typu server již dokáže provádět různé lokální i globální optimalizace a taktéž stráví větší množství času s alokací registrů mikroprocesoru pro potřeby uložení lokálních proměnných, mezivýsledků výpočtů atd. atd. Ovšem aby tento typ překladače pracoval skutečně efektivně, musí mít k dispozici větší množství informací o dynamických vlastnostech metody/smyčky – a tyto informace získá opět z interpretru. Zjednodušeně řečeno to znamená, že čím vícekrát je metoda/smyčka zpracována pomalým interpretrem, tím lepší informace má k dispozici optimalizující JIT překladač, takže se dostáváme do poněkud paradoxní situace, že pro dlouhotrvající aplikace je výhodnější, když jsou nějaký čas provozovány jen v režimu interpretru (jak to přesně funguje a jak je možné nastavit příslušné parametry JVM si řekneme příště).

Z výše uvedeného taktéž vyplývá, že JIT překladače typu server nemusí být vždy výhodnější a pro aplikace běžící jen krátkou dobu může být JIT překladač typu client lepší volbou. Výkon JIT překladačů se taktéž postupně vylepšuje, což je patrné i z následující tabulky porovnávající dobu běhu jednoho benchmarku (zmíněného příště) na dvou různých JVM v režimu interpretru, JIT překladače typu client i JIT překladače typu server:

Překladač/interpret Java 1.6.0 Java 1.7.0_25
-Xint 5018676954 ns 5016002314 ns
-client  407990681 ns  409226033 ns
-server  512994248 ns  334564030 ns

Povšimněte si toho, že interpret je více než 10× pomalejší v porovnání s JIT i toho, jak se vylepšily vlastnosti JIT typu server.

Poznámka: pokud se spoléháte na to, že na počítačích klientů bude vždy použit JIT překladač typu server, je nutné zde upozornit na fakt, že pro 32bitové systémy se již Oracle JRE 7 dodává jen a pouze ve verzi client. Kdo potřebuje i JIT server, musí si nainstalovat celé JVM (nebo použít OpenJDK, což je většinou lepší volba).

5. Použití vhodných typů kolekcí

Jen pro úplnost je nutné v tomto článku zmínit i nutnost použití vhodného typu kolekcí, popř. zvážit, zda použití klasických polí nepovede k nárůstu výkonu aplikace. Následující tabulka se čtyřmi typy kolekcí a jejich základními implementacemi je velmi pravděpodobně čtenářům tohoto článku dobře známá:

Rozhraní Implementace
polem
Implementace
stromem
Implementace
seznamem
Implementace
hash mapou
Set   TreeSet   HashSet, LinkedHashSet
Map   TreeMap   HashMap, LinkedHashMap
List ArrayList   LinkedList  
Deque ArrayDeque   LinkedList  

Z této tabulky je patrné, že každé rozhraní je implementováno minimálně dvakrát, někdy i třikrát. To má samozřejmě svůj význam, protože každá implementace má odlišné chování. Nás bude nejvíce zajímat časová složitost, kterou lze poměrně snadno zjistit pohledem do zdrojových kódů (ty jsou v JDK k dispozici). Pro ilustraci si uveďme časovou složitost některých operací prováděných nad kolekcí typu List:

Operace ArrayList LinkedList
set(index,E) O(1) O(n)
get(index) O(1) O(n)
add(index,E) O(n) O(n)
remove(index) O(n) O(n)
toArray() O(n) * O(n) **

* – provádí se kopie celého pole, ** – provádí se postupné kopírování prvků do pole

Podobně lze zjistit časovou složitost kolekce typu Deque (což je kolekce sdružující vlastnosti obousměrné fronty a seznamu):

Operace ArrayDeque LinkedList
addFirst() O(1) nebo O(n) O(1)
addLast() O(1) nebo O(n) O(1)
removeFirst() O(1) O(1)
removeLast() O(1) O(1)
getFirst() O(1) O(1)
getLast() O(1) O(1)
toArray() O(n) * O(n) **

* – provádí se kopie celého pole, ** – provádí se postupné kopírování prvků do pole

Podobně je možné snadno zjistit vlastnosti a rozdíly mezi dvojicí HashSet/TreeSetHashMap/TreeMap. Další alternativou je použití ConcurrentHashMap, která v některých případech nabízí vyšší výkonnost (tu je ovšem nutné otestovat pomocí profileru).

Ovšem implementace kolekcí, která je dodávaná přímo s JRE, není v žádném případě jediná existující implementace. K dispozici jsou totiž i mnohé další implementace, které se liší jak použitými algoritmy, tak i způsobem uložení kolekce na haldě.

6. Pole a další možnosti náhrady kolekcí

Kolekce se v Javě používají velmi jednoduše, což však může znamenat, že je programátor někdy bezmyšlenkovitě využije i tam, kde to není vhodné. Existuje totiž poměrně velké množství situací, v nichž jsou běžná pole mnohem efektivnější, a to jak z hlediska obsazené paměti, tak i z hlediska rychlosti přístupu k prvkům. Typickým příkladem je nutnost ukládat prvky primitivních datových typů, což není u kolekcí možné – zde je nutné aplikovat autoboxing, který je časově náročný a navíc i objekt obalující hodnotu primitivního datového typu obsadí velké množství paměti na haldě (tu navíc prochází správce paměti, čímž výkonnostní problémy ještě narůstají). Navíc jsou všechny operace s polem překládány do k tomu určených instrukcí bajtkódu, zatímco při práci s kolekcemi libovolného typu je nutné stále volat příslušné metody (add, remove, get, set, …).

Sice by se mohlo zdát, že JIT překladače si s tímto problémem poradí, ale jak již bylo naznačeno ve čtvrté kapitole, není vhodné se slepě spoléhat na magické schopnosti JITu, už jen z toho důvodu, že se v mnoha případech ve skutečnosti bude kód pouze interpretovat. Ostatně ukažme si jednoduchý odstrašující příklad:

import java.util.*;
 
public class Test {
 
    private static final int LIST_SIZE = 5000;
 
    public static void main(String[] args) {
        List<Integer> x = new ArrayList<Integer>(LIST_SIZE);
        for (int i = 0; i < LIST_SIZE; i++) {
            x.add(i);
        }
    }
 
}

V tomto příkladu je seznam celých čísel uložen velmi neefektivně (kvůli autoboxingu), samotné vložení jednoho prvku znamená volání několika metod a navíc je velikost seznamu (5000 prvků) tak malá, že se ani nespustí JIT překladač typu server! Můžeme se o tom sami přesvědčit:

javac Test.java
java -server -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation Test

(tyto přepínače budou podrobně vysvětleny příště).

Naproti tomu je práce s poli podporována přímo v bajtkódu, a to konkrétně následujícími instrukcemi:

# Instrukce Opkód Operandy Prováděná operace
1 newarray 0×BC arraytype Vytvoří nové pole s prvky primitivního datového typu
2 anewarray 0×BD highbyte, lowbyte Vytvoří nové pole objektů
3 multianewarray 0×C5 highbyte, lowbyte, dimensions Vytvoří vícedimenzionální pole o dimensions dimenzích
4 iaload 0×2E × přečtení prvku z pole typu int[]
5 laload 0×2F × přečtení prvku z pole typu long[]
6 faload 0×30 × přečtení prvku z pole typu float[]
7 daload 0×31 × přečtení prvku z pole typu double[]
8 aaload 0×32 × přečtení prvku z pole typu reference[]
9 baload 0×33 × přečtení prvku z pole typu byte[] nebo boolean[]
10 caload 0×34 × přečtení prvku z pole typu char[]
11 saload 0×35 × přečtení prvku z pole typu short[]
12 iastore 0×4F × zápis nové hodnoty prvku do pole typu int[]
13 lastore 0×50 × zápis nové hodnoty prvku do pole typu long[]
14 fastore 0×51 × zápis nové hodnoty prvku do pole typu float[]
15 dastore 0×52 × zápis nové hodnoty prvku do pole typu double[]
16 aastore 0×53 × zápis nové hodnoty prvku do pole typu reference[]
17 bastore 0×54 × zápis nové hodnoty prvku do pole typu byte[] nebo boolean[]
18 castore 0×55 × zápis nové hodnoty prvku do pole typu char[]
19 sastore 0×56 × zápis nové hodnoty prvku do pole typu short[]
20 arraylength 0×BE × zjištění délky pole

Přístup k prvkům pole se většinou překládá do tří kroků (viz též například zdrojový soubor hotspot/src/cpu/x86/vm/tem­plateTable_x86_32.cpp ve zdrojových kódech OpenJDK):

  1. Kontrola, zda objekt představující pole není null
  2. Kontrola, zda index je menší než mez pole
  3. Vlastní čtení/zápis (1–2 instrukce)

Optimalizační překladač může první dva kroky zcela eliminovat a dosáhnout tak rychlosti srovnatelné s přeloženým céčkem.

7. Nízkoúrovňové optimalizace – mají vůbec v Javě smysl?

Poněkud překvapující může být fakt, že i některé „nízkoúrovňové“ zásahy do zdrojového kódu mohou vylepšit čas běhu aplikace. Podívejme se na velmi jednoduchý (i když trošku umělý) příklad aplikace, v níž se vyplňuje pole sekvencí celých čísel. Jsou použity dva typy smyček, přičemž v první smyčce se neustále (v každé iteraci) čte atribut array.length, zatímco ve druhé smyčce je tato hodnota uložena do pomocné lokální konstanty. Každá smyčka proběhne dvakrát, první kolo je zahřívací:

public class ArrayTest1 {
    public static final int ARRAY_SIZE = 20000;
 
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.getProperty("java.version"));
 
        for (int i=0; i<2; i++) {
            long t1, t2;
 
            System.gc();
            Thread.sleep(1000);
            t1 = System.nanoTime();
            test1();
            t2 = System.nanoTime();
            System.out.println("Method #1: " + (t2 - t1) + " ns");
 
            System.gc();
            Thread.sleep(1000);
            t1 = System.nanoTime();
            test2();
            t2 = System.nanoTime();
            System.out.println("Method #2: " + (t2 - t1) + " ns");
        }

    }
 
    private static void test1() {
        int[] array = new int[ARRAY_SIZE];
        for (int i = 0; i < ARRAY_SIZE; i++) {
            for (int j = 0; j < array.length; j++) {
                array[j] = j;
            }
        }
    }
 
    private static void test2() {
        int[] array = new int[ARRAY_SIZE];
        final int length = array.length;
        for (int i = 0; i < ARRAY_SIZE; i++) {
            for (int j = 0; j < length; j++) {
                array[j] = j;
            }
        }
    }

Jak dopadnou časy běhu obou typů smyček lze snadno zjistit:

java -Xint ArrayTest1
1.7.0_25
Method #1: 21546434533 ns
Method #2: 20551668997 ns
Method #1: 21568800224 ns
Method #2: 20092864038 ns
 
java -client ArrayTest1
1.7.0_25
Method #1: 2066722727 ns
Method #2: 1711408014 ns
Method #1: 2065038433 ns
Method #2: 1719504295 ns
 
java -server ArrayTest1
1.7.0_25
Method #1: 1401953601 ns
Method #2: 1361821964 ns
Method #1: 1393230703 ns
Method #2: 1360679360 ns

Nejzajímavější jsou rozdíly v časech běhu obou smyček při použití JIT překladače typu client, kdy se časy liší o zhruba 16%, takže uvedená „optimalizace“ má kupodivu smysl. K tomu je nutno brát v úvahu již zmíněný fakt, že novější JRE 7 jsou společností Oracle na 32bitové systémy dodávány jen s JIT překladačem typu client, takže se spoléhat na to, že JIT server vše přeloží stejně (viz též třetí běh) nemusí být vždy na místě.

Proč je však JIT typu client pomalejší v prvním případě? Je tomu tak z toho důvodu, že „šablona“ instrukce arraylength (volaná před každou iterací!) obsahuje i kontrolu na to, zda není předaný objekt roven null. JIT typu server již dopředu ví (dozví se to z interpretru), že objekt není roven null a může tuto kontrolu zcela vynechat, rozbalit smyčku, rozdělit ji na tři části (viz též další díl) atd.

8. Ladění parametrů JVM (aneb JIT ani GC nejsou samospasitelné)

Při snaze o optimalizaci javovských aplikací je vhodné mít neustále na paměti, že i když JIT překladače a správci paměti (GC), které najdeme v dostupných virtuálních strojích Javy, patří mezi ty nejlepší technologie, které jsou v současnosti dostupné, nejsou a ani nemohou být samospasitelné. To je sice pochopitelné, poněkud hůře se však akceptuje fakt, že současné JIT a GC nedokážou dobře automaticky měnit své chování v závislosti na vlastnostech běžící aplikace. Asi nejvíce je tento problém palčivý při nastavování haldy a správců paměti, kdy je mnohdy nutné nejenom zvolit konkrétního správce paměti, ale i nastavit, kolik vláken má tento správce paměti využít (to se navíc odlišuje při takzvaném FullGC a rychlém běhu), v jakém poměru má být halda rozdělena na části youngtenured, jak má být část young rozdělena na další bloky (eden, survivor space1, survivor space2) atd. Podobné nastavování nás čeká při optimalizacích běhu JIT překladače.

CS24_early

Aby si vážený čtenář sám udělal přehled, kolika volbami lze vlastně JVM ovlivnit, stačí napsat příkaz:

java -XX:+PrintFlagsFinal -version

V případě JDK 1.7.0_25 se vypíše více než 600 parametrů!

[Global flags]
    uintx AdaptivePermSizeWeight                    = 20              {product}
    uintx AdaptiveSizeDecrementScaleFactor          = 4               {product}
    uintx AdaptiveSizeMajorGCDecayTimeScale         = 10              {product}
    uintx AdaptiveSizePausePolicy                   = 0               {product}
    uintx AdaptiveSizePolicyCollectionCostMargin    = 50              {product}
    uintx AdaptiveSizePolicyInitializingSteps       = 20              {product}
    uintx AdaptiveSizePolicyOutputInterval          = 0               {product}
    uintx AdaptiveSizePolicyWeight                  = 10              {product}
    uintx AdaptiveSizeThroughPutPolicy              = 0               {product}
    uintx AdaptiveTimeWeight                        = 25              {product}
     bool AdjustConcurrency                         = false           {product}
     bool AggressiveOpts                            = false           {product}
...
...
...
    uintx YoungGenerationSizeIncrement              = 20              {product}
    uintx YoungGenerationSizeSupplement             = 80              {product}
    uintx YoungGenerationSizeSupplementDecay        = 8               {product}
    uintx YoungPLABSize                             = 4096            {product}
     bool ZeroTLAB                                  = false           {product}
     intx hashCode                                  = 0               {product

9. Odkazy na Internetu

  1. Open Source ByteCode Libraries in Java
    http://java-source.net/open-source/bytecode-libraries
  2. ASM Home page
    http://asm.ow2.org/
  3. Seznam nástrojů využívajících projekt ASM
    http://asm.ow2.org/users.html
  4. ObjectWeb ASM (Wikipedia)
    http://en.wikipedia.org/wi­ki/ObjectWeb_ASM
  5. Java Bytecode BCEL vs ASM
    http://james.onegoodcooki­e.com/2005/10/26/java-bytecode-bcel-vs-asm/
  6. BCEL Home page
    http://commons.apache.org/bcel/
  7. Byte Code Engineering Library (před verzí 5.0)
    http://bcel.sourceforge.net/
  8. Byte Code Engineering Library (verze >= 5.0)
    http://commons.apache.org/pro­per/commons-bcel/
  9. BCEL Manual
    http://commons.apache.org/bcel/ma­nual.html
  10. Byte Code Engineering Library (Wikipedia)
    http://en.wikipedia.org/wiki/BCEL
  11. BCEL Tutorial
    http://www.smfsupport.com/sup­port/java/bcel-tutorial!/
  12. Bytecode Engineering
    http://book.chinaunix.net/spe­cial/ebook/Core_Java2_Volu­me2AF/0131118269/ch13lev1sec6­.html
  13. Bytecode Outline plugin for Eclipse (screenshoty + info)
    http://asm.ow2.org/eclipse/index.html
  14. Javassist
    http://www.jboss.org/javassist/
  15. Byteman
    http://www.jboss.org/byteman
  16. Java programming dynamics, Part 7: Bytecode engineering with BCEL
    http://www.ibm.com/develo­perworks/java/library/j-dyn0414/
  17. The JavaTM Virtual Machine Specification, Second Edition
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/VMSpec­TOC.doc.html
  18. The class File Format
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/Clas­sFile.doc.html
  19. javap – The Java Class File Disassembler
    http://docs.oracle.com/ja­vase/1.4.2/docs/tooldocs/win­dows/javap.html
  20. javap-java-1.6.0-openjdk(1) – Linux man page
    http://linux.die.net/man/1/javap-java-1.6.0-openjdk
  21. Using javap
    http://www.idevelopment.in­fo/data/Programming/java/mis­cellaneous_java/Using_javap­.html
  22. Examine class files with the javap command
    http://www.techrepublic.com/ar­ticle/examine-class-files-with-the-javap-command/5815354
  23. aspectj (Eclipse)
    http://www.eclipse.org/aspectj/
  24. Aspect-oriented programming (Wikipedia)
    http://en.wikipedia.org/wi­ki/Aspect_oriented_program­ming
  25. AspectJ (Wikipedia)
    http://en.wikipedia.org/wiki/AspectJ
  26. EMMA: a free Java code coverage tool
    http://emma.sourceforge.net/
  27. Cobertura
    http://cobertura.sourceforge.net/
  28. jclasslib bytecode viewer
    http://www.ej-technologies.com/products/jclas­slib/overview.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.