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:
Typ | Definice |
---|---|
byte | jednobajtové celé číslo se znaménkem se dvěma znaménky |
krátký | dvoubajtové celé číslo se znaménkem se dvěma bajty |
int | 4-bajtové celé číslo se znaménkem se dvěma znaménky |
dlouho | Celé číslo komplementu se znaménkem 8 bajtů se znaménkem |
plovák | 4bajtový plovoucí plovoucí protokol IEEE 754 s jednou přesností |
dvojnásobek | 8bajtový float s dvojitou přesností IEEE 754 |
char | 2bajtový 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ód | Operand (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ód | Operand (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ód | Operand (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ód | Operand (s) | Popis |
---|---|---|
bipush | byte1 | expanduje byte1 (typ bytu) na int a posune jej do zásobníku |
sipush | byte1, byte2 | rozbalí 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ód | Operand (s) | Popis |
---|---|---|
ldc1 | indexbyte1 | vloží do zásobníku 32bitový záznam constant_pool určený indexbyte1 |
ldc2 | indexbyte1, indexbyte2 | vloží do zásobníku 32bitový záznam constant_pool určený indexbyte1, indexbyte2 |
ldc2w | indexbyte1, indexbyte2 | posune 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ód | Operand (s) | Popis |
---|---|---|
iload | vindex | tlačí 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číst | vindex | tlačí 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.