Programování

Základy Bytecode

Vítejte v další části hry „Under The Hood“. Tento sloupec poskytuje vývojářům prostředí Java letmý pohled na to, co se děje pod jejich spuštěnými programy Java. Článek z tohoto měsíce pojednává nejprve o instrukční sadě bajtových kódů virtuálního stroje Java (JVM). Tento článek popisuje primitivní typy provozované podle bytecodes, bytecodes, které převádějí mezi typy, a bytecodes, které fungují na zásobníku. Následující články pojednávají o dalších členech rodiny bytových kódů.

Formát bytecode

Bytecodes jsou strojový jazyk virtuálního stroje Java. Když JVM načte soubor třídy, získá pro každou metodu ve třídě jeden proud bajtových kódů. Proudy bytových kódů jsou uloženy v oblasti metod JVM. Bajtkódy pro metodu se provádějí, když je tato metoda vyvolána během běhu programu. Mohou být provedeny intepretací, just-in-time kompilací nebo jakoukoli jinou technikou, kterou vybral designér konkrétního JVM.

Proud bytecode metody je posloupnost pokynů pro virtuální stroj Java. Každá instrukce se skládá z jednobajtového operační kód následuje nula nebo více operandy. Operační kód označuje akci, kterou je třeba provést. Pokud je před provedením akce vyžadováno více informací, jsou tyto informace zakódovány do jednoho nebo více operandů, které bezprostředně následují operační kód.

Každý typ operačního kódu má mnemotechniku. V typickém stylu jazyka sestavení mohou být proudy bajtových kódů Java reprezentovány jejich mnemotechnikou následovanou libovolnými hodnotami operandů. Například následující proud bytecodes lze rozebrat do mnemotechnických pomůcek:

// Bytecode stream: 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 go -7 // a7 ff f9 

Sada instrukcí bytecode byla navržena tak, aby byla kompaktní. Všechny pokyny, s výjimkou dvou, které se zabývají skákáním tabulky, jsou zarovnány na hranicích bajtů. Celkový počet kódů je dostatečně malý, takže tyto kódy zabírají pouze jeden bajt. To pomáhá minimalizovat velikost souborů třídy, které mohou cestovat po sítích, než budou načteny JVM. Pomáhá také udržovat malou velikost implementace JVM.

Veškerý výpočet v JVM se soustředí na zásobník. Protože JVM nemá žádné registry pro ukládání náhodných hodnot, musí být vše před vložením do výpočtu vloženo do zásobníku. Pokyny Bytecode proto fungují primárně na zásobníku. Například ve výše uvedené sekvenci bajtových kódů se lokální proměnná vynásobí dvěma tak, že se lokální proměnná nejprve zatlačí na zásobník pomocí iload_0 instrukci, poté zatlačte dva na stack pomocí iconst_2. Po vložení obou celých čísel do zásobníku se zobrazí imul instrukce efektivně vyskočí dvě celá čísla ze zásobníku, znásobí je a posune výsledek zpět do zásobníku. Výsledek je vyskočen z horní části zásobníku a uložen zpět do místní proměnné pomocí istore_0 návod. JVM byl navržen spíše jako stroj na bázi zásobníku než na stroji založeném na registrech, aby se usnadnila efektivní implementace na architekturách chudých na registry, jako je Intel 486.

Primitivní typy

JVM podporuje sedm primitivních datových typů. Programátoři Java mohou deklarovat a používat proměnné těchto datových typů a bajtové kódy Java pracují s těmito datovými typy. Sedm primitivních typů je uvedeno v následující tabulce:

TypDefinice
bytejednobajtové celé číslo se znaménkem se dvěma znaménky
krátkýdvoubajtové celé číslo se znaménkem se dvěma bajty
int4-bajtové celé číslo se znaménkem se dvěma znaménky
dlouhoCelé číslo komplementu se znaménkem 8 bajtů se znaménkem
plovák4bajtový plovoucí plovoucí protokol IEEE 754 s jednou přesností
dvojnásobek8bajtový float s dvojitou přesností IEEE 754
char2bajtový znak Unicode bez znaménka

Primitivní typy se objevují jako operandy v streamech bytecode. Všechny primitivní typy, které zabírají více než 1 bajt, jsou uloženy v pořadí big-endian v proudu bytového kódu, což znamená, že bajty vyššího řádu předcházejí bajty nižšího řádu. Například k vložení konstantní hodnoty 256 (hex 0100) do zásobníku byste použili sipush operační kód následovaný krátkým operandem. Zkratka se v proudu bytového kódu, který je zobrazen níže, zobrazuje jako „01 00“, protože JVM je big-endian. Pokud by JVM byly málo endianské, zkratka by se zobrazila jako „00 01“.

 // Bytecode stream: 17 01 00 // Demontáž: sipush 256; // 17 01 00 

Opcodes Java obecně označují typ jejich operandů. To umožňuje operandům být sami sebou, aniž by bylo nutné identifikovat jejich typ podle JVM. Například místo toho, aby měl jeden operační kód, který tlačí lokální proměnnou do zásobníku, má JVM několik. Operační kódy iload, Načíst, načíst, a dload vložit lokální proměnné typu int, long, float a double do zásobníku.

Vytlačování konstant do zásobníku

Mnoho operačních kódů tlačí konstanty do zásobníku. Operační kódy označují konstantní hodnotu, která se má tlačit třemi různými způsoby. Konstantní hodnota je buď implicitní v samotném operačním kódu, sleduje operační kód v proudu bytecode jako operand, nebo je převzata z fondu konstant.

Některé operační kódy samy o sobě označují typ a konstantní hodnotu, která se má odeslat. Například iconst_1 opcode říká JVM, aby vložil celočíselnou hodnotu jedna. Takové bytecodes jsou definovány pro některé běžně tlačené počty různých typů. Tyto pokyny zabírají pouze 1 bajt v proudu bytového kódu. Zvyšují účinnost provádění bytecode a snižují velikost streamů bytecode. Opcodes, které push ints a floats jsou uvedeny v následující tabulce:

Operační kódOperand (s)Popis
iconst_m1(žádný)posune int -1 do zásobníku
iconst_0(žádný)tlačí int 0 do zásobníku
iconst_1(žádný)tlačí int 1 do zásobníku
iconst_2(žádný)tlačí int 2 do zásobníku
iconst_3(žádný)tlačí int 3 do zásobníku
iconst_4(žádný)tlačí int 4 do zásobníku
iconst_5(žádný)tlačí int 5 do zásobníku
fconst_0(žádný)tlačí float 0 do zásobníku
fconst_1(žádný)tlačí float 1 do stohu
fconst_2(žádný)tlačí float 2 na stack

Opcodes zobrazené v předchozí tabulce push ints a floats, což jsou 32bitové hodnoty. Každý slot v zásobníku Java je široký 32 bitů. Proto pokaždé, když je int nebo float tlačen na stack, zabírá jeden slot.

Opcodes zobrazené v následující tabulce tlačí longs a zdvojnásobuje. Dlouhé a dvojité hodnoty zabírají 64 bitů. Pokaždé, když je na stack vložen dlouhý nebo double, jeho hodnota zabírá dva sloty na stacku. V následující tabulce jsou uvedeny operační kódy, které označují konkrétní dlouhou nebo dvojitou hodnotu, která se má odeslat:

Operační kódOperand (s)Popis
lconst_0(žádný)tlačí dlouhou 0 do zásobníku
lconst_1(žádný)tlačí dlouhou 1 do stohu
dconst_0(žádný)tlačí dvojitou 0 do zásobníku
dconst_1(žádný)tlačí dvojitý 1 do stohu

Jeden další operační kód posílá implicitní konstantní hodnotu do zásobníku. The aconst_null operační kód, zobrazený v následující tabulce, vloží do zásobníku odkaz na nulový objekt. Formát odkazu na objekt závisí na implementaci JVM. Odkaz na objekt bude nějakým způsobem odkazovat na objekt Java na hromadě odpadu. Odkaz na nulový objekt označuje, že proměnná odkazu na objekt aktuálně neodkazuje na žádný platný objekt. The aconst_null opcode se používá v procesu přiřazování null k referenční proměnné objektu.

Operační kódOperand (s)Popis
aconst_null(žádný)vloží do zásobníku odkaz na nulový objekt

Dva operační kódy označují konstantu, která má být tlačena operandem, který bezprostředně následuje operační kód. Tyto operační kódy, zobrazené v následující tabulce, se používají k odeslání celočíselných konstant, které jsou v platném rozsahu pro bajt nebo krátké typy. Bajt nebo zkratka, která následuje po operačním kódu, se před vložením do zásobníku rozbalí na int, protože každý slot v zásobníku Java je široký 32 bitů. Operace s bajty a šortkami, které byly vloženy do zásobníku, se ve skutečnosti provádějí na jejich int ekvivalentech.

Operační kódOperand (s)Popis
bipushbyte1expanduje byte1 (typ bytu) na int a posune jej do zásobníku
sipushbyte1, byte2rozbalí byte1, byte2 (krátký typ) na int a vloží jej do zásobníku

Tři operační kódy tlačí konstanty z konstantní zásoby. Všechny konstanty spojené s třídou, například konečné hodnoty proměnných, jsou uloženy ve fondu konstant třídy. Opcodes, které posílají konstanty z fondu konstant, mají operandy, které označují, které konstanty se mají poslat, zadáním indexu konstantního fondu. Virtuální stroj Java vyhledá konstantu danou indexem, určí typ konstanty a vloží ji do zásobníku.

Index konstantního fondu je nepodepsaná hodnota, která bezprostředně následuje po operačním kódu v proudu bytového kódu. Operační kódy lcd1 a lcd2 vložit do zásobníku 32bitovou položku, například int nebo float. Rozdíl mezi lcd1 a lcd2 je to lcd1 může odkazovat pouze na konstantní umístění fondu od 1 do 255, protože jeho index je pouze 1 bajt. (Konstantní umístění nulového fondu se nepoužívá.) lcd2 má 2bajtový index, takže může odkazovat na jakékoli konstantní umístění fondu. lcd2w má také 2bajtový index a používá se k označení jakéhokoli konstantního umístění fondu obsahujícího dlouhý nebo dvojitý, které zabírají 64 bitů. Opcodes, které tlačí konstanty z fondu konstant, jsou uvedeny v následující tabulce:

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

Vkládání lokálních proměnných do zásobníku

Místní proměnné jsou uloženy ve speciální části rámce zásobníku. Rámec zásobníku je část zásobníku používaná aktuálně prováděnou metodou. Každý rámec zásobníku se skládá ze tří částí - lokálních proměnných, prostředí provádění a zásobníku operandů. Vložení lokální proměnné do zásobníku ve skutečnosti zahrnuje přesunutí hodnoty z části lokálních proměnných v rámci zásobníku do sekce operandu. Sekce operandu aktuálně provádějící metody je vždy horní část zásobníku, takže posunutí hodnoty na část operandu aktuálního rámce zásobníku je stejné jako vložení hodnoty na horní část zásobníku.

Zásobník Java je zásobník s 32bitovými sloty typu „poslední dovnitř a první“. Protože každý slot v zásobníku zabírá 32 bitů, všechny místní proměnné zabírají alespoň 32 bitů. Místní proměnné typu long a double, což jsou 64bitové veličiny, zabírají ve slotu dva sloty. Místní proměnné typu byte nebo short jsou uloženy jako místní proměnné typu int, ale s hodnotou, která je platná pro menší typ. Například lokální proměnná int, která představuje typ bajtu, bude vždy obsahovat hodnotu platnou pro bajt (-128 <= hodnota <= 127).

Každá místní proměnná metody má jedinečný index. Sekce lokální proměnné rámce zásobníku metody lze považovat za pole 32bitových slotů, z nichž každý je adresovatelný indexem pole. Místní proměnné typu long nebo double, které zabírají dva sloty, jsou označovány nižší z těchto dvou indexů slotů. Například dvojník, který zabírá sloty dva a tři, by byl označen indexem dvou.

Existuje několik operačních kódů, které posílají lokální proměnné int a float do zásobníku operandů. Jsou definovány některé operační kódy, které implicitně odkazují na běžně používanou pozici lokální proměnné. Například, iload_0 načte lokální proměnnou int na pozici nula. Ostatní lokální proměnné jsou do zásobníku vloženy operačním kódem, který přebírá index místní proměnné z prvního bajtu následujícího po operačním kódu. The iload instrukce je příkladem tohoto typu operačního kódu. Následující první bajt iload je interpretován jako nepodepsaný 8bitový index, který odkazuje na místní proměnnou.

Nepodepsané 8bitové indexy místních proměnných, například index, který následuje za iload instrukce, omezte počet lokálních proměnných v metodě na 256. Samostatná instrukce, tzv široký, může rozšířit 8bitový index o dalších 8 bitů. Tím se zvýší limit místní proměnné na 64 kilobajtů. The široký po operačním kódu následuje 8bitový operand. The široký operační kód a jeho operand může předcházet instrukci, například iload, který přebírá 8bitový nepodepsaný index místní proměnné. JVM kombinuje 8bitový operand z široký instrukce s 8bitovým operandem iload instrukce k získání 16bitového nepodepsaného indexu místní proměnné.

Opcodes, které posílají lokální proměnné int a float do zásobníku, jsou uvedeny v následující tabulce:

Operační kódOperand (s)Popis
iloadvindextlačí int z lokální proměnné polohy vindex
iload_0(žádný)tlačí int z pozice lokální proměnné nula
iload_1(žádný)tlačí int z pozice lokální proměnné jedna
iload_2(žádný)tlačí int z pozice lokální proměnné dvě
iload_3(žádný)tlačí int z pozice lokální proměnné tři
načístvindextlačí float z lokální proměnné polohy vindex
fload_0(žádný)tlačí float z lokální proměnné polohy nula
fload_1(žádný)tlačí float z lokální proměnné pozice jedna
fload_2(žádný)tlačí float z lokální proměnné pozice dvě
fload_3(žádný)tlačí float z lokální proměnné pozice tři

V následující tabulce jsou uvedeny pokyny, které do zásobníku posílají místní proměnné typu long a double. Tyto pokyny přesunou 64 bitů z místní proměnné sekce rámce zásobníku do sekce operandu.

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