Sipos Róbert (2014)
Az összeállításhoz használt oldalak:
Java SE 7 specifikáció
Java bytecode fundamentals
Bytecode
Bytecode basics
Java memory model
Heap structure in JVM
The Java memory architectrue
JVM
Java 7 design flaw
Make Java fast

Bevezetés

A Java a cikk írásának idején a statisztikák szerint a második legelterjedtebb és legnépszerűbb programozási nyelv (első a C). Elterjedtségének okai számosak, ezek egyike a nyelv és a futtatókörnyezet felépítése: a Java nyelven írt programok fordításának eredménye egy platformfüggetlen bináris adattömb, amit bájtkódnak nevezünk. A bájtkód maga a program egyfajta köztes állapotban. Hasonlóan, ahogy a C programokból a C fordító assembly kódot generál, a Java programokból a Java fordító bájtkódot állít elő. Minden Java osztályt ezután egy .class fájl reprezentál, ami a bájtkód utasításokat és az osztállyal kapcsolatos adatokat tartalmazza. Ezek a fájlok aztán dinamikusan töltődnek be a futtatókörnyezetbe (ezt a műveletet az ún. osztálybetöltő - class loader - végzi) és ott végrehajtódnak függetlenül attól, milyen operációs rendszer is van a környezet alatt.

Ez a cikk azzal foglalkozik, hogyan is néz ki a bájtkód és rövid leírást ad arról is, hogyan történik a Java programok futtatása. A megértéshez szükség van a Java nyelv valamint az alapvető, programozással kapcsolatos fogalmak ismeretére. Habár vannak eszközök a Java programok közvetlen assembly formába való fordítására is, ezek jelentősége pillanatnyilag elhanyagolható, ezért ez a cikk sem foglalkozik vele. A nyelvhez nagyon sok nyílt forrású vagy kereskedelmi eszköz és futtatókörnyezet elérhető, de a cikk csak az Oracle (korábban Sun) megoldásait tárgyalja, mert jelenleg messze ezek a legelterjedtebbek. Minden, az összeállításhoz használt cikkből átvett példakód esetén változatlanul hagytam az eredeti angol elnevezéseket.

Az itt közölt információk a Java 7-es verziójára vonatkoznak, habár a témával kapcsolatos szerteágazó irodalom összehangolása következtében előfordulhatnak pontatlanságok.

A Java Virtuális Gép

Az ábra bemutatja egy Java osztály fordítását és futtatását: a forrásfájl (HelloWorld.java) .class fájllá fordítódik (HelloWorld.class), ami aztán betöltődik a virtuális gépbe, ahol végrehajtódik.

kep1

Java osztályok fordítása és végrehajtása

A Java eleinte interpreteres programnyelv volt, ahol a JVM (Java Virtuális Gép) a bájtkódot közvetlenül, utasításonként hajtotta végre. A virtuális gép elnevezés találó, hiszen ahogy egy valódi hardver a gépi kódot, a virtuális gép a bájtkódot hajtja végre szekvenciálisan. A fordítók fejlődésével azonban már 1997 elején elérhető lett egy JIT (Just In Time) futtatókörnyezet a nyelvhez a Symantec jóvoltából, akkor még csak Windows platformra. A bájtkódot a JIT környezet lefordítja gépi kódra, de csak akkor, amikor az adott programrészt ténylegesen futtatni kell. A Sun az 1998 végén megjelenő 1.2-es verzióba ezt már licencelte és alapértelmezetten beépítette. A 2000 májusában megjelenő 1.3-as Java már egy új, HotSpot nevű futtatókörnyezetet tartalmazott, ami megpróbálta egyesíteni az interpretált megközelítés és a JIT előnyeit. A Sun (és ma már az Oracle) által kiadott minden további Java verzió a HotSpot továbbfejlesztett változatait használja. Mivel a kód logikailag továbbra is virtuális gépen fut, minden leírás ezt a kifejezést használja.

Megjegyzendő, hogy a Java szónak valójában kétféle jelentése van: egyrészről a programozási nyelvet jelenti, másrészről a Java virtuális gépet, amire viszont nem feltétlenül csak Javában lehet programozni, hanem egyéb nyelvekben is, amelyekhez létezik bájtkód fordító. Amikor pedig Java virtuális gépről beszélünk, valójában három dolgot is érthetünk alatta:

A cikkben a szövegkörnyezetből mindig kiderül, hogy melyikről is van szó.

Az egyik ok, amiért a JVM ilyen népszerű lett és ami miatt annyi különböző nyelv, fejlesztőkörnyezet, profiler, debugger és egyéb nagyszerű eszköz létrejött az, hogy a Java alapvetően interpretált nyelvnek készült és a JVM bájtkód eléggé egyszerű. Bárkit meg lehet tanítani a teljes tananyagra egy óra alatt, ezután pedig bárki képes használni egy ASM keretrendszert és módosítgatni a bájtkódot, mert az összes fordítóprogramokkal kapcsolatos fekete mágia a JVM HotSpot fordítójába került a javac helyett, ez pedig a futtatás során már pontos információkkal rendelkezik a használt hardver architektúráról, elérhető erőforrásokról és egyéb futtatási információkról, így sokkal hatékonyabb optimalizálást képes végezni, mint egy általános fordítóprogram).

Sok kutatás foglalkozik a nyelv vagy a fordított programok futásidejű viselkedésének fejlesztésével. Bárki írhat plugint a rendszer osztálybetöltőjéhez, ami azért felelős, hogy a .class fájlokat futásidőben betöltse és átadja a bájtkódot a virtuális gépnek. A módosított osztálybetöltők használhatók arra, hogy elfogják a betöltési folyamatot és módosítsanak az osztályokon (viszonylag könnyen, hiszen a bájtkód egyszerű), mielőtt azok átadódnak végrehajtásra a JVM-nek. Mivel az eredeti class fájl mindig változatlan marad, az osztálybetöltő viselkedése akár minden végrehajtás előtt újrakonfigurálható vagy pedig dinamikusan változtatható.

A Java Virtuális Gép architektúrája

A bájtkód tárgyalásához előbb tisztázni kell a JVM absztrakt felépítését; a környezetet, ahol a kód futni fog.

Amikor egy Java alkalmazás elindul (a java HelloWorld-től egészen egy asadmin start-domain --verbose-ig - ami egy Glassfish alkalmazásszervert indít el), egyetlen futásidejű virtuális gép jön létre. Amikor az alkalmazás futása befejeződik, a futó JVM életének is vége szakad. Ha három Java alkalmazást is elindítunk ugyanazon a gépen ugyanabban az időben ugyanazt a konkrét implementációt használva, akkor három JVM példányt kapunk, vagyis minden Java alkalmazás saját külön JVM-ben fut.

Amint már említettem, minden JVM-nek van egy ún. osztálybetöltő alrendszere (class loader), ami futásidőben betölti a .class fájlokban tárolt típusokat (osztályok és interfészek). A JVM-en belül különböző memóriaterületek vannak:

A metódus terület és a heap megosztott használatban van minden, a JVM-ben futó programszál között.

A metódus terület

A JVM-en belül a betöltött típusokhoz tartozó inforomáció az ún. metódus területen tárolódik. Amikor az osztálybetöltő betölt egy .class fájt, a tartalmát átadja a virtuális gépnek, amely feldolgozza és eltárolja azt a metódus területen. Az osztályban deklarált statikus változók is ide kerülnek. Mivel minden szál megosztva használja ezt a területet, ezért az itt lévő adatszerkezetek kezelését a programozónak kell szálbiztosra tervezni.

A konstanskészlet

A konstanskészlet a metódus területen foglal helyet, de fontossága miatt külön is érdemes tárgyalni. Osztálybetöltéskor a JVM minden betöltött típushoz lefoglal egy konstankészlet memóriaterületet a metódus területen. Ez valójában egy tömb adatszerkezet és a típushoz tartozó sztring, egész vagy lebegőpontos konstansokat, szimbolikus linkeket tárolja. A konstanskészlet elemeit azok indexével lehet elérni.

Vermek, veremkeret

A JVM veremalapú gép, nincsenek regiszterei átmeneti adatok tárolására, a bájtkód utasítások adattárolásra a vermet használják. (Ezt még a Java tervezői döntötték el és az volt az oka, hogy egyszerű maradjon a virtuális gép utasításkészlete és könnyű legyen implementálni olyan architektúrákon is, ahol kevés általános célú regiszter van vagy azok kezelése eltér a megszokottól. Ezen kívül lehetővé teszi a JIT fordító számára a futásidejű optimalizálást is.) A java vermek ún. veremkeretekből (stack frame) állnak. Minden alkalommal, amikor egy metódus meghívódik, egy új veremkeret jön létre, ami a metódus-futtatás állapotát tárolja. Amikor a metódus véget ér (normál befejeződéssel vagy kivétel dobása miatt), a hozzá tartozó veremkeret is megszűnik, az aktuális veremkeret pedig a hívó metódus veremkerete lesz, ahová visszakerül a vezérlés.

A Java vermek kezelése látható a következő ábrán három programszál esetén (az első kettő Java programot futtat, a harmadik natív metódust):

stacks

Egy szálhoz tartozó metódus csak a saját utasításszámlálóját és veremkeretét érheti el, ez pedig biztosítja, hogy ha több szál futtatja ugyanazt a metódust (a metódusok lokális változói külön veremkeretbe kerülnek), nincs szükség külön szinkronizálásra. Az ábrán a vermek lefelé nőnek, minden verem "teteje" az ábra alján van. A jelenleg futó metódusok veremkeretei világosabb szürke háttérrel vannak ábrázolva. Azon metódusok esetén, amelyek Java kódot futtatnak, a programszámláló a következő utasításra mutat. Mivel a harmadik szál natív metódust futtat, programszámlálója (sötétszürkével jelölve) nem definiált.

Egy veremkeretnek három része van:

Lokális változók

A lokális változók tömbben tárolódnak, melynek mérete fordítási időben dől el. Minden utasítás, ami lokális változót akar használni, egy indexet tartalmaz, ami kijelöli a változót ebben a tömbben (az indexek 0-val kezdődnek). Egy int, float, referencia és returnAddress típusú elem egy, long és double típusú elem pedig két bejegyzést foglal el ebben a tömbben. (A returnAddress belsőleg használt primitív típus, amit a JVM a finally blokk megvalósításához használ. A programozó számára nem elérhető. Két bejegyzést elfoglaló érték eléréséhez az első bejegyzés indexét használja az adott opkód.) A byte, short és char típusú értékek int típussá konvertálódnak veremkeretre tárolás előtt. Amikor ezeket az értékeket a JVM visszaírja a heap-re, akkor visszakonvertálódnak eredeti típusúvá. A boolean típusú értékeket a JVM minden belső tárolásnál int típusként kezeli.

A lokális változók tömbje 0-tól 65535-ig indexelődik, tehát elméletileg 65536 lokális változónk lehet metódusonként (példányszintű metódusoknál egyel kevesebb, lásd lejjebb). A hívó metódus veremkerete átlapolódik, vagyis a hívó az argumentumokat az operandus veremre teszi és a meghívott metódus ezeket helyi változóként éri el, a lokális változótömb tartalmazza ugyanis a metódus helyi változóin kívül a paramétereket is. A fordító a paramétereket teszi először a tömbbe abban a sorrendben, ahogyan a metódus leírásában definiáltak. Így például: method

class Methods {
    public static int classMethod(int i, long l, float f,
      double d, Object o, byte b) {
        return 0;
    }
    public int instanceMethod(char c, double d, short s,
      boolean b) {
        return 0;
    }
}

Amint az ábrából is látható, az instanceMethod() első paramétere referencia típusú, habár ilyen paraméter nem látható a kódban. Ez a rejtett this referencia a példánymetódusoknál jelenik meg, amelyek ezt a referenciát használják arra, hogy annak az objektumnak a példányadatait érjék el, amelyiken meghívódtak. Ahogyan az ábrán látható, statikus metódusoknál nincs ilyen paraméter, ezek csak az osztály statikus mezőit érik el. Az objektumok referenciaként adódnak át, tehát a heap-en tárolt objektumokról sosem található másolat a helyi változók között vagy a vermen, csak annak referenciájáról.

A metódus paramétereitől eltérően a lokális változók sorrendje nem kötött, a tömbben a paraméterek után bármilyen sorrendben elhelyezkedhetnek, egy pozíciót pedig akár két különböző változó is használhat, ha hatáskörük nem fedi egymást. A pontos pozíciókat ebben az esetben is a fordító határozza meg.

Akárcsak a többi memóriaterület esetén, a JVM specifikációja nem mondja meg, hogy a Java típusokhoz egy adott architektúrán milyen típusok feleljenek meg, ez mindig az adott JVM implementáció tervezőire van bízva. Ha tehát egy implementáció 64 bites szóhosszt használó processzoron fut, akkor a long vagy double típusokat a lokális változók tömbjében akár egyetlen bejegyzésben is lehet tárolni, ilyenkor a második bejegyzés üres marad (mert a JVM specifikáció csak azt mondja meg, hogy két bejegyzés szükséges a tároláshoz).

Operandus verem

Akárcsak a lokális változók, az operandus verem is tömbként van szervezve és LIFO elven működik, mérete fordítási időben dől el. A bájtkódban - ahogy a neve is mutatja - minden egyes utasítást egy bájt reprezentál, ezt a bájtot opkódnak hívják. Az aload_0 utasítás opkódja például 0x2A. Egyes utasítások esetén az operandus az utasítást követi a bájtkódban vagy pedig a konstanskészletből olvasódik ki, de az operandusok helye legtöbb esetben az operandus verem. Sok utasítás leemel operandus(oka)t a veremről, műveletet végez vele (velük) és az eredményt visszateszi a veremre. Egy összeadás például a következőképp néz ki:

  iload_0  //a 0. indexen lévő helyi változót a veremre helyezi
  iload_1  //az 1. indexen lévő helyi változót a veremre helyezi.
  iadd     //levesz a veremről két egész értéket, összeadja ezeket majd visszateszi az eredményt
  istore_2 // levesz a veremről egy egész értéket és a 2-es indexen lévő helyre teszi

verem1

Az operandus verembe kerülnek a metódusok visszatérési értékei is.

Keret adatok

A lokális változók és az operandus verem mellett a veremkeret speciális adatokat is tárol, amelyek kellenek a metódus végrehajtásához és a kivételkezeléshez. Ezeket nevezzük keret adatoknak.

A JVM sok utasítása hivatkozik a konstanskészlet bejegyzéseire, néhány utasítás közvetlenül ebből az adatszerkezetből helyez adatokat a veremre vagy ebből nyeri ki azokat az adatokat, amelyeket példányosításhoz, mező eléréshez vagy metódushíváshoz használ. Ezért a keret adatok tartalmaznak egy mutatót a konstanskészletre.

Az adatok tartalmaznak egyfajta referenciát a metódus kivételtáblájára is, amit a JVM arra használ, hogy feldolgozza a futás során dobott kivételeket (bővebben lejjebb) valamint tartalmazhatnak egyéb adatokat is, amelyek a metódushívást vagy a nyomkövetést segítik. Pontos szerkezete implementációfüggő.

kep2
A veremkeret elvi ábrázolása

Memóriaszerkezet áttekintés

Az eddig leírtak érthetőbbé teszik az alábbi JVM memóriaszerkezeti ábrát. Az ábrán főként az eredeti angol elnevezéseket használom, azonban a részletezésben ezeknek megadom a magyar megfelelőit is.

memory

Amint látható, minden fentebb tárgyalt terület több kisebb részre oszlik. Az alkalmazás memóriahasználata szempontjából a legfontosabb a heap terület és a metódus terület. A több részre való osztás oka főként a szemétgyűjtő működése, az alábbi részletezés erre is kitér.

Egy konkrét osztályt használva a memóriaszervezés a következőképpen néz ki:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.Logger;

public class HelloWorld {
 private static Logger LOGGER=Logger.getLogger(HelloWorld.class.getName());
 public void sayHello(String message){
  SimpleDateFormat formatter=new SimpleDateFormat("YYYY.MM.dd");
  String today=formatter.format(new Date());
  LOGGER.info(today+": "+message);
}

A memóriabeli tartalom:

pelda

OutOfMemoryError: de akkor melyik is?

Némi rálátással a JVM memóriakezelésére már könnyebben megérthető, mi is okozza az időnként felmerülő OutOfMemoryError kivételeket.

És még egy érdekesség: a JVM vermén előforduló memóriafoglalási hiba (ha túl sok veremkeret van már a JVM veremben) nem OutOfMemoryError kivételt dob, hanem java.lang.StackOverflowError-t.

Java .class fájlformátum

classfile

Az ábra bemutatja egy Java .class fájl egyszerűsített felépítését. A fájl a fejléccel kezdődik, ami az ún. mágikus számot (0xCAFEBABE) és egy verziószámot tartalmaz. Ezt követi a konstanskészlet, az osztály elérési jogai bitmaszkos reprezentációban, az általa implementált interfészek listája, az osztály által tartalmazott mezők és metódusok, végül pedig az attribútumok, mint például a SourceFile attribútum, ami a forrásfájl helyét adja meg. Az attribútumok lehetőséget adnak arra, hogy kiegészítő, felhasználó által definiált információt adjunk a .class fájl adatstruktúrájához. Egy saját osztálybetöltő például kiértékelhet egy ilyen attribútumot, hogy bizonyos átalakításokat végezzen a bájtkódon. A JVM specifikációja azt mondja, hogy minden virtuális gép implementációnak figyelmen kívül kell hagynia az ismeretlen (például a felhasználó által definiált) attribútmokat.

Mivel az összes olyan információ, ami ahhoz szükséges, hogy futásidőben feloldjuk az osztályreferenciákat, mezőket és metódusokat sztring konstansokban van elmentve, a konstans pool valójában egy átlagos .class fájl nagyjából 60%-át elfoglalja. Maguk a bájtkód utasítások átlagosan csak 12%-ot foglalnak el.

Az ábra jobb felső doboza a konstanskészlet egy változatát mutatja, az alatta lévő lekerekített doboz pedig a példaosztály egyik metódusának utasításait tartalmazza. Ez a metódus egy szokványos műveletet végez el:

System.out.println("Hello, world");

Az első utasítás betölti a java.lang.System osztály out mezőjének tartalmát az operandusverembe. Ez a java.io.PrintStream osztály egy példánya. Az "ldc" (konstans betöltése) a "Hello world" sztring referenciáját helyezi a veremre. A következő utasítás meghívja a println példánymetódust, ami mindkét értéket paraméterben kapja (a példánymetódusok mindig implicit módon a példány referenciát kapják nulladik paraméterként).

A class fájlban lévő utasítások, egyéb adatstruktúrák és maguk a konstansok is hivatkozhatnak más elemekre a konstanskészletben. Ilyen referenciák fix indexeket használnak, amik közvetlenül az utasításokba vannak kódolva. Ezek az ábrán bekeretezve láthatóak.

Az invokevirtual utasítás például egy CONSTANT_MethodRef_info (röviden MethodRef) konstanskészlet-bejegyzésre hivatkozik, ami a hívott metódus nevéről tartalmaz információkat, annak az aláírásáról (argumentumok, visszatérési érték típusa) és hogy melyik osztályhoz tartozik. Valójában, ahogy a bekeretezett érték mutatja, a MethodRef konstans csak egyéb bejegyzésekre hivatkozik, amelyek már a valódi adatokat tartalmazzák. Hivatkozik például a ConstantClass bejegyzésre, ami szimbolikus linket tartalmaz a java.io.PrintStream osztályra. Hogy a class fájl mérete kicsi maradjon, ilyen konstansok általában meg vannak osztva különböző utasítások és egyéb konstanskészletbeli bejegyzések között. Ehhez hasonlóan egy mezőt egy Fieldref konstans reprezentál, ami adatokat tartalmaz a nevéről, típusáról és az azt tartalmazó osztályról.

A konstanskészlet alapvetően a következő típusú bejegyzéseket tartalmazza: referenciák metódusokra, mezők és osztályok, sztringek, integer-ek, float-ok, long-ok és double-k.

Fájlformátum

Az alábbi táblázat részletesen bemutatja a .class fájl felépítését. Az első oszlopban található az adott rész bájtokban adott mérete, a másodikban a Java specifikációjában használt kódnév, a harmadikban pedig rövid leírás.

Méret Kód Leírás
4 magic 0xCAFEBABE
2 minor_version A fájlformátum alverziója
2 major_version A fájlformátum főverziója. J2SE 8 esetén 52 (0x34), J2SE 7 esetén 51 (0x33), J2SE 6 esetén 50 (0x32).
2 constant_pool_count A konstanskészletbeli bejegyzések száma
cp_info constant_pool[constant_pool_count-1] Konstanskészlet bejegyzések. Indexelésük 1-től indul, mert a 0-s index speciális célokra van fenntartva.
2 access_flags A fájlban tárolt osztály vagy interface elérési jogainak bitmaszkja. Azt is megmutatja, hogy a fájlban osztály, interface vagy enum tárolódik-e.
2 this_class Konstanskészletbeli index, ami egy CONSTANT_Class_info szerkezetet tárol.
2 super_class Értéke 0 vagy egy konstanskészletbeli index. Ha nem 0, akkor értéke egy, a közvetlen ősosztályt reprezentáló CONSTANT_Class_info szerkezetre mutat. Egyetlen ősosztálynak sem lehet beállítva az ACC_FINAL flag-je az access_flags mezőben. Ha értéke 0, akkor ez a .class fájl az Object osztályt tartalmazza, aminek nincs ősosztálya. Interfészek esetén mindig egy konstanskészletbeli index, ami az Object-et reprezentáló CONSTANT_Class_info struktúrára mutat.
2 interfaces_count Az osztály vagy interfész által implementált közvetlen interfészek listája.
2*interfaces_count interfaces[interfaces_count] Minden egyes elem konstanskészletbeli index, ami egy CONSTANT_Class_info szerkezetre mutat.
2 fields_count A field_info szerkezet bejegyzéseinek száma.
field_info fields[fields_count] Minden eleme leírja az osztály vagy interfész egy mezőjének tulajdonságait. Mind az osztályszintű, mind a példányszintű mezőket tartalmazza (kivéve az öröklötteket).
2 methods_count A method_info szerkezet bejegyzéseinek száma.
method_info methods[methods_count] Minden bejegyzése az osztály által megvalósított egy metódusról tartalmaz információkat (az öröklött metódusokról nem). Amennyiben a metódushoz tartozó elérési jogosultságok között nincs bekapcsolva az ACC_NATIVE vagy az ACC_ABSTRACT flag, akkor a metódus bájtkódját is ez tartalmazza.
2 attributes_count Az attributes listában szereplő bejegyzések száma.
attribute_info attributes[attributes_count] Az osztályhoz tartozó kiegészítő attribútumok, mint például a belső osztályok, a forrásfájl helye, annotációk, stb.

Metódus kód

Minden konstanskészletbeli bejegyzés egy egybájtos címkéből és az azt követő bájttömből áll. A címke határozza meg a bájttömb értelmezését. Néhány típus:

A .class fájl teljes specifikációja az Oracle dokumentációjában megtalálható, jelen cikk csak a method_info struktúra részletezésére tér ki. Ez látható a következő táblázatban.

Méret Kód Leírás
2 access_flags A metódushoz tartozó jogosultság bitmaszkja.
2 name_index Konstanskészletbeli index, egy CONSTANT_Utf8_info struktúrára mutat, ami a metódus nevét tartalmazza. Konstruktorok esetén ez a név <init>, statikus konstruktorok esetén <clinit>, egyéb esetben pedig az, amit az osztály fejlesztője megadott. A konstruktorok fix elnevezése nem okoz problémát, mert ilyen nevet a Java programkódban nem lehet megadni.
2 descriptor_index Konstanskészletbeli index, ami a metódus deszkriptorára mutat. Ez tartalmazza a metódus paramétereinek és visszatérési értékének leírását.
2 attributes_count Az attributes listában szereplő bejegyzések száma.
attribute_info attributes[attributes_count] A metódushoz tartozó attribútumok.

Az attributes lista szerkezete a következő:

Méret Kód Leírás
2 attribute_name_index Konstanskészletbeli index, ami egy CONSTANT_Utf8_info szerkezetre mutat. Az adott attribútum típusát jelöli. Nem absztrakt (és nem natív) metódusok esetén ez "Code".
4 attribute_length Az info szerkezet mérete bájtokban
1 info[attribute_length] Az attribútum információja. "Code" attribútum esetén a metódus veremkeretének maximális mérete, a lokális változók száma és a bájtkód utasítások tömbje. A lokális változók neveiről valamint a forrásfájlbeli sorszámokról is tartalmazhat információt, ezeket a debuggerek használják, de ezek megléte opcionális. Ha a forrást -g:none kapcsolóval fordítjuk, akkor ezek az információk nem kerülnek a .class fájlba.

Példakód

Ez az alfejezet egy .class fájl vizsgálatát tartalmazza a fenti leírás alapján. A példaosztály a lehető legegyszerűbb:

public class HelloWorld {
  public void process() {
    System.out.println("Hello World!");
  }
}

Miután ezt javac -g:none paranccsal lefordítjuk, a következő 319 bájtos fájl jön létre (hexa editor nézetben megtekintve, a számjegyként és betűként értelmezhetetlen karakterek helyén _ áll):

helloclass

A fájl tartalmának elemzése a fenti információk alapján (bővebb elemzése szöveges fájlban letölthető innen):

Pozíció Méret Leírás
0 4 0xCAFEBABE - a "mágikus szám"
4 2 az alverzió értéke: 0
6 2 a főverzió értéke: 0x33, vagyis J2SE7 fordítóval készült class fájlról van szó
8 2 a konstanskészletbeli bejegyzések száma+1: 0x19 vagyis 25
10 231 a konstanskészletbeli bejegyzések
10 5 1. konstansbejegyzés. A 10. bájtpozíción 0xA vagyis 10 áll, ami megmutatja, hogy ez egy CONSTANT_Methodref_info bejegyzés
15 5 2. konstansbejegyzés. A 15. bájtpozíción 9 áll, ami megmutatja, hogy ez egy CONSTANT_Fieldref_info bejegyzés
20 3 3. konstansbejegyzés. A 20. bájtpozíción 8 áll, ami megmutatja, hogy ez egy CONSTANT_String_info bejegyzés
23 5 4. konstansbejegyzés: CONSTANT_Methodref_info
28 3 5. konstansbejegyzés. A 28. bájtpozíción 7 áll, ami megmutatja, hogy ez egy CONSTANT_Class_info bejegyzés
31 3 6. konstansbejegyzés: CONSTANT_Class_info
34 32 7.-10. konstansbejegyzések. Mindegyik CONSTANT_Utf8_info típusú. Értékeik: "<init>", "()V", "Code", "process"
66 5 11. konstansbejegyzés. A 66. bájtpozíción 0xC vagyis 12 áll, ami megmutatja, hogy ez egy CONSTANT_NameAndType_info bejegyzés
71 31 12.-16. konstansbejegyzések.
102 137 17.-24. konstansbejegyzések. Mindegyik CONSTANT_Utf8_info típusú. Értékeik: "HelloWorld", "java/lang/Object", "java/lang/System", "out", "Ljava/io/PrintStream;", "java/io/PrintStream", "println", "(Ljava/lang/String;)V"
239 2 Jogosultsági flagek.
241 2 Konstanskészlet-beli index a this osztályhoz: 5. Ezen az indexen egy CONSTANT_Class_info bejegyzés áll, aminek egyetlen értéke van: egy index az osztály nevére. Ennek értéke 0x11, vagyis a 17. konstansra mutat, ami pedig a "HelloWorld" karaktersorozat (a 105. pozíción kezdődik).
243 2 Konstanskészletbeli index az ősosztályhoz: 6. Ezen az indexen egy CONSTANT_Class_info bejegyzés áll, aminek egyetlen értéke van: egy index az osztály nevére. Ennek értéke 0x12, vagyis a 18. konstansra mutat, ami pedig a "java/lang/Object" karaktersorozat, vagyis az osztály az Object osztályból származik.
245 2 Az osztály által implementált interface-ek száma. Ez esetben 0.
247 2 Az osztály mezőinek száma. Ez esetben 0.
249 2 Az osztály metódusainak száma. Ez esetben 2 (konstruktor és a process).
251 66 A metódusok tartalma.
251 2 A metódus elérési jogosultságainak bitmaszkja.
253 2 A metódus nevének konstans-indexe: 7. Ez pedig az <init>, vagyis az alapértelmezett konstruktor.
255 2 Metódusleíró konstanskészletbeli indexe. Itt 8, ami a következő sztringet tartalmazza: "()V". Ez azt jelenti, hogy a metódusnak nincsenek paraméterei és "V", vagyis void a visszatérési értéke.
257 2 Attribútumok száma: 1
259 2 Az első attribútum neve: index a konstanskészletben: 9, vagyis "Code". Ez mutatja, hogy az attribútum a bájtkódot tartalmazza (néhány, a JVM számára szükséges kiegészítő információval együtt).
261 4 Az attribútum hossza: 0x11, vagyis 17 bájt.
265 17 Az attribútum tartalma: ez maga a bájtkód.
282 2 A második metódus elérési jogosultságai.
284 33 A második metódus leírója és bájtkódja, hasonlóan az elsőhöz, itt már nem részletezve.
317 2 A fájl attribútumai, ez esetben 0

Amint látható, a .class fájlban a bájtkódot kivéve a legtöbb információ szöveges formában tárolódik, amihez egy vagy több szintű konstanskészlet-indexeken keresztül jutunk el. A Java erősen típusos nyelv és a mezőkhöz, lokális változókhoz és metódus paraméterekhez kapcsolódó típusokról szóló információt ún. aláírásokban tárolják. Ezek szintén a konstanskészletben tárolt speciális formátumú sztringek. Az általános main metódus paramétere és visszatérési érték típusa például:

public static void main(String[] argv)

a következő aláírással van jelölve:

([java/lang/String;)V

Az osztályokat belsőleg sztringek reprezentálják, mint például a "java/lang/String", a primitív típusokat pedig, mint például a float egy egész szám. Aláírásokon belül ezeket egy karakter jelenti, például az I az egészet. A tömbök jelzésére [ használatos az aláírás kezdetén.

Opkódok - a bájtkód utasításkészlet

A bájtkód utasításkészlet jelenleg 212 utasítást tartalmaz, 44 opkód fenntartottként van jelölve jövőbeli felhasználásra vagy a virtuális gép belső optimalizációihoz. Az utasításkészletet nagyjából a következő csoportokra lehet osztani:

Veremműveletek: konstansok veremre helyezése vagy azok betöltése a konstanskészletből az ldc utasítással vagy speciális rövidített utasításokkal, ahol az operandus magába az utasításba van kódolva, például iconst_0 vagy bipush (bájt érték veremre helyezése).

Aritmetikai operátorok: a JVM utasításkészlete külön csoportokba osztja az aritmetikai utasításokat aszerint, hogy azok milyen típusú operandusokon végeznek műveleteket. Az i-vel kezdődő aritmetikai operátorok integer operátorokat jelölnek. Az iadd például két integert ad össze, az eredményt pedig visszahelyezi a veremre. A Java típusok boolean, byte, short és char a JVM-ben mind integerként kezelődnek.

Folyamatvezérlés: vannak vezérlésátadó utasítások, mint a goto és az if_icmpeq, ami két egész közötti egyenlőséget vizsgál. Aztán van a jsr (szubrutinra ugrás) és a ret utasítás, amelyeket általában arra használnak, hogy a try-catch blokkok finally ágát implementálják. Kivételeket az athrow utasítással lehet dobni. Az ugrási pozíciókat az aktuális bájtkód pozíciótól számítot eltolással (offset - például egész szám) lehet megadni.

Betöltés és tárolás: helyi változók esetén iload és istore. Vannak aztán tömbműveletek is, mint például az iastore, amivel egy egész értéket egy tömbbe lehet tárolni.

Mező elérés: egy példánymező értékét a getfield utasítással lehet kiolvasni, a putfield utasítással írni. Statikus mezőknél ugyanez getstatic és putstatic.

Metódushívás: a statikus metódusok meghívhatók az invokestatic utasítással vagy virtuális kötéssel az invokevirtual utasítással. Ősosztály és privát metódusok hívására szolgál az invokespecial. Interfész metódusok hívására pedig az invokeinterface utasítás szolgál.

Objektum foglalás: példányokat lehet létrehozni a new utasítással, primitív típusokból álló tömbök, mint például int[] létrehozhatóak a newarray utasítással, referenciákat tartalmazó tömb, mint például String[][] az anewarray vagy a multianewarray utasítással.

Konverzió és típus ellenőrzés: primitív típusú verem operandusok esetén léteznek konverziós utasítások, mint az f2i, ami lebegőpontos értéket egésszé alakít. Típuskényszerítés validálása történhet a checkcast utasítással, az instanceof utasítás pedig megfelel a Java instanceof utasításának.

A legtöbb utasításnak adott mérete van, de létezik néhány változó hosszúság utasítás is, mint például a lookupswitch és tableswitch, amelyek a switch() kifejezések implementálására használatosak. Mivel a case esetek száma változhat, ezek az utasítások változó számú kifejezést tartalmaznak.

Vezérlésátadó utasítások, mint például a goto a bájtkód tömbjén belüli relatív offszetekkel dolgozik, a kivételkezelők és lokális változók viszont abszolút címeket használnak a bájtkódon belül. Az előbbi referenciákat tárol a try blokk elejére és végére valamint kezelő kódjára. Az utóbbi kijelöli azt a kódszakaszt, vagyis hatókört, ahol a változó elérhető. Ez bonyolulttá teszi kódok beszúrását vagy törlését az absztrakciónak ezen a szintjén, mivel ezeket az offszeteket újra kell számolni minden esetben és frissíteni kell a hivatkozó objektumokat.

Ha tanulmányozzuk a Java bájtkód utasításokat, láthatjuk, hogy néhány utasításnak prefixe van, mint például aload_0 vagy istore_2. Ez az utasítás által kezelt adat típusára vonatkozik. Az "a" prefix azt jelenti, hogy az opkód objektum referenciával dolgozik, az "i" pedig azt, hogy egész értékkel. Léteznek opkódok a következő prefixszel is: "b" bájt esetén, "l" hosszú egész, "c" karakter esetén, "f" egyszeres, "d" kétszeres pontosságú lebegőpontos érték esetén.
Ezen cikknek nem témája a teljes opkód referencia tárgyalása. A teljes lista elérhető itt.

Bájtkód élesben

A memóriaszerkezet és a fájlformátum után most már lássuk, hogyan is néz ki maga a bájtkód valódi programok esetén. A .class fájlok visszafejtésére a Java tartalmaz parancssori eszközt, ez a javap. Paraméter nélkül pusztán csak az osztály vázát jeleníti meg, -c paraméterrel a bájtkódot is. Elsőként a fentebb lefordított HelloWorld osztályt vizsgáljuk meg:

public class HelloWorld {
  public void process() {
    System.out.println("Hello World!");
  }
}
javap -c HelloWorld
public class HelloWorld {
  public HelloWorld();
    Code:
       0: aload_0       
       1: invokespecial #1                // Method java/lang/Object."<init>":()V
       4: return        

  public void process();
    Code:
       0: getstatic     #2                // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                // String Hello World!
       5: invokevirtual #4                // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return        
}

Az osztály nagyon egyszerű, könnyű látni a kapcsolatot a forráskód és a legenerált bájtkód között. Vegyük észre - ahogyan a .class fájl elemzéséből is látható volt -: a fordító bizony legenerálta az alapértelmezett konstruktort (ahogy ez következik is a JVM specifikációjából). Az alapértelmezett konstruktor első opkódja, az aload_0 a helyi változótábla 0. indexen található értékét teszi az operandus verem tetejére. Korábban már említettem, hogy a helyi változótábla arra is használva van, hogy a metódusoknak paramétert adjunk át. A "this" referencia mindig a 0. helyen tárolódik. Ezt példánymetódusok esetén mindig át kell adni, mert a metódus az osztály példányváltozóit és nevét ezen keresztül éri el, tehát egy paraméterek nélküli példánymetódus is kap valójában egy paramétert, a this referenciát. A második opkód utasítás az 1. helyen az invokespecial. Ez meghívja az ősosztály konstruktorát. Korábban említettem, hogy metódus hívásakor a paraméterek a verembe kerülnek, majd a hívás során verem tartalma átlapolódik és a hívott metódusban a helyi változótáblába kerül. Tehát az invokespecial előtt a this referenciát a lokális változótábla 0. pozíciójáról a verembe tesszük (aload_0), ahonnan a hívás során leemelődik és a meghívott metódus lokális változótáblájába kerül. A return visszatér a metódusból. Észrevehetjük azt is, hogy néhány opkódnak fura operandusa van, mint #1 vagy #2. Ez az osztály konstanskészletére hivatkozik. Az invokespecial esetén egy Methodref-re, vagyis metódus információra mutat. Ha ezt végigkövetjük: az első bejegyzés osztályhivatkozása a 0x6, ami egy Class szerkezet. Az ebben lévő hivatkozás pedig (0x12) a java/lang/Object-et mutatja. A Methodref második indexe 0xB, ami egy NameAndType szerkezetet indexel. Ennek első indexe (0x7) a névre hivatkozik: a 7. helyen a konstanskészletben a <init> található, második indexe pedig a típusokról ad információt, itt 0x8 az index, ez pedig a ()V sztringet jelöli ki. Így tehát összeáll a hívott metódus, amint azt az opkód utáni megjegyzés is mutatja: java/lang/Object."<init>":()V

A process metódus első opkódja a System osztály statikus out mezőjét tölti be, az ldc utasítás a 3-as indexű konstanst tölti be a verem tetejére, ami a "Hello World!" sztring. Ez lesz a paramétere a meghívott metódusnak, amit az invokevirtual utasítás hív meg. Ez a 4. konstansra hivatkozik, ami egy Methodref érték, megadja a hívott metódust.
Lássunk most egy egyszerű POJO-t egy mezővel, getterrel és setterrel.

public class Foo {
  private String bar;

  public String getBar(){ 
    return bar; 
  }

  public void setBar(String bar) {
    this.bar = bar;
  }
}

Bájtkódja így néz ki:

public class Foo {
  public Foo();
    Code:
       0: aload_0       
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return        

  public java.lang.String getBar();
    Code:
       0: aload_0       
       1: getfield      #2                  // Field bar:Ljava/lang/String;
       4: areturn       

  public void setBar(java.lang.String);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2                  // Field bar:Ljava/lang/String;
       5: return        
}

A fentebb tárgyalt részletes .class fáj kifejtést a javap összefoglalva meg tudja mutatni. Ehhez futtassuk a következő parancsot: javap -c -s -verbose (a -s kiírja a szignatúrákat, a -verbose kiírja az összes részletet).

Classfile /f:/Programok/java/sample/Foo.class
  Last modified 2014.04.06.; size 291 bytes
  MD5 checksum 1a7709f1dca471e22d2083654069ef6b
public class Foo
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER

Constant pool:
   #1 = Methodref          #4.#14         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#15         //  Foo.bar:Ljava/lang/String;
   #3 = Class              #16            //  Foo
   #4 = Class              #17            //  java/lang/Object
   #5 = Utf8               bar
   #6 = Utf8               Ljava/lang/String;
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               getBar
  #11 = Utf8               ()Ljava/lang/String;
  #12 = Utf8               setBar
  #13 = Utf8               (Ljava/lang/String;)V
  #14 = NameAndType        #7:#8          //  "<init>":()V
  #15 = NameAndType        #5:#6          //  bar:Ljava/lang/String;
  #16 = Utf8               Foo
  #17 = Utf8               java/lang/Object
{
  public Foo();
    Signature: ()V
    flags: ACC_PUBLIC

    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        

  public java.lang.String getBar();
    Signature: ()Ljava/lang/String;
    flags: ACC_PUBLIC

    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: getfield      #2                  // Field bar:Ljava/lang/String;
         4: areturn       

  public void setBar(java.lang.String);
    Signature: (Ljava/lang/String;)V
    flags: ACC_PUBLIC

    Code:
      stack=2, locals=2, args_size=2
         0: aload_0       
         1: aload_1       
         2: putfield      #2                  // Field bar:Ljava/lang/String;
         5: return        
}

A kifejtés első négy sora értelemszerű. A minor version a fájl alverziója, a major version a főverziója. A flags mutatja az osztály elérési jogosultságait. Az ACC_PUBLIC jelenléte mutatja, hogy az osztály publikus elérésű, az ACC_SUPER flag visszamenőleges kompatibilitás miatt szükséges, az összes modern fordító beállítja. Ezután a konstanskészlet összes bejegyzése következik. Ahogy fentebb a .class fájl közvetlen vizsgálatakor látszottak a hivatkozások, itt is látható. A #2 például:

const #2 = Field #3.#18; // Foo.bar:Ljava/lang/String;

hivatkozik a következőkre:

const #3 = class #19; // Foo
const #18 = NameAndType #5:#6;// bar:Ljava/lang/String;

és így tovább. Ezután következnek a metódusok, mindegyiknek jelezve a paraméterlistája és a jogosultságai. A stack mutatja a metódus vermének méretét, ahogy fentebb is olvasható volt, ez fordítási időben meghatározódik, itt pedig látható, hogy a "Code" attribútumon belül, a bájtkóddal együtt tárolódik. A locals mutatja a helyi változók táblájának méretét, az args_size pedig a paraméterlista méretét. A bájtkódon belül minden opkód egy számmal van jelölve (0: aload_0). Ez az utasítás keretben elfoglalt helyére vonatkozik (lásd lejjebb).

public String getBar(){ 
    return bar; 
}

public java.lang.String getBar();
  Code:
     0: aload_0       
     1: getfield      #2                  // Field bar:Ljava/lang/String;
     4: areturn

A felső metódus bájtkódja három opkód utasítást tartalmaz. Az aload_0 már ismert, ráteszi az operandus veremre a lokális változótábla 0. indexén szereplő referenciát (this). A következő opkód utasítás, a getfield arra szolgál, hogy egy objektum egyik mezőjének értékét elérjük. Az utolsó utasítás, az areturn visszatér egy referenciával egy metódusból. A fenti példaosztály .class fájljának bájttömbjében zöld háttér mutatja a tárgyalt opkódoknak megfelelő bájtkódot:

sample2

A getBar metódus bájtkódja 2A B4 00 02 B0. A 0x2A kód felel meg az aload_0 utasításnak, a 0xB0 pedig az areturn utasításnak. Talán meglepő, hogy a metódus 3 opkódból áll, de a bájtkód tömbje 5 elemet tartalmaz. Ennek az a magyarázata, hogy a getfield-nek (0xB4) 2 bájtra van szüksége az indexeléshez (00 és 02) és ezek a paraméterek a 2-es és 3-as pozíción foglalnak helyet a tömbben, így annak mérete 5 bájt és az areturn utasítás eltolódott a 4-es helyre.

A lokális változók tömbje

A lokális változók tömbjének szerepéről egy korábbi fejezetben már volt szó, most megvizsgáljuk konkrét osztályon, hogyan is néz ki.

public class Example {
   public int plus(int a){
     int b = 1;
     return a + b;
   }
}

Ez a metódus 2 lokális változót kezel: egyet a metódus paramétereként kapja, a másik pedig az int típusú "b" lokális változó. Ahhoz, hogy a javap megmutassa a lokális változók tömbjét, a programot -g kapcsolóval kell fordítani, a javap-nek pedig a -l kapcsolót kell megadni. Ezután így néz ki a bájtkód:

public int plus(int);
  Code:
     0: iconst_1      
     1: istore_2      
     2: iload_1       
     3: iload_2       
     4: iadd          
     5: ireturn       
  LineNumberTable:
    line 3: 0
    line 4: 2
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0       6     0  this   LExample;
        0       6     1     a   I
        2       4     2     b   I

A javac alapértelmezettként a nyomkövetéshez szükséges programsor-információkat is belegenerálja a .class fájlba, ez itt is látható. A változótábla mutatja, hogy a helyi változók hogyan helyezkednek el. Elsőként a metódus betölti az 1 konstanst az iconst_1 utasítással, majd az istore_2 utasítással eltárolja azt a helyi változótábla második helyére. A helyi változótáblában láthatjuk is, hogy a kettes hely (slot) fenn van tartva a b nevű változónak. Következőként az iload_1 elhelyezi az a értékét a verem teteján, az iload_2 pedig a b értékét. Az iadd levesz két elemet a veremről, összeadja ezeket aztán az eredményt visszateszi a veremre, amivel a return visszatér a metódusból.

A tábla start oszlopa azt mutatja meg, hogy a bájtkódban az adott változó melyik sortól elérhető, a length pedig azt, hogy hány utasításon keresztül él. A this és az a változó már a metódus 0. utasításánál elérhető, a b pedig a másodiktól. A signature oszlop megmutatja a változó típusát.

Kivételkezelés

Érdemes megvizsgálni a bájtkódot kivételkezelésnél is, például hogy milyen kód generálódik a try-catch-finally blokknak. Amikor a végrehajtás során egy kivétel dobódik, a JVM megvizsgálja a kivételek tábláját. Ez a tábla tartalmazza a kivételkezelőket: azon kódrészek hivatkozását, amelyek felelősek bizonyos, a bájtkód megadott területén fellépő megadott típusú kivételekért. Amikor nincs megfelelő kezelő, a kivétel továbbdobódik a metódus hívójának. A kezelő információ a sorszámokhoz és a helyi változók táblájához hasonlóan a Code attribútumon belül tárolódik.

public class ExceptionExample {

  public void foo(){
    try {
      tryMethod();
    }
    catch (Exception e) {
      catchMethod();
    }finally{
      finallyMethod();
    }
  }

  private void tryMethod() throws Exception{}

  private void catchMethod() {}

  private void finallyMethod(){}

}

Ez fordítódik a foo() metódusból:

public void foo();
  Code:
     0: aload_0       
     1: invokespecial #2                  // Method tryMethod:()V
     4: aload_0       
     5: invokespecial #3                  // Method finallyMethod:()V
     8: goto          30
    11: astore_1      
    12: aload_0       
    13: invokespecial #5                  // Method catchMethod:()V
    16: aload_0       
    17: invokespecial #3                  // Method finallyMethod:()V
    20: goto          30
    23: astore_2      
    24: aload_0       
    25: invokespecial #3                  // Method finallyMethod:()V
    28: aload_2       
    29: athrow        
    30: return        
  Exception table:
     from    to  target type
         0     4    11   Class java/lang/Exception
         0     4    23   any
        11    16    23   any
        23    24    23   any
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        12       4     1     e   Ljava/lang/Exception;
         0      31     0  this   LExceptionExample;

Amint látható, a fordító legenerálja a kódot az összes lehetséges helyzethez, ami a try-catch-finally végrehajtása során történhet: a finallyMethod() hívása 3-szor van megadva! A try blokk úgy készül el, mintha a try nem is létezne; összefűződik a finally-val:

public void foo();
  Code:
     0: aload_0       
     1: invokespecial #2                  // Method tryMethod:()V
     4: aload_0       
     5: invokespecial #3                  // Method finallyMethod:()V
     8: goto          30

Ha a blokk sikeresen lefut, akkor a goto utasítás a 30-as opkódra adja a vezérlést, ami egy return. Ha a tryMethod() egy Exception példányt dob, akkor JVM megvizsgálja a kivételtáblát. Ezt a fordító generálja és a bájtkód tartalmazza. A from és to mezők megadják, hogy az adott type típusú kivétel a kód mely részétől meddig léphet fel, a target megadja, hogy a kivétel esetén mely pozícióra kell ugrani a vezérlésnek. A példában Exception példányú kivétel esetén a kivételtábla első kivételkezelője lesz kiválasztva. A kivételtáblából látható, hogy a megfelelő kivételkezelő pozíciója ez esetben 11:

from   to  target type
   0    4     11   Class java/lang/Exception

Ennek során végrehajtódik a catchMethod() és a finallyMethod():

    11: astore_1      
    12: aload_0       
    13: invokespecial #5                  // Method catchMethod:()V
    16: aload_0       
    17: invokespecial #3                  // Method finallyMethod:()V
    20: goto          30

Az astore_1 eltárolja az "e" nevű változóba, a helyi változók táblájának 1. helyére a verembe került kivétel referenciát, majd meghívódik a catchMethod() és a finallyMethod is. Amennyiben a végrehajtás során bármilyen más kivétel dobódna, a kivételtáblából látható, hogy a tetszőleges típusú kivétel kezeléséhez (amelyek a try-catch-finally szerkezet bármely pontján felléphetnek) szükséges pozíció a 23:

     from    to  target type
         0     4    23   any
        11    16    23   any
        23    24    23   any

Nézzük meg, milyen utasítások állnak a 23-as pozíciótól kezdődően:

    23: astore_2      
    24: aload_0       
    25: invokespecial #3                  // Method finallyMethod:()V
    28: aload_2       
    29: athrow        
    30: return

Vagyis a finallyMethod() ebben az esetben is meghívódik. Az astore_2 a helyi változótömb 2. pozíciójára menti a kivételt, amit a metódushívás után visszatesz az aload_2 a verem tetejére és ezt dobja tovább az athrow. Érdemes még megemlíteni, hogy a Java 7 egyik újítása, a több típusú catch lehetősége a bájtkódon semmiben sem változtatott, vagyis ha a tryMethod()-ot átírjuk mondjuk erre:

private void tryMethod() throws IOException, IllegalArgumentException{}

a catch ágat pedig így:

catch (IOException | IllegalArgumentException e) {

akkor a legenerált bájtkód érdemben nem változik, csak egy újabb sor kerül a kivételzekező táblájába, ami azt mutatja, hogy a második kivétel esetén is a 11-es pozícióban kezdődik a kivételkezelő:

0     4    11   Class java/lang/IllegalArgumentException

Optimalizálás

A javac fordító nem végez összetettebb optimalizálást a kódon. A legelső Java implementáció esetén még volt egy -O kapcsolója, amiről a dokumentáció azt állította, optimalizálni fogja a kódot a nagyobb sebesség érdekében, de a -O használatának Sun/Oracle Java 2 SDK vagy annál újabb környezet estén már nincs hatása a generált kódra. Meghagyásának egyetlen oka, hogy a javac kompatibilis maradjon a régebbi make fájlokkal. A Java bájtkód végrehajtásáért a HotSpot JIT felel, ami szükség esetén lefordítja azt gépi kódra és összetett vizsgálatokat valamint optimalizálásokat képes végezni az adott platform képességeinek figyelmbe vételével. Ezek tárgyalása külön cikket igényelne, ezért itt nem térek ki rá. Néhány alapvető optimalizálást a javac fordító alapértelmezetten is elvégez, mint például a konstans kifejezések kiértékelése vagy a soha nem futó kódrészek kihagyása. Nézzük a következő metódust:

public void sampleMethod() {
    double i = 10l * Math.PI;
    if (true)
        System.out.println(i);
    if (false)
        System.out.println(i * 100);
}

A metódusból az alábbi bájtkód készül:

  public void sampleMethod();
    Code:
       0: ldc2_w        #11                 // double 31.41592653589793d
       3: dstore_1      
       4: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: dload_1       
       8: invokevirtual #19                 // Method java/io/PrintStream.println:(D)V
      11: return        

Mint látható, a fordító kiértékelte az i kezdőértékének konstans kifejezését és az if (true) valamint if (false) kifejezéseket is, ennek megfelelően a második println már nem is került a bájtkódba. A javac emellett azt is észreveszi, ha egy sztring literál többször szerepel egy osztályban, ilyenkor a konstanskészletbe csak egyszer kerül be és minden előfordulás esetén ugyanarra az indexre lesz hivatkozás.

Szinkronizálás

Az JIT által végzett optimalizálás viszont még nem jelenti azt, hogy célszerűtlen lenne hatékony kódot írni, egy jó példa erre a szinkronizáció esete. A következő két metódus visszaadja egy tömbként implementált verem legfelső elemét. Mindkettő használ szinkronizációt és funkcionálisan azonosak:

public synchronized int top1() {
    return intArr[0];
}

public int top2() {
    synchronized (this) {
        return intArr[0];
    }
}

A funkcionális azonosság ellenére különböző méretű és sebességű kód fordítódik belőlük. Ebben az esetben a top1 gyorsabb és kisebb, mint a top2. Vizsgáljuk meg a generált bájtkódot, hogy lássuk miben különböznek a metódusok. A bájtkód minden sorához megjegyzések tartoznak, hogy segítsenek megérteni, mi is történik.

public synchronized int top1();
  Code:
     0: aload_0                       // A this refeerncia veremre helyezése
     1: getfield      #29             // Field intArr:[I
                                      // A this referencia leemelése a veremről és az intArr
                                      // referenciájának elérése a konstanskészletből.
     4: iconst_0                      // A 0 veremre helyezése
     5: iaload                        // A verem tetején lévő két elem leemelése és az intArr
                                      // 0. elemének veremre helyezése
     6: ireturn                       // a verem legfelső elemének a hívó vermének tetejére
                                      // helyezése és visszatérés a metódusból

public int top2();
  Code:
     0: aload_0                       // A this refeerncia veremre helyezése
     1: dup                           // A verem tetején lévő érték még egyszer a verem
                                      // tetejére kerül 
     2: astore_1                      // A verem tetején lévő referencia a változótábla 1.
                                      // helyére kerül
     3: monitorenter                  // A verem tetején lévő referencia (this) leemelése és
                                      // a monitor belépése
     4: aload_0                       // A synchronized blokk kezdete. A this refeerncia
                                      // veremre helyezése
     5: getfield      #29             // Field intArr:[I
                                      // A this referencia leemelése a veremről és az intArr
                                      // referenciájának elérése a konstanskészletből.
     8: iconst_0                      // A 0 veremre helyezése
     9: iaload                        // A verem tetején lévő két elem leemelése és az intArr
                                      // 0. elemének veremre helyezése
    10: aload_1                       // A változótábla 1. elemének a verem tetejére helyezése
    11: monitorexit                   // A verem tetején lévő referencia (this) leemelése
                                      // és kilépés a monitorból
    12: ireturn                       // a verem legfelső elemének a hívó vermének tetejére
                                      // helyezése és visszatérés a metódusból
    13: aload_1                       // A változótábla 1. elemének a verem tetejére helyezése
    14: monitorexit                   // A verem tetején lévő referencia (this) leemelése
                                      // és kilépés a monitorból
    15: athrow                        // A verem tetején lévő referencia (this) leemelése
                                      // és a kivétel dobása
  Exception table:
     from    to  target type
         4    12    13   any
        13    15    13   any

A top2 nagyobb és lassabb, mint a top1 a szinkronizálás és a kivételkezelés eltérő megvalósítása miatt. A top1 a synchronized metódus módosítót használja, ami nem generál extra kódot. A top2 a synchronized funkciót a metódus törzsében használja. Ez pedig a monitorenter és monitorexit opkódok, valamint kivételkezelő kód generálását eredményezi. Ha a szinkronizált blokk (egy monitor) futtatása során kivétel jön létre, a zárolás garantáltan megszűnik a szinkronizált blokkból való kilépés előtt. A top1 kissé hatékonyabb, mint a top2. Amikor - mint a top1 esetén - a synchronized metódus módosítót használjuk, a zárolás és annak elengedése nem a monitorenter és monitorexit opkódokkal történik, hanem amikor a JVM meghívja a metódust, ellenőrzi a method_info struktúra access_flags bitmaszkban az ACC_SYNCHRONIZED beállítását. Ha ez meg van adva, akkor a futtató szál zárolódik, meghívja a metódust, aztán megszünteti a zárolást, amikor a metódus véget ér. Ha a futtatás során kivétel dobódik, akkor a zárolás automatikusan megszűnik, mielőtt a kivétel elhagyja a metódust. A különböző használatnak méretbeli hatásai is vannak. Szinkronizált metódusokat csak akkor érdemes használni, amikor a kódnak valóban szüksége van rá és megértettük a használatának költségeit. Ha egy teljes metódusnak szüksége van a szinkronizálásra, akkor célszerű előnyben részesíteni a metódus módosítót a synchronized blokkal szemben.

StringBuilder

A Java nyelvben a sztringműveletek használata sokat egyszerűsödött a C nyelvhez képest, a sztringek literálként kezelhetőek, sőt alapértelmezettként ez ajánlott. Ezek a bájtkódban azonban már viszonylag alacsony szintű műveletekké fordítódnak le. Lássuk, hogyan:

public void stringMethod() {
    String o = new String("string object");
    String l = "string literal";
    System.out.println(o + l);
    System.out.println(o + 100);
}

A példában létrehozunk két szöveges változót, majd összefűzve kiíratjuk őket, illetve az egyik esetében egy számot is hozzáadunk. Ebből a következő bájtkód készül:

public void stringMethod();
  Code:
     0: new           #11     // class java/lang/String
     3: dup           
     4: ldc           #30     // String string object
     6: invokespecial #15     // Method java/lang/String."<init>":(Ljava/lang/String;)V
     9: astore_1      
    10: ldc           #32     // String string literal
    12: astore_2      
    13: getstatic     #18     // Field java/lang/System.out:Ljava/io/PrintStream;
    16: new           #34     // class java/lang/StringBuilder
    19: dup           
    20: aload_1       
    21: invokestatic  #36     // Method java/lang/String.valueOf:(Ljava/lang/Object;)
                              // Ljava/lang/String;
    24: invokespecial #40     // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
    27: aload_2       
    28: invokevirtual #41     // Method java/lang/StringBuilder.append:
                              //(Ljava/lang/String;)Ljava/lang/StringBuilder;
    31: invokevirtual #45     // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    34: invokevirtual #24     // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    37: getstatic     #18     // Field java/lang/System.out:Ljava/io/PrintStream;
    40: new           #34     // class java/lang/StringBuilder
    43: dup           
    44: aload_1       
    45: invokestatic  #36     // Method java/lang/String.valueOf:(Ljava/lang/Object;)
                              // Ljava/lang/String;
    48: invokespecial #40     // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
    51: bipush        100
    53: invokevirtual #49     // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
    56: invokevirtual #45     // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    59: invokevirtual #24     // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    62: return        

A new opkód létrehozza a konstanskészletben megadott index által azonosított osztály új példányát és elhelyezi annak referenciáját a vermen. Látható, hogy a String o = new String("string object") utasításhoz 5 bájtkód sor tartozik (0-9), míg a String l = "string literal" sorhoz mindössze kettő (10-12), ez esetben semmilyen új objektum nem jön létre, mindössze a lokális változótábla megfelelő cellájába bekerül a sztringkonstans referenciája. Az első összeadáshoz tartozó bájtkód a 16-31. sorban található. Még ebben a legegyszerűbb esetben is létrejön egy új StringBuilder példány, az összeadás pedig StringBuilder.append műveletté fordítódik. A második összeadás hasonlóan StringBuilderrel történik (40-56. sor), a 100 konstans közvetlenül a megfelelő append metódusnak adódik át. Mivel a 100 egy bájtban is elfér, ezért használható a bipush opkód, ami a következő bájt értéket helyezi el integerként a veremre. Amennyiben nagyobb számot adunk meg konstansként, sipush (2 bájt esetén) vagy konstanskészletbeli betöltés (long esetén) kerül a helyére. Ezzel a fordító helyet is spórol, a konstanshoz az alkalmas legkisebb tárterület lesz használva a bájtkódban.

Autoboxing

A Java 5-ben jelent meg a primitív típusokat automatikusan objektumokká és vissza alakító autoboxing technológia. Ennek segítségével már nem kell egy adott műveletnél arra figyelni, hogy primitív típust vagy az annak megfelelő referencia-típust használjuk-e. A következő példa megmutatja, hogyan néz ki ez a bájtkódban:

public void boxing() {
    long res = 0l;
    res = boxingAdd(res);
}

public Long boxingAdd(Long a) {
    return a + 2l;
}

Ez a kódrészlet a következő bájtkóddá fordul le:

public void boxing();
  Code:
     0: lconst_0      
     1: lstore_1      
     2: aload_0       
     3: lload_1       
     4: invokestatic  #30                 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
     7: invokevirtual #36                 // Method boxingAdd:(Ljava/lang/Long;)Ljava/lang/Long;
    10: invokevirtual #40                 // Method java/lang/Long.longValue:()J
    13: lstore_1      
    21: return        

public java.lang.Long boxingAdd(java.lang.Long);
  Code:
     0: aload_1       
     1: invokevirtual #40                 // Method java/lang/Long.longValue:()J
     4: ldc2_w        #47                 // long 2l
     7: ladd          
     8: invokestatic  #30                 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
    11: areturn       

Amint látható, amikor az átalakításra szükség van, a Long.valueOf vagy Long.longValue metódusok hívódnak meg.

Konstans referencia

A Java tartalmaz egy kevésbé ismert, ám egy rejtelmes hibalehetőséget magában rejtő tulajdonságot is (hibát, amennyiben a nyelvet nem megfelelően használják).

A Long.valueOf(int i) metódus forráskódja a következő módon néz ki:

public static Integer valueOf(int i) {
    assert IntegerCache.high >= 127;
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

Mivel az objektum létrehozás viszonylag költséges művelet, a Java fejlesztői a teljesítmény növelésére és a tárhely csökkentésére azt a megoldást találták ki, hogy bizonyos primitív típusok esetén előre eltárolják azok gyakran használt értékéhez tartozó előre létrehozott (ez esetben statikus inicializátorban létrejött) objektumokat és amikor erre szükség van (mint például a fenti autoboxing példában), akkor nem hoznak létre új objektumot, hanem csak az átmeneti tárolóból (IntegerCache) választják ki az értékhez tartozó objektum referenciáját. Alapértelmezetten ez a -128 és 127 közötti értékeknél működik (a felső érték paraméterezhető, de 127-nél nem lehet nagyobb).

Primitív típusok esetén az == művelet a változók értékét hasonlítja össze, objektumok esetén viszont a referencia egyezését. (A String típus esetén literálként használva ugyanazon konstanskészlet-bejegyzésben tárolódnak, tehát "string"=="string" true lesz, de new String("string")==new String("string") már false)
Ennek a megoldásnak van egy nagyon fontos következménye. Amennyiben mindezzel nem vagyunk tisztában, a következő kód meglepő eredményt ad:

Integer i1 = 128;
Integer i2 = 128;
System.out.println(i1 == i2);
i1 = 127;
i2 = 127;
System.out.println(i1 == i2);

Az első feltételvizsgálat eredménye false lesz, a második eredménye true. Ennek oka most már világos: első esetben két új Integer objektum jön létre, a második esetben viszont mindkét változó ugyanannak az objektumnak a referenciáját kapja meg. (És Long típus esetén ugyanez az eredmény.) Az elvárt működéshez itt is az equals metódust kell használni.

Ha pedig még a reflection keretrendszert is bevetjük, akár meg is változtathatjuk az egyébként private elérésű IntegerCache értékeit, így aztán tetszőleges értéket adhatunk bizonyos számoknak:

value = Integer.class.getDeclaredField("value");
value.setAccessible(true);
value.set(1, 2);
Integer i = 1;
System.out.println(i + 1);

A programrészlet eredménye az autoboxing miatt 3 lesz! Ha viszont az i=new Integer(1) utasítással inicializálunk, akkor már 2.

Vararg

Az autoboxing mellett ugyancsak a Java 5-ben jelent meg a változó hosszúságú paraméterlista, ami olvashatóbbá tette tetszőleges számú azonos típusú paraméter átadását metódusoknak, hiszen nem kellett ezeket listába vagy tömbbe szervezni. Az ezt használó metódus törzsében ezt a paraméterlistát tömbként lehet elérni (ennek mérete lehet 0 is, ha a metódus változó paraméterlistáját nem adjuk meg).

public void methodVar(long... longparam) {
	System.out.println(longparam.length);
}

public void methodArray(long[] longparam) {
	System.out.println(longparam.length);
}

public void caller() {
	methodVar(1l, 2l, 3l);
	methodArray(new long[] { 1l, 2l, 3l });
}

A két példametódus törzsében a longparam paraméter kezelése azonos, azonban a függvények szignatúrája illetve hívásuk eltérő. Amint azt a bájtkód vizsgálata megmutatja, ez a nyelvi tulajdonság csupán a fordítót érintette, a bájtkód semmiben nem változott. A két metódus bájtkódja teljesen azonos (lefordított szignatúrájuk is, ezért nem is lehet azonos néven definiálni ilyen metódusokat a Java kódban sem):

    Code:
       0: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_1       
       4: arraylength   
       5: invokevirtual #21                 // Method java/io/PrintStream.println:(I)V
       8: return        

A két metódus hívása a fenti példában teljesen megegyezik (alább a 0.-19. sor teljesen megismétlődik a 21. sortól). Mindkét esetben egy új long tömb jön létre a paraméterekkel, sőt a fordító még azt is megengedi, hogy a methodVar metódust is explicit tömb paraméterrel hívjuk meg: methodVar(new long[] { 1l, 2l, 3l });. Ha egy változó paraméterlistájú metódust többször hívunk azonos paraméterekkel, akkor annyit érdemes megfontolni, hogy a paraméterlistából minden esetben új tömb jön létre, ez pedig költségesebb, mintha ugyanannak az előre létrehozott tömbnek a referenciáját kapná meg minden esetben.

  public void caller();
    Code:
       0: aload_0       
       1: iconst_3      
       2: newarray       long
       4: dup           
       5: iconst_0      
       6: lconst_1      
       7: lastore       
       8: dup           
       9: iconst_1      
      10: ldc2_w        #31                 // long 2l
      13: lastore       
      14: dup           
      15: iconst_2      
      16: ldc2_w        #33                 // long 3l
      19: lastore       
      20: invokevirtual #35                 // Method methodVar:([J)V
      23: aload_0       
      24: iconst_3      
      25: newarray       long
      27: dup           
      28: iconst_0      
      29: lconst_1      
      30: lastore       
      31: dup           
      32: iconst_1      
      33: ldc2_w        #31                 // long 2l
      36: lastore       
      37: dup           
      38: iconst_2      
      39: ldc2_w        #33                 // long 3l
      42: lastore       
      43: invokevirtual #37                 // Method methodArray:([J)V
      46: return        

Switch

A switch kifejezés támogatásához a bájtkód két speciális utasítást használ: tableswitch és lookupswitch. Mindkettő int típusú értékekkel dolgozik (más, a switch kifejezésben használható típus int típussá konvertálódik a JVM-ben). A tableswitch általában gyorsabb, viszont több memóriát igényel, ugyanis a switch kifejezésben megadott értékek minimuma és maximuma között minden lehetséges értéket felsorol. Ilyen módon a JVM azonnal a default blokkra tud ugrani, amennyiben a vizsgált változó értéke nincs a felsorolt case esetek között. A bájtkódba tehát bekerülnek azok az értékek is (a default blokkra hivatkozva), amelyek a Java kódban nem voltak felsorolva. Tekintsük a következő példát:

public void methodSwitch(int param) {
	switch (param) {
	case 5:
		System.out.println("öt");
		break;
	case 2:
		System.out.println("kettő");
		break;
	case 1:
		System.out.println("egy");
		break;
	default:
		System.out.println("sok");
	}
}

Ebből a következő bájtkód készül:

  public void methodSwitch(int);
    Code:
       0: iload_1       
       1: tableswitch   { // 1 to 5
                     1: 58
                     2: 47
                     3: 69
                     4: 69
                     5: 36
               default: 69
          }
      36: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
      39: ldc           #21                 // String öt
      41: invokevirtual #23                 // Method java/io/PrintStream.println:
                                            // (Ljava/lang/String;)V
      44: goto          77
      47: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
      50: ldc           #29                 // String kettő
      52: invokevirtual #23                 // Method java/io/PrintStream.println:
                                            // (Ljava/lang/String;)V
      55: goto          77
      58: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
      61: ldc           #31                 // String egy
      63: invokevirtual #23                 // Method java/io/PrintStream.println:
                                            // (Ljava/lang/String;)V
      66: goto          77
      69: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
      72: ldc           #33                 // String sok
      74: invokevirtual #23                 // Method java/io/PrintStream.println:
                                            // (Ljava/lang/String;)V
      77: return                            // A switch utáni első utasítás: itt visszatérés a
                                            // metódusból

A tableswitch utasításnak 1, 2 és 5 van megadva a megfelelő case kifejezésekhez, melyek mindegyike az annak megfelelő blokk első utasítását címzi a kódban (58, 47, 36). Emellett bekerült a 3 és 4 is, ezek a switch kifejezésben nem szerepeltek, ezért a default blokkra mutatnak (69). Amennyiben a Java kódban nem szerepel default ág, a tableswitch default része a switch utáni első utasításra fog mutatni. A generált .class fájlban a művelet a következő bájtokból áll:

Hexa Leírás
AA 00 00 Az utasítás opkódja: AA. Az opkód után ennél az utasításnál annyi 0 értékű bájt áll még, hogy az első operandus 4-el osztható bájton kezdődjön. A példában ehhez 2 bájt kell. Az utasítás minden operandusa int típusú, vagyis négy bájtból áll.
00 00 00 44 A default ághoz tartozó kód kezdetének távolsága az opkódtól. Itt 68.
00 00 00 01 Az utasítás case esetei által befogott intervallum minimuma.
00 00 00 05 Az utasítás case esetei által befogott intervallum maximuma.
00 00 00 39 A minimum értékhez (most 1) tartozó ág távolsága (itt 57).
00 00 00 2E A minimum+1 értékhez tartozó ág távolsága.
00 00 00 44 A minimum+2 értékhez tartozó ág távolsága.
00 00 00 44 A minimum+3 értékhez tartozó ág távolsága.
00 00 00 23 A minimum+4 értékhez (itt a maximum) tartozó ág távolsága.

Amint látható, a bájtkódban először következik a default ághoz tartozó kód távolsága, aztán a minimum és maximum értékek s csak aztán az egyes esetek. Amikor az utasítás végrehajtódik, az operandusverem tetején lévő értéket megvizsgálja a JVM, hogy a minimum és maximum közé esik-e. Ha nem, akkor a vezérlés a default ágra kerül. Ha igen, akkor az érték az utasításhoz tartozó célcím-táblázat indexelésére lesz felhasználva, így szinte azonnal végrehajtható az ugrás a megfelelő eset kódjára. Amennyiben hét case ágnál több eset lehetséges ezzel a módszerrel, a lookupswitch utasítást használja a fordító. Ez történik a Java 7-ben bevezetett sztringeket kezelő switch esetén is. Mivel a lookupswitch is csak int típussal képes dolgozni, ezért string típusú paraméter esetén a fordító előbb generál egy hashCode() metódushívást a paraméter sztringen, ami meghatározza a sztringnek megfelelő hash kódot és ez lesz aztán felhasználva a műveletnél. Tekintsük a következő példát:

public void methodSwitch(String param) {
	switch (param) {
	case "egy":
		System.out.println(1);
		break;
	case "kettő":
		System.out.println(2);
		break;
	default:
	}
}

A switch utasítás esetén tehát a generált kód esetében nincs különbség int vagy azzá konvertálható típusú paraméterekhez képest, csupán egy hashCode() metódushívás megelőzi a lookupswitch-et, ami aztán annak az eredményével fog dolgozni. Ez látható a bájtkódban:

  public void methodSwitch(java.lang.String);
    Code:
       0: aload_1       
       1: dup           
       2: astore_2      
       3: invokevirtual #33                 // Method java/lang/String.hashCode:()I
       6: lookupswitch  { // 2
                100375: 32
             101941047: 44
               default: 73
          }
      32: aload_2       
      33: ldc           #28                 // String egy
      35: invokevirtual #39                 // Method java/lang/String.equals:
                                            // (Ljava/lang/Object;)Z
      38: ifne          56
      41: goto          73
      44: aload_2       
      45: ldc           #26                 // String kettő
      47: invokevirtual #39                 // Method java/lang/String.equals:
                                            // (Ljava/lang/Object;)Z
      50: ifne          66
      53: goto          73
      56: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
      59: iconst_1      
      60: invokevirtual #43                 // Method java/io/PrintStream.println:(I)V
      63: goto          73
      66: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
      69: iconst_2      
      70: invokevirtual #43                 // Method java/io/PrintStream.println:(I)V
      73: return        

A lookupswitch opkódja AB, ez is tartalmaz kiegészítő 0 bájtokat, ha szükséges. Ezt követi a default ág kódjának távolsága, a case ágak száma, majd a case esetek párjai, minden esetben elsőként az érték, aztán a kód távolsága. Az utasítás végrehajtásakor a JVM minden egyes esettel összehasonlítja a vermen lévő értéket és az ennek megfelelő ágra ugrik (a gyorsabb összehasonlítás érdekében a JVM specifikációja előírja, hogy az "ágcímek" sorba legyenek rendezve). Mivel string típusú paraméter esetén a hash kód van használva, ezért a string-switch több ág esetén gyorsabb, mintha if-else utasításokkal minden egyes változaton egy equals() függvényhívás lenne megadva.

Generikus osztályok

Végezetül viszgáljuk meg, a generikus osztályok hogyan működnek a bájtkód szintjén. A Java nyelvben a generikus programozás szigorúbb típusellenőrzést tesz lehetővé fordítási időben. A javac fordításkor ún. típustörlést végez, vagyis lecserél minden típusparamétert azok megadott ősére vagy ha ilyen nincs megadva, Object típusra. A generált bájtkód így generikus típus nélküli lesz. Ezen kívül ha szükséges, típuskényszerítést (cast) generál a kódba, hogy biztosítsa a típusbiztonságot valamint áthidaló metódusokat is létrehoz ha szükséges, hogy biztosítsa a polimorfizmust. Nézzük a következő osztályt:

public class GenericsClass {
    List<String> array = new ArrayList<>();
    List array2 = new ArrayList();

    public void sampleMethod() {
        array.add("string");
        array2.add("string");
    }
}

A bájtkód vizsgálata azt mutatja, hogy mindkét lista ugyanúgy jön létre (az inicializálást a fordító a konstruktorba teszi) és az add metódus használatában sincs semmilyen különbség. Ami különbözik a két esetben, hogy az array változónak van egy Signature záradéka, amely a konstanskészletben kijelöli a mezőhöz tartozó pontos típust (a könnyebb áttekinthetőség érdekében a példa szempontjából lényegtelen adatokat kitöröltem).

Constant pool:
   #8 = Utf8               Ljava/util/List<Ljava/lang/String;>;
{
  java.util.List<java.lang.String> array;
    Signature: #8                           // Ljava/util/List<Ljava/lang/String;>;

  java.util.List array2;

  public GenericsClass();
    flags: ACC_PUBLIC

    LocalVariableTable:
      Start  Length  Slot  Name   Signature
             0      27     0  this   LGenericsClass;
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #13                 // Method java/lang/Object."<init>":()V
         4: aload_0       
         5: new           #15                 // class java/util/ArrayList
         8: dup           
         9: invokespecial #17                 // Method java/util/ArrayList."<init>":()V
        12: putfield      #18                 // Field array:Ljava/util/List;
        15: aload_0       
        16: new           #15                 // class java/util/ArrayList
        19: dup           
        20: invokespecial #17                 // Method java/util/ArrayList."<init>":()V
        23: putfield      #20                 // Field array2:Ljava/util/List;
        26: return        
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      27     0  this   LGenericsClass;

  public void sampleMethod();
    flags: ACC_PUBLIC

    LocalVariableTable:
      Start  Length  Slot  Name   Signature
             0      25     0  this   LGenericsClass;
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0       
         1: getfield      #18                 // Field array:Ljava/util/List;
         4: ldc           #27                 // String string
         6: invokeinterface #29,  2           // InterfaceMethod java/util/List.add:
                                              // (Ljava/lang/Object;)Z
        11: pop           
        12: aload_0       
        13: getfield      #20                 // Field array2:Ljava/util/List;
        16: ldc           #27                 // String string
        18: invokeinterface #29,  2           // InterfaceMethod java/util/List.add:
                                              // (Ljava/lang/Object;)Z
        23: pop           
        24: return        
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      25     0  this   LGenericsClass;
}

Ez ez írás csak bevezetést adott a Java bájtkód világába, de minden Java programozónak érdemes megismerni legalább nagy vonalakban, mi is történik a motorház alatt. Az összeállításhoz nagyon sok internetes forrást felhasználtam, az alábbi listában felsorolom a legfontosabbakat.