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_0
uputom, a zatim gurne dvije na stog s iconst_2
. Nakon što su obje cjelobrojne vrijednosti gurnute na stog, imul
uputa 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_0
uputa. 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 sipush
opcode 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
, fload
i dload
gurnuti 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_1
opcode 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.