Grunderna i Bytecode

Välkommen till en annan del av "Under The Hood." Den här kolumnen ger Java-utvecklare en glimt av vad som händer under deras Java-program. Den här månadens artikel tar en första titt på instruktionsuppsättningen bytecode på Java virtual machine (JVM). Artikeln täcker primitiva typer som drivs av bytecodes, bytecodes som konverterar mellan typer och bytecodes som fungerar på stacken. Senare artiklar kommer att diskutera andra medlemmar i bytecode-familjen.

Bytkodformatet

Bytecodes är maskinspråket för den virtuella Java-maskinen. När en JVM laddar en klassfil får den en ström av bytekoder för varje metod i klassen. Bytkodströmmarna lagras i metodområdet för JVM. Bytkoderna för en metod körs när den metoden anropas under programmet. De kan köras genom tolkning, just-in-time kompilering eller någon annan teknik som valdes av designern av en viss JVM.

En metods bytecode-ström är en sekvens av instruktioner för den virtuella Java-maskinen. Varje instruktion består av en byte- opkod följt av noll eller fler operander . Opkoden anger vilken åtgärd som ska vidtas. Om mer information krävs innan JVM kan vidta åtgärderna kodas den informationen i en eller flera operander som omedelbart följer opkoden.

Varje typ av opkod har ett minnesmärke. I den typiska monteringsspråksstilen kan strömmar av Java-bytkoder representeras av deras minnesmärken följt av alla operandvärden. Till exempel kan följande ström av bytecodes demonteras till mnemonics:

// Bytecode stream: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Demontering: 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 

Instruktionsuppsättningen för bytecode var utformad för att vara kompakt. Alla instruktioner, utom två som handlar om bordshoppning, är inriktade på bytegränser. Det totala antalet opkoder är tillräckligt litet så att opkoder endast upptar en byte. Detta hjälper till att minimera storleken på klassfiler som kan färdas över nätverk innan de laddas av en JVM. Det hjälper också till att hålla storleken på JVM-implementeringen liten.

All beräkning i JVM centrerar på stacken. Eftersom JVM inte har några register för lagring av abiträra värden, måste allt tryckas på stacken innan det kan användas i en beräkning. Bytecode-instruktioner fungerar därför främst på stacken. Till exempel, i ovanstående bytkodsekvens multipliceras en lokal variabel med två genom att först trycka den lokala variabeln på stacken med iload_0instruktionen, och sedan trycka två på stacken med iconst_2. Efter att båda heltalen har skjutits in på stacken, kommer imulinstruktionen effektivt att poppa de två heltalen från stacken, multiplicera dem och skjuter resultatet tillbaka till stacken. Resultatet poppas upp från stapelns överdel och lagras tillbaka till den lokala variabeln avistore_0instruktion. JVM designades som en stackbaserad maskin snarare än en registerbaserad maskin för att underlätta effektiv implementering på registerfattiga arkitekturer som Intel 486.

Primitiva typer

JVM stöder sju primitiva datatyper. Java-programmerare kan deklarera och använda variabler av dessa datatyper, och Java-bytkoder fungerar på dessa datatyper. De sju primitiva typerna listas i följande tabell:

Typ Definition
byte en-byte undertecknade tvås komplement heltal
short två-byte undertecknade tvås komplement heltal
int 4 byte signerade tvås komplement heltal
long 8-byte undertecknade tvås komplement heltal
float 4-byte IEEE 754 enkelprecisionsflottör
double 8-byte IEEE 754 dubbel-precision flottör
char 2-byte osignerad Unicode-karaktär

De primitiva typerna visas som operander i bytekodströmmar. Alla primitiva typer som upptar mer än 1 byte lagras i stor endianordning i bytekodströmmen, vilket betyder högre ordningsbyte före lägre ordningsbyte. Till exempel, för att trycka det konstanta värdet 256 (hex 0100) på stacken, skulle du använda sipushopkoden följt av en kort operand. Den korta visas i bytecode-strömmen, som visas nedan, som "01 00" eftersom JVM är stor-endian. Om JVM var liten endian, skulle den korta visas som "00 01".

// Bytecode stream: 17 01 00 // Demontering: sipush 256; // 17 01 00

Java-koder anger vanligtvis typen av deras operander. Detta gör att operander bara kan vara sig själva utan att behöva identifiera sin typ för JVM. Till exempel, i stället för att ha en opkod som skjuter en lokal variabel till stacken, har JVM flera. Opkoder iload, lload, fload, och dloaddriva lokala variabler av typen int, long, flyta, och dubbla, respektive, på stacken.

Skjuter konstanter på stapeln

Många opkoder skjuter konstanter på stacken. Opkoder indikerar det konstanta värdet för att trycka på tre olika sätt. Det konstanta värdet är antingen implicit i själva opkoden, följer opkoden i bytekodströmmen som en operand eller tas från den konstanta poolen.

Vissa opcoder indikerar i sig en typ och ett konstant värde att trycka på. Till exempel iconst_1säger opcoden till JVM att trycka på heltal ett. Sådana bytekoder är definierade för vissa vanligt förekommande nummer av olika typer. Dessa instruktioner upptar endast 1 byte i bytecode-strömmen. De ökar effektiviteten för exekvering av bytecode och minskar storleken på bytecode-strömmar. Opkoderna som skjuter in och flyter visas i följande tabell:

Opcode Operand (er) Beskrivning
iconst_m1 (ingen) trycker int -1 på stacken
iconst_0 (ingen) trycker int 0 på stacken
iconst_1 (ingen) skjuter int 1 på stacken
iconst_2 (ingen) skjuter int 2 på stacken
iconst_3 (ingen) skjuter int 3 på stacken
iconst_4 (ingen) skjuter int 4 på stacken
iconst_5 (ingen) skjuter int 5 på stacken
fconst_0 (ingen) skjuter float 0 på stacken
fconst_1 (ingen) skjuter flottör 1 på stapeln
fconst_2 (ingen) skjuter flottör 2 på stapeln

Opkoderna som visas i föregående tabell skjuter in och flyter, vilka är 32-bitars värden. Varje plats på Java-stacken är 32 bitar bred. Varje gång ett int eller ett flottör trycks på stacken upptar det därför en plats.

Opkoderna som visas i nästa tabell skjuter längs och dubbel. Långa och dubbla värden upptar 64 bitar. Varje gång en lång eller dubbel trycks in på stacken upptar dess värde två platser på stacken. Opkoder som anger ett specifikt långt eller dubbelt värde att trycka visas i följande tabell:

Opcode Operand (er) Beskrivning
lconst_0 (ingen) skjuter långt 0 på stacken
lconst_1 (ingen) skjuter lång 1 på stacken
dconst_0 (ingen) skjuter dubbel 0 på stacken
dconst_1 (ingen) skjuter dubbel 1 på stacken

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) trycker int från lokal variabel position noll
iload_1 (ingen) trycker int från lokal variabel position en
iload_2 (ingen) trycker int från lokal variabel position två
iload_3 (ingen) trycker int från lokal variabel position tre
fload vindex skjuter float från lokal variabel position vindex
fload_0 (ingen) skjuter flyt från lokal variabel position noll
fload_1 (ingen) skjuter flyt från lokal variabel position en
fload_2 (ingen) skjuter flyt från lokal variabel position två
fload_3 (ingen) skjuter flyt från lokal variabel position tre

Nästa tabell visar instruktionerna som skjuter lokala variabler av typen lång och dubbel på stacken. Dessa instruktioner flyttar 64 bitar från den lokala variabla delen av stapelramen till operandavsnittet.