Programovanie

Základy kódu Bytecode

Vitajte v ďalšej časti hry „Under The Hood“. Tento stĺpec poskytuje vývojárom Java prehľad o tom, čo sa deje pod ich spustenými programami Java. V tomto článku sa najskôr venujeme inštrukčnej sade bytových kódov virtuálneho stroja Java (JVM). Tento článok sa týka primitívnych typov prevádzkovaných pomocou bajtových kódov, bajtových kódov konvertujúcich medzi typmi a bajtových kódov fungujúcich na zásobníku. V nasledujúcich článkoch sa bude diskutovať o ďalších členoch rodiny bytecode.

Formát bytecode

Bytecodes sú strojový jazyk virtuálneho stroja Java. Keď JVM načíta súbor triedy, získa pre každú metódu v triede jeden prúd bajtových kódov. Prúdy bytových kódov sú uložené v oblasti metód JVM. Bajtkódy pre metódu sa vykonajú, keď sa táto metóda vyvolá v priebehu behu programu. Môžu byť vykonané pomocou interpretácie, kompilácie just-in-time alebo akejkoľvek inej techniky, ktorú vybral dizajnér konkrétneho JVM.

Stream bytového kódu metódy je postupnosťou pokynov pre virtuálny stroj Java. Každá inštrukcia pozostáva z jednobajtového kódu operačný kód po ktorom nasleduje nula alebo viac operandy. Operačný kód označuje akciu, ktorú je potrebné vykonať. Ak je predtým, ako môže JVM vykonať akciu, potrebných viac informácií, tieto informácie sú zakódované do jedného alebo viacerých operandov, ktoré bezprostredne nasledujú po operačnom kóde.

Každý typ operačného kódu má mnemotechniku. V typickom štýle montážneho jazyka môžu byť prúdy bajtových kódov Java reprezentované ich mnemotechnikami, za ktorými nasledujú ľubovoľné hodnoty operandov. Napríklad nasledujúci prúd bajtových kódov možno rozobrať do mnemotechniky:

// Prúd bytecode: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Demontáž: iconst_0 // 03 istore_0 // 3b iinc 0, 1 // 84 00 01 iload_0 // 1a iconst_2 // 05 imul // 68 istore_0 // 3b choď -7 // a7 ff f9 

Sada inštrukcií bytecode bola navrhnutá tak, aby bola kompaktná. Všetky pokyny, s výnimkou dvoch, ktoré sa zaoberajú skákaním tabuľky, sú zarovnané na hraniciach bajtov. Celkový počet kódov je dosť malý, takže kódy zaberajú iba jeden bajt. To pomáha minimalizovať veľkosť súborov triedy, ktoré môžu cestovať v sieťach predtým, ako ich načíta JVM. Pomáha tiež udržiavať malú veľkosť implementácie JVM.

Všetky výpočty v JVM sa sústreďujú na hromádku. Pretože JVM nemá žiadne registre na ukladanie náhodných hodnôt, musí sa všetko vložiť do zásobníka, aby sa dalo použiť vo výpočte. Pokyny pre Bytecode preto fungujú primárne na zásobníku. Napríklad vo vyššie uvedenej sekvencii bytecode sa lokálna premenná vynásobí dvoma tak, že sa lokálna premenná najskôr natlačí na zásobník pomocou iload_0 inštrukcie, potom zatlačte dva na stoh pomocou ikonast_2. Po vložení oboch celých čísel do stohu sa zobrazí znak imul inštrukcia efektívne odstráni dve celé čísla zo zásobníka, znásobí ich a posunie výsledok späť do zásobníka. Výsledok sa vysunie z hornej časti zásobníka a uloží ho späť do miestnej premennej istore_0 poučenie. JVM bol navrhnutý skôr ako zásobníkový stroj ako stroj založený na registroch, aby sa uľahčila efektívna implementácia na architektúrach so zlým registrom, ako je Intel 486.

Primitívne typy

JVM podporuje sedem primitívnych dátových typov. Programátori jazyka Java môžu deklarovať a používať premenné týchto dátových typov a bajtové kódy Java pracujú s týmito dátovými typmi. Sedem primitívnych typov je uvedených v nasledujúcej tabuľke:

TypDefinícia
bajtjednobajtové celé číslo so znamienkom dvojky
krátkydvojbajtové celé číslo so znamienkom dvojky
int4-bajtové celé číslo podpísané dvojkou
dlho8-bajtové celé číslo so znamienkom dvojky
plavák4-bajtový float s jednou presnosťou IEEE 754 s jednou presnosťou
dvojitý8-bajtový float s dvojitou presnosťou IEEE 754
char2-bajtový znak Unicode bez znamienka

Primitívne typy sa v tokoch bajtových kódov zobrazujú ako operandy. Všetky primitívne typy, ktoré zaberajú viac ako 1 bajt, sú uložené v toku big-endian v streame bytecode, čo znamená, že bajty vyššieho poradia predchádzajú bajtom nižšieho rádu. Napríklad na tlačenie konštantnej hodnoty 256 (hex 0100) do zásobníka by ste použili sipush operačný kód nasledovaný krátkym operandom. Skratka sa v prúde bytecode, ktorý je zobrazený nižšie, zobrazuje ako „01 00“, pretože JVM je big-endian. Ak by JVM boli málo endiánske, skratka by sa javila ako „00 01“.

 // Stream bytecode: 17 01 00 // Demontáž: sipush 256; // 17 01 00 

Operačné kódy Java vo všeobecnosti označujú typ svojich operandov. To umožňuje operandom byť sami sebou, bez potreby identifikácie ich typu pre JVM. Napríklad namiesto toho, aby mal jeden operačný kód, ktorý tlačí lokálnu premennú do zásobníka, má JVM niekoľko. Operačné kódy iload, naložiť, naložiťa dload natlačiť do zásobníka lokálne premenné typu int, long, float a double.

Vytlačenie konštánt do stohu

Mnoho operačných kódov tlačí konštanty do zásobníka. Operačné kódy označujú konštantnú hodnotu, ktorá sa má tlačiť, tromi rôznymi spôsobmi. Konštantná hodnota je buď implicitná v samotnom operačnom kóde, sleduje operačný kód v prúde bytecode ako operand, alebo je prevzatá z konštantnej oblasti.

Niektoré operačné kódy samy o sebe označujú typ a konštantnú hodnotu, ktoré sa majú tlačiť. Napríklad ikonast_1 operačný kód povie JVM, aby vložil celočíselnú hodnotu jedna. Takéto bajtkódy sú definované pre niektoré bežne tlačené počty rôznych typov. Tieto pokyny zaberajú v toku bajtových kódov iba 1 bajt. Zvyšujú efektívnosť vykonávania bytecode a znižujú veľkosť prúdov bytecode. Ochranné kódy, ktoré tlačia dovnútra a plávajú, sú uvedené v nasledujúcej tabuľke:

Operačný kódOperand (y)Popis
ikonast_m1(žiadny)tlačí do zásobníka int -1
iconst_0(žiadny)tlačí int 0 do stohu
ikonast_1(žiadny)tlačí int 1 do stohu
ikonast_2(žiadny)tlačí int 2 do stohu
iconst_3(žiadny)tlačí int 3 do stohu
iconst_4(žiadny)tlačí int 4 do stohu
iconst_5(žiadny)tlačí int 5 do stohu
fconst_0(žiadny)tlačí float 0 do stohu
fconst_1(žiadny)tlačí float 1 do stohu
fconst_2(žiadny)tlačí float 2 do stohu

Opcodes zobrazené v predchádzajúcej tabuľke tlačia ints a float, čo sú 32-bitové hodnoty. Každý slot v zásobníku Java je široký 32 bitov. Preto zakaždým, keď je int alebo plavák natlačený na komín, obsadí jeden slot.

Protokódy zobrazené v nasledujúcej tabuľke tlačia dlho a zdvojnásobujú sa. Dlhé a dvojité hodnoty zaberajú 64 bitov. Zakaždým, keď je dlhý alebo dvojitý posunutý na hromádku, jej hodnota zaberá dva sloty na hromádke. V nasledujúcej tabuľke sú uvedené operačné kódy, ktoré označujú konkrétnu dlhú alebo dvojitú hodnotu, ktorá sa má stlačiť.

Operačný kódOperand (y)Popis
lconst_0(žiadny)tlačí dlhú 0 do stohu
lconst_1(žiadny)tlačí dlhú 1 do stohu
dconst_0(žiadny)natlačí na stack dvojitú 0
dconst_1(žiadny)tlačí dvojitý 1 do stohu

Jeden ďalší operačný kód tlačí implicitnú konštantnú hodnotu do zásobníka. The aconst_null operačný kód, zobrazený v nasledujúcej tabuľke, vloží do zásobníka odkaz na nulový objekt. Formát referencie na objekt závisí od implementácie JVM. Odkaz na objekt bude nejakým spôsobom odkazovať na objekt Java na halde zhromaždenej odpadkami. Nulový odkaz na objekt označuje, že premenná odkazu na objekt momentálne neodkazuje na žiadny platný objekt. The aconst_null opcode sa používa v procese priraďovania null k referenčnej premennej objektu.

Operačný kódOperand (y)Popis
aconst_null(žiadny)vloží do zásobníka odkaz na nulový objekt

Dva operačné kódy označujú konštantu, ktorá sa má tlačiť, operandom, ktorý bezprostredne nasleduje po operačnom kóde. Tieto operačné kódy zobrazené v nasledujúcej tabuľke sa používajú na vloženie celočíselných konštánt, ktoré sú v rámci platného rozsahu pre bajt alebo krátke typy. Bajt alebo skratka, ktorá nasleduje po operačnom kóde, sa pred vložením do zásobníka rozšíri na int, pretože každý slot v zásobníku Java je široký 32 bitov. Operácie s bajtmi a skratmi, ktoré boli vložené do zásobníka, sa v skutočnosti vykonávajú na ich ekvivalentoch int.

Operačný kódOperand (y)Popis
bipushbyte1rozbalí byte1 (typ bytu) na int a vloží ho do zásobníka
sipushbyte1, byte2rozšíri byte1, byte2 (krátky typ) na int a presunie ho do zásobníka

Tri opcodes tlačia konštanty z konštantného poolu. Všetky konštanty spojené s triedou, napríklad konečné hodnoty premenných, sú uložené v skupine konštantných zdrojov triedy. Opcodes, ktoré tlačia konštanty z konštantnej oblasti, majú operandy, ktoré určujú, ktorú konštantu majú tlačiť, zadaním indexu konštantnej oblasti. Virtuálny stroj Java vyhľadá konštantu danú indexom, určí typ konštanty a natlačí ju na zásobník.

Index konštantnej oblasti je hodnota bez znamienka, ktorá bezprostredne nadväzuje na operačný kód v prúde bytecode. Operačné kódy lcd1 a lcd2 natlačí 32-bitovú položku do stohu, napríklad int alebo float. Rozdiel medzi lcd1 a lcd2 je to tak lcd1 môže odkazovať iba na konštantné umiestnenia spoločného fondu od 1 do 255, pretože jeho index je iba 1 bajt. (Konštantná nula umiestnenia spoločného fondu sa nepoužíva.) lcd2 má 2-bajtový index, takže môže odkazovať na akékoľvek konštantné umiestnenie fondu. lcd2w má tiež dvojbajtový index a používa sa na označenie ľubovoľného konštantného umiestnenia spoločného fondu obsahujúceho dlhý alebo dvojitý, ktorý zaberá 64 bitov. Protokoly, ktoré tlačia konštanty z konštantnej oblasti, sú uvedené v nasledujúcej tabuľke:

Operačný kódOperand (y)Popis
ldc1indexbyte1vloží do zásobníka 32-bitový záznam constant_pool určený indexbyte1
ldc2indexbyte1, indexbyte2vloží do zásobníka 32-bitový záznam constant_pool určený indexbyte1, indexbyte2
ldc2windexbyte1, indexbyte2vloží do zásobníka 64-bitový záznam constant_pool určený indexbyte1, indexbyte2

Vytlačenie lokálnych premenných do zásobníka

Lokálne premenné sú uložené v špeciálnej časti rámca zásobníka. Rámec stohu je časť stohu, ktorá sa používa v súčasnosti vykonávajúcej metóde. Každý rámec zásobníka sa skladá z troch častí - lokálnych premenných, prostredia vykonávania a zásobníka operandov. Vloženie lokálnej premennej do zásobníka v skutočnosti zahŕňa presun hodnoty z časti lokálnych premenných rámca zásobníka do časti operandu. Sekcia operandu aktuálne vykonávajúcej metódy je vždy horná časť zásobníka, takže tlačenie hodnoty na operandovú časť aktuálneho rámca zásobníka je rovnaké ako tlačenie hodnoty na hornú časť zásobníka.

Zásobník Java je posledný a prvý zásobník 32-bitových slotov. Pretože každý slot v zásobníku zaberá 32 bitov, všetky miestne premenné zaberajú najmenej 32 bitov. Lokálne premenné typu long a double, čo sú 64-bitové veličiny, zaberajú v zásobníku dva sloty. Lokálne premenné typu byte alebo short sú uložené ako lokálne premenné typu int, ale s hodnotou platnou pre menší typ. Napríklad lokálna premenná typu int, ktorá predstavuje typ bajtu, bude vždy obsahovať hodnotu platnú pre bajt (-128 <= hodnota <= 127).

Každá lokálna premenná metódy má jedinečný index. Sekciu lokálnych premenných rámca zásobníka metódy možno považovať za pole 32-bitových slotov, z ktorých každý je adresovateľný indexom poľa. Lokálne premenné typu long alebo double, ktoré zaberajú dva sloty, sa označujú spodným z dvoch indexov slotov. Napríklad dvojník, ktorý zaberá sloty dva a tri, by sa označoval indexom dva.

Existuje niekoľko operačných kódov, ktoré tlačia lokálne a interné premenné do zásobníka operandov. Sú definované niektoré operačné kódy, ktoré implicitne odkazujú na bežne používanú pozíciu lokálnej premennej. Napríklad, iload_0 načíta lokálnu premennú int na pozícii nula. Ostatné lokálne premenné sú do zásobníka vložené operačným kódom, ktorý prevezme index lokálnych premenných z prvého bajtu nasledujúceho po operačnom kóde. The iload inštrukcia je príkladom tohto typu operačného kódu. Nasleduje prvý bajt iload sa interpretuje ako nepodpísaný 8-bitový index, ktorý odkazuje na lokálnu premennú.

Nepodpísané 8-bitové indexy lokálnych premenných, napríklad index, ktorý nasleduje za iload inštrukcia, obmedzte počet lokálnych premenných v metóde na 256. Samostatná inštrukcia, tzv široký, môže rozšíriť 8-bitový index o ďalších 8 bitov. To zvyšuje limit lokálnych premenných na 64 kilobajtov. The široký za operačným kódom nasleduje 8-bitový operand. The široký operačný kód a jeho operand môže predchádzať inštrukcii, ako napr iload, ktorý vyžaduje 8-bitový nepodpísaný index lokálnych premenných. JVM kombinuje 8-bitový operand operačného systému široký inštrukcia s 8-bitovým operandom operačného systému iload inštrukcia na získanie 16-bitového indexu lokálnej premennej bez znamienka.

Opcodes, ktoré tlačia lokálne a interné premenné do zásobníka, sú uvedené v nasledujúcej tabuľke:

Operačný kódOperand (y)Popis
iloadvindextlačí int z lokálnej premennej polohy vindex
iload_0(žiadny)tlačí int z polohy lokálnej premennej nula
iload_1(žiadny)tlačí int z lokálnej premennej polohy jedna
iload_2(žiadny)tlačí int z polohy lokálnej premennej dva
iload_3(žiadny)tlačí int z lokálnej premennej polohy tri
naložiťvindextlačí float z lokálnej premennej polohy vindex
fload_0(žiadny)tlačí float z lokálnej premennej polohy nula
fload_1(žiadny)tlačí float z lokálnej premennej polohy jedna
fload_2(žiadny)tlačí float z lokálnej premennej polohy dva
fload_3(žiadny)tlačí float z lokálnej premennej polohy tri

Nasledujúca tabuľka zobrazuje pokyny, ktoré posúvajú lokálne premenné typu long a double do zásobníka. Tieto pokyny presunú 64 bitov z časti lokálnej premennej rámca zásobníka do časti operandu.

$config[zx-auto] not found$config[zx-overlay] not found