Osnove bytecode-a

Dobrodošli u još jedan dio "Under the Hood". Ovaj stupac daje programerima Java uvid u to što se događa ispod njihovih pokrenutih Java programa. Ovomjesečni članak uvodno razmatra skup naredbi bajt-koda Java virtualnog stroja (JVM). Članak pokriva primitivne tipove kojima upravljaju bajtkodovi, bajtkodovi koji pretvaraju između tipova i bajtkodovi koji djeluju na stogu. U sljedećim člancima raspravljat će se o ostalim članovima obitelji bajt kodova.

Format bajtkoda

Bytecode su strojni jezik Java virtualnog stroja. Kada JVM učita datoteku klase, dobiva jedan tok bajt kodova za svaku metodu u klasi. Tok bajtkodova pohranjen je u području metode JVM-a. Bajtkodovi metode izvršavaju se kada se ta metoda poziva tijekom izvođenja programa. Mogu se izvršiti interpretacijom, pravodobnim sastavljanjem ili bilo kojom drugom tehnikom koju je odabrao dizajner određenog JVM-a.

Tok bytecode-a metode slijed je uputa za Java virtualni stroj. Svaka se uputa sastoji od jednobajtnog optičkog koda nakon kojeg slijedi nula ili više operanda . Optički kod označava radnju koju treba poduzeti. Ako je potrebno više podataka prije nego što JVM može poduzeti radnju, te se informacije kodiraju u jedan ili više operanda koji odmah slijede optički kod.

Svaka vrsta opkoda ima mnemotehniku. U tipičnom stilu asemblerskog jezika, tokovi Java bajt kodova mogu se predstaviti njihovim mnemotehnikama, a zatim bilo kojim vrijednostima operanda. Na primjer, sljedeći tok bajt kodova može se rastaviti u mnemotehniku:

// Tok bajtkoda: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Rastavljanje: iconst_0 // 03 istore_0 // 3b iinc 0, 1 // 84 00 01 iload_0 // 1a iconst_2 // 05 imul // 68 istore_0 // 3b goto -7 // a7 ff f9 

Set uputa za bajtkod je dizajniran da bude kompaktan. Sve su upute, osim dvije koje se bave preskakanjem stolova, poravnate na granicama bajtova. Ukupan broj opkodova dovoljno je mali da opkodovi zauzimaju samo jedan bajt. To pomaže minimizirati veličinu datoteka klase koje možda putuju mrežama prije nego što ih JVM učita. Također pomaže u održavanju veličine implementacije JVM-a malom.

Sva izračunavanja u JVM centriraju se na stog. Budući da JVM nema registre za pohranu abitorialnih vrijednosti, sve se mora gurnuti u stog prije nego što se može koristiti u izračunu. Upute za bytecode stoga djeluju prvenstveno na stogu. Na primjer, u gore navedenom redoslijedu bajt kodova lokalna varijabla množi se s dva tako što se lokalna varijabla najprije gurne na stog s iload_0uputom, a zatim gurne dvije na stog s iconst_2. Nakon što su obje cjelobrojne vrijednosti gurnute na stog, imuluputa učinkovito izbacuje dvije cijele brojeve iz stoga, množi ih i gura rezultat natrag u stog. Rezultat se iskače s vrha stoga i pohranjuje natrag u lokalnu varijabluistore_0uputa. JVM je dizajniran kao stroj zasnovan na stogu, a ne kao stroj zasnovan na registru kako bi se olakšala učinkovita primjena na arhitekturama siromašnim registrom, kao što je Intel 486.

Primitivni tipovi

JVM podržava sedam primitivnih vrsta podataka. Java programeri mogu deklarirati i koristiti varijable ovih tipova podataka, a Java bajtkodovi djeluju na te tipove podataka. U sljedećoj tablici navedeno je sedam primitivnih tipova:

Tip Definicija
byte jednobajtni potpisan komplementarni dvobroj
short dvobajtni potpisan komplementarni dvobroj
int Cjelobroj komplementa s dva bajta potpisana dvojka
long 8-bajtni znak komplementarnih dviju potpisanih dvojki
float 4-bajtni IEEE 754 jednostruki precizni plovak
double 8-bajtni IEEE 754 dvostruki precizni plovak
char 2-bajtni nepotpisani Unicode znak

Primitivni tipovi pojavljuju se kao operandi u streamovima bytecode-a. Svi primitivni tipovi koji zauzimaju više od 1 bajta pohranjuju se u big-end redoslijedu u stream bytecode-a, što znači da bajtovi višeg reda prethode bajtovima nižeg reda. Na primjer, za guranje konstantne vrijednosti 256 (hex 0100) na stog, upotrijebit ćete sipushopcode koji slijedi kratki operand. Kratko se pojavljuje u toku bajtkoda, prikazanom dolje, kao "01 00" jer je JVM big-endian. Da je JVM malo endijski, kratica bi se pojavila kao "00 01".

// Tok bajtkoda: 17 01 00 // Rastavljanje: sipush 256; // 17 01 00

Java operativni kodovi obično označavaju vrstu njihovih operanda. To omogućava da operandi budu samo oni sami, bez potrebe da identificiraju svoj tip JVM-u. Na primjer, umjesto da ima jedan opcode koji gura lokalnu varijablu na stog, JVM ih ima nekoliko. Opcodes iload, lload, floadi dloadgurnuti lokalne varijable tipa int, dugo, plutaju, i dvaput, odnosno, na stog.

Potiskivanje konstanti na stog

Mnogi opkodovi guraju konstante na stog. Opkodovi označavaju konstantnu vrijednost za potiskivanje na tri različita načina. Vrijednost konstante je ili implicitna u samom opcodeu, slijedi opcode u toku bytecodea kao operand ili je preuzeta iz spremišta konstanti.

Neki opkodovi sami po sebi označavaju vrstu i konstantnu vrijednost za potiskivanje. Na primjer, iconst_1opcode govori JVM-u da potisne cijelu vrijednost jedan. Takvi bajtkodovi definirani su za neke često potisnute brojeve različitih vrsta. Ove upute zauzimaju samo 1 bajt u toku bajtkoda. Povećavaju učinkovitost izvršavanja bajtkoda i smanjuju veličinu tokova bajtkoda. Opkodovi koji guraju intove i plutaju prikazani su u sljedećoj tablici:

Opcode Operand (i) Opis
iconst_m1 (nema) gura int -1 na stog
iconst_0 (nema) gura int 0 na stog
iconst_1 (nema) gura int 1 na stog
iconst_2 (nema) gura int 2 na stog
iconst_3 (nema) gura int 3 na stog
iconst_4 (nema) gura int 4 na stog
iconst_5 (nema) gura int 5 na stog
fconst_0 (nema) gura float 0 na stog
fconst_1 (nema) gura float 1 na stog
fconst_2 (nema) gura float 2 na stog

Opkodovi prikazani u prethodnoj tablici guraju intove i plutajuće vrijednosti, a to su 32-bitne vrijednosti. Svaki utor na Java stogu širok je 32 bita. Stoga svaki put kada se int ili float gurne na stog, zauzima jedan utor.

Opkodovi prikazani u sljedećoj tablici guraju se udvostručavaju. Duge i dvostruke vrijednosti zauzimaju 64 bita. Svaki put kada se long ili double gurne na stog, njegova vrijednost zauzima dva utora na stogu. Opkodovi koji označavaju određenu dugu ili dvostruku vrijednost za potiskivanje prikazani su u sljedećoj tablici:

Opcode Operand (i) Opis
lconst_0 (nema) gura dugo 0 na stog
lconst_1 (nema) gura dugo 1 na stog
dconst_0 (nema) gura dvostruko 0 na stog
dconst_1 (nema) gura double 1 na stog

One other opcode pushes an implicit constant value onto the stack. The aconst_null opcode, shown in the following table, pushes a null object reference onto the stack. The format of an object reference depends upon the JVM implementation. An object reference will somehow refer to a Java object on the garbage-collected heap. A null object reference indicates an object reference variable does not currently refer to any valid object. The aconst_null opcode is used in the process of assigning null to an object reference variable.

Opcode Operand(s) Description
aconst_null (none) pushes a null object reference onto the stack

Two opcodes indicate the constant to push with an operand that immediately follows the opcode. These opcodes, shown in the following table, are used to push integer constants that are within the valid range for byte or short types. The byte or short that follows the opcode is expanded to an int before it is pushed onto the stack, because every slot on the Java stack is 32 bits wide. Operations on bytes and shorts that have been pushed onto the stack are actually done on their int equivalents.

Opcode Operand(s) Description
bipush byte1 expands byte1 (a byte type) to an int and pushes it onto the stack
sipush byte1, byte2 expands byte1, byte2 (a short type) to an int and pushes it onto the stack

Three opcodes push constants from the constant pool. All constants associated with a class, such as final variables values, are stored in the class's constant pool. Opcodes that push constants from the constant pool have operands that indicate which constant to push by specifying a constant pool index. The Java virtual machine will look up the constant given the index, determine the constant's type, and push it onto the stack.

The constant pool index is an unsigned value that immediately follows the opcode in the bytecode stream. Opcodes lcd1 and lcd2 push a 32-bit item onto the stack, such as an int or float. The difference between lcd1 and lcd2 is that lcd1 can only refer to constant pool locations one through 255 because its index is just 1 byte. (Constant pool location zero is unused.) lcd2 has a 2-byte index, so it can refer to any constant pool location. lcd2w also has a 2-byte index, and it is used to refer to any constant pool location containing a long or double, which occupy 64 bits. The opcodes that push constants from the constant pool are shown in the following table:

Opcode Operand(s) Description
ldc1 indexbyte1 pushes 32-bit constant_pool entry specified by indexbyte1 onto the stack
ldc2 indexbyte1, indexbyte2 pushes 32-bit constant_pool entry specified by indexbyte1, indexbyte2 onto the stack
ldc2w indexbyte1, indexbyte2 pushes 64-bit constant_pool entry specified by indexbyte1, indexbyte2 onto the stack

Pushing local variables onto the stack

Local variables are stored in a special section of the stack frame. The stack frame is the portion of the stack being used by the currently executing method. Each stack frame consists of three sections -- the local variables, the execution environment, and the operand stack. Pushing a local variable onto the stack actually involves moving a value from the local variables section of the stack frame to the operand section. The operand section of the currently executing method is always the top of the stack, so pushing a value onto the operand section of the current stack frame is the same as pushing a value onto the top of the stack.

The Java stack is a last-in, first-out stack of 32-bit slots. Because each slot in the stack occupies 32 bits, all local variables occupy at least 32 bits. Local variables of type long and double, which are 64-bit quantities, occupy two slots on the stack. Local variables of type byte or short are stored as local variables of type int, but with a value that is valid for the smaller type. For example, an int local variable which represents a byte type will always contain a value valid for a byte (-128 <= value <= 127).

Each local variable of a method has a unique index. The local variable section of a method's stack frame can be thought of as an array of 32-bit slots, each one addressable by the array index. Local variables of type long or double, which occupy two slots, are referred to by the lower of the two slot indexes. For example, a double that occupies slots two and three would be referred to by an index of two.

Several opcodes exist that push int and float local variables onto the operand stack. Some opcodes are defined that implicitly refer to a commonly used local variable position. For example, iload_0 loads the int local variable at position zero. Other local variables are pushed onto the stack by an opcode that takes the local variable index from the first byte following the opcode. The iload instruction is an example of this type of opcode. The first byte following iload is interpreted as an unsigned 8-bit index that refers to a local variable.

Unsigned 8-bit local variable indexes, such as the one that follows the iload instruction, limit the number of local variables in a method to 256. A separate instruction, called wide, can extend an 8-bit index by another 8 bits. This raises the local variable limit to 64 kilobytes. The wide opcode is followed by an 8-bit operand. The wide opcode and its operand can precede an instruction, such as iload, that takes an 8-bit unsigned local variable index. The JVM combines the 8-bit operand of the wide instruction with the 8-bit operand of the iload instruction to yield a 16-bit unsigned local variable index.

The opcodes that push int and float local variables onto the stack are shown in the following table:

Opcode Operand(s) Description
iload vindex pushes int from local variable position vindex
iload_0 (none) potiskuje int iz položaja lokalne varijable nula
iload_1 (nema) potiskuje int iz lokalne varijable pozicija jedan
iload_2 (nema) potiskuje int iz lokalne varijable pozicije dva
iload_3 (nema) potiskuje int iz lokalne varijable pozicije tri
fload vindex gura plovak s lokalnog promjenjivog položaja vindex
fload_0 (nema) gura plutajuću vrijednost s lokalne varijable položaja nula
fload_1 (nema) gura plovak iz lokalne promjenjive pozicije jedan
fload_2 (nema) gura plovak s lokalne promjenjive pozicije dva
fload_3 (nema) gura plovak s lokalne promjenjive pozicije tri

Sljedeća tablica prikazuje upute koje guraju lokalne varijable tipa long i double na stog. Ove upute premještaju 64 bita iz odjeljka lokalne varijable okvira steka u odjeljak operanda.