Основи на байт кода

Добре дошли в друга част от „Под капака“. Тази колона дава на разработчиците на Java представа какво се случва под техните работещи Java програми. Статията от този месец първоначално разглежда набора от инструкции за байт кода на виртуалната машина Java (JVM). Статията обхваща примитивни типове, оперирани от байт кодове, байт кодове, които преобразуват между типове, и байт кодове, които работят на стека. Следващите статии ще обсъждат други членове на семейството на байт кодове.

Форматът на байт кода

Байт кодовете са машинният език на виртуалната машина Java. Когато JVM зарежда файл с клас, той получава един поток байт кодове за всеки метод в класа. Потоците на байт кодове се съхраняват в областта на метода на JVM. Байт кодовете за метод се изпълняват, когато този метод бъде извикан по време на изпълнението на програмата. Те могат да бъдат изпълнени чрез интерпретация, компилиране точно навреме или всяка друга техника, избрана от дизайнера на конкретна JVM.

Потокът от байт кодове на метода е последователност от инструкции за виртуалната машина Java. Всяка инструкция се състои от еднобайтов операционен код, последван от нула или повече операнди . Опкодът показва действието, което трябва да се предприеме. Ако се изисква повече информация, преди JVM да може да предприеме действие, тази информация се кодира в един или повече операнди, които следват непосредствено кода за действие.

Всеки тип opcode има мнемоника. В типичния стил на асемблерен език потоците от байт кодове на Java могат да бъдат представени чрез своите мнемоники, последвани от всякакви операндни стойности. Например следният поток от байт кодове може да бъде разглобен в мнемоника:

// Поток от байт кодове: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Демонтаж: 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 

Наборът от инструкции за байт код е проектиран да бъде компактен. Всички инструкции, с изключение на две, които се занимават с прескачане на таблици, са подравнени по границите на байта. Общият брой на опкодовете е достатъчно малък, така че опкодовете да заемат само един байт. Това помага да се сведе до минимум размерът на файловете на класа, които може да пътуват през мрежи, преди да бъдат заредени от JVM. Също така помага да се запази размерът на внедряването на JVM малък.

Всички изчисления в JVM се центрират в стека. Тъй като JVM няма регистри за съхраняване на произволни стойности, всичко трябва да бъде избутано в стека, преди да може да се използва при изчисление. Следователно инструкциите за байт кодове работят предимно на стека. Например, в горната поредица от байт кодове локалната променлива се умножава по две, като първо се избутва локалната променлива върху стека с iload_0инструкцията, след което се избутват две върху стека с iconst_2. След като двете цели числа са избутани в стека, imulинструкцията ефективно изважда двете цели числа от стека, умножава ги и изтласква резултата обратно в стека. Резултатът се изскача от горната част на стека и се съхранява обратно в локалната променлива отistore_0инструкция. JVM е проектиран като машина, базирана на стека, а не като машина, базирана на регистър, за да се улесни ефективното внедряване на бедни на регистър архитектури като Intel 486.

Примитивни типове

JVM поддържа седем примитивни типа данни. Програмистите на Java могат да декларират и използват променливи от тези типове данни, а байт кодовете на Java работят върху тези типове данни. Седемте примитивни типа са изброени в следната таблица:

Тип Определение
byte еднобайтово подписано цяло число на комплемента на две
short двубайтово подписано цяло число на комплемента на две
int 4-байтово подписано цяло число на комплемента на две
long 8-байтово подписано цяло число на комплемента на две
float 4-байтов IEEE 754 едноточен поплавък
double 8-байтов поплавък с двойна прецизност IEEE 754
char 2-байтов неподписан Unicode знак

Примитивните типове се появяват като операнди в потоци от байт кодове. Всички примитивни типове, които заемат повече от 1 байт, се съхраняват в порядък от голям край в байтовия поток, което означава, че байтовете от по-висок ред предшестват байтове от по-нисък ред. Например, за да изтласкате константната стойност 256 (hex 0100) върху стека, ще използвате sipushопкода, последван от кратък операнд. Късото се появява в потока от байтови кодове, показан по-долу, като "01 00", тъй като JVM е от голям край. Ако JVM беше малко ендиански, късото щеше да се появи като "00 01".

// Поток от байт кодове: 17 01 00 // Демонтаж: sipush 256; // 17 01 00

Операционните кодове на Java обикновено показват вида на техните операнди. Това позволява операндите просто да бъдат себе си, без да е необходимо да идентифицират техния тип в JVM. Например, вместо да има един opcode, който изтласква локална променлива в стека, JVM има няколко. Опкодовете iload, lload, fload, и dloadизтласкват местните променливи от тип Int, дълги, поплавък, и двойно, съответно, върху комина.

Бутане на константи върху стека

Много опкодове натискат константи върху стека. Опкодовете показват постоянната стойност, която трябва да се натисне по три различни начина. Стойността на константата е или имплицитна в самия opcode, следва опкода в потока от байт кодове като операнд, или е взета от константния пул.

Някои операционни кодове сами по себе си показват тип и постоянна стойност, която да се натисне. Например, iconst_1opcode казва на JVM да изтласка целочислена стойност едно. Такива байт кодове са дефинирани за някои често избутвани числа от различни типове. Тези инструкции заемат само 1 байт в потока от байт кодове. Те увеличават ефективността на изпълнението на байт кода и намаляват размера на потоците байт кодове. Опкодовете, които натискат ints и float, са показани в следната таблица:

Opcode Операнд (и) Описание
iconst_m1 (нито един) избутва int -1 в стека
iconst_0 (нито един) избутва int 0 в стека
iconst_1 (нито един) избутва int 1 върху стека
iconst_2 (нито един) избутва int 2 върху стека
iconst_3 (нито един) избутва int 3 върху стека
iconst_4 (нито един) избутва int 4 върху стека
iconst_5 (нито един) избутва int 5 върху стека
fconst_0 (нито един) избутва float 0 върху стека
fconst_1 (нито един) избутва float 1 върху стека
fconst_2 (нито един) избутва float 2 върху стека

Опкодовете, показани в предишната таблица, тласкат ints и floats, които са 32-битови стойности. Всеки слот на стека на Java е широк 32 бита. Следователно всеки път, когато int или float се избута върху стека, той заема един слот.

Опкодовете, показани в следващата таблица, удвояват и удвояват. Дългите и двойните стойности заемат 64 бита. Всеки път, когато long или double се избута върху стека, стойността му заема два слота в стека. Опкодовете, които показват конкретна дълга или двойна стойност за натискане, са показани в следната таблица:

Opcode Операнд (и) Описание
lconst_0 (нито един) натиска дълги 0 върху стека
lconst_1 (нито един) натиска дълго 1 върху стека
dconst_0 (нито един) избутва двойно 0 върху стека
dconst_1 (нито един) натиска двойно 1 върху стека

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) избутва int от локална променлива позиция нула
iload_1 (нито един) избутва int от локална променлива позиция едно
iload_2 (нито един) избутва int от локална променлива позиция две
iload_3 (нито един) избутва int от локална променлива позиция три
fload vindex избутва плувка от локална променлива позиция vindex
fload_0 (нито един) избутва float от локална променлива позиция нула
fload_1 (нито един) избутва float от локална променлива позиция едно
fload_2 (нито един) избутва float от локална променлива позиция две
fload_3 (нито един) избутва float от локална променлива позиция три

Следващата таблица показва инструкциите, които изтласкват локалните променливи от тип long и double на стека. Тези инструкции преместват 64 бита от локалната променлива секция на рамката на стека към секцията операнд.