Podstawy kodu bajtowego

Witamy w kolejnej odsłonie „Under The Hood”. Ta kolumna daje programistom Java wgląd w to, co dzieje się pod ich uruchomionymi programami Java. Artykuł z tego miesiąca poświęcony jest wstępnemu przeglądowi zestawu instrukcji kodu bajtowego wirtualnej maszyny języka Java (JVM). Artykuł obejmuje typy pierwotne obsługiwane przez kody bajtowe, kody bajtowe konwertowane między typami oraz kody bajtowe działające na stosie. W kolejnych artykułach omówimy innych członków rodziny kodów bajtowych.

Format kodu bajtowego

Kody bajtowe to język maszynowy wirtualnej maszyny Java. Kiedy maszyna JVM ładuje plik klasy, pobiera jeden strumień kodów bajtowych dla każdej metody w klasie. Strumienie kodów bajtowych są przechowywane w obszarze metod maszyny JVM. Kody bajtowe metody są wykonywane, gdy ta metoda jest wywoływana w trakcie wykonywania programu. Można je wykonać poprzez interpretację, kompilację just-in-time lub dowolną inną techniką wybraną przez projektanta konkretnej maszyny JVM.

Strumień kodu bajtowego metody to sekwencja instrukcji dla wirtualnej maszyny języka Java. Każda instrukcja składa się z jednobajtowego kodu operacji, po którym następuje zero lub więcej operandów . Kod operacji wskazuje działanie, jakie należy podjąć. Jeśli przed podjęciem działania przez maszynę JVM wymagane jest więcej informacji, informacje te są kodowane w co najmniej jednym operandzie, który następuje bezpośrednio po kodzie operacyjnym.

Każdy typ kodu operacji ma mnemonik. W typowym stylu asemblera, strumienie kodów bajtowych Javy mogą być reprezentowane przez ich mnemoniki, po których następują dowolne wartości operandów. Na przykład następujący strumień kodów bajtowych można rozłożyć na mnemoniki:

// Strumień kodu bajtowego: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Demontaż: 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 

Zestaw instrukcji kodu bajtowego został zaprojektowany jako zwarty. Wszystkie instrukcje, z wyjątkiem dwóch, które dotyczą przeskakiwania tabeli, są wyrównane do granic bajtów. Całkowita liczba rozkazów jest na tyle mała, że ​​opkody zajmują tylko jeden bajt. Pomaga to zminimalizować rozmiar plików klas, które mogą podróżować przez sieci, zanim zostaną załadowane przez maszynę JVM. Pomaga również utrzymać niewielki rozmiar implementacji maszyny JVM.

Wszystkie obliczenia w JVM koncentrują się na stosie. Ponieważ maszyna JVM nie ma rejestrów do przechowywania wartości abitrary, wszystko musi zostać umieszczone na stosie, zanim będzie można je wykorzystać w obliczeniach. Dlatego instrukcje w kodzie bajtowym działają głównie na stosie. Na przykład w powyższej sekwencji kodu bajtowego zmienna lokalna jest mnożona przez dwa, najpierw umieszczając zmienną lokalną na stosie z iload_0instrukcją, a następnie umieszczając dwie na stosie za pomocą iconst_2. Po umieszczeniu obu liczb całkowitych na stosie, imulinstrukcja skutecznie usuwa dwie liczby ze stosu, mnoży je i odkłada wynik z powrotem na stos. Wynik jest zdejmowany ze szczytu stosu i zapisywany z powrotem w zmiennej lokalnej przezistore_0instrukcja. JVM została zaprojektowana jako maszyna oparta na stosie, a nie jako maszyna oparta na rejestrach, aby ułatwić wydajną implementację na architekturach o niskiej liczbie rejestrów, takich jak Intel 486.

Typy prymitywne

JVM obsługuje siedem pierwotnych typów danych. Programiści języka Java mogą deklarować i wykorzystywać zmienne tych typów danych, a kody bajtowe języka Java działają na tych typach danych. W poniższej tabeli wymieniono siedem typów pierwotnych:

Rodzaj Definicja
byte jednobajtowa liczba całkowita dopełnienia do dwóch ze znakiem
short dwubajtowa liczba całkowita dopełnienia do dwóch ze znakiem
int 4-bajtowa liczba całkowita uzupełniająca do dwóch ze znakiem
long 8-bajtowa liczba całkowita uzupełniająca do dwóch ze znakiem
float 4-bajtowe zmiennoprzecinkowe pojedynczej precyzji IEEE 754
double 8-bajtowy zmiennoprzecinkowy IEEE 754 o podwójnej precyzji
char 2-bajtowy znak Unicode bez znaku

Typy pierwotne pojawiają się jako operandy w strumieniach kodu bajtowego. Wszystkie typy pierwotne, które zajmują więcej niż 1 bajt, są przechowywane w kolejności big-endian w strumieniu kodu bajtowego, co oznacza, że ​​bajty wyższego rzędu poprzedzają bajty niższego rzędu. Na przykład, aby umieścić stałą wartość 256 (szesnastkowo 0100) na stosie, należy użyć sipushkodu operacji, po którym następuje krótki operand. Skrót pojawia się w strumieniu kodu bajtowego, pokazanym poniżej, jako „01 00”, ponieważ maszyna JVM jest typu big-endian. Gdyby maszyna JVM była typu little-endian, skrót byłby wyświetlany jako „00 01”.

// Strumień kodu bajtowego: 17 01 00 // Dissassembly: sipush 256; // 17 01 00

Kody operacyjne Java zazwyczaj wskazują typ ich operandów. Dzięki temu operandy mogą być sobą, bez konieczności identyfikowania ich typu w JVM. Na przykład zamiast jednego kodu operacji, który wypycha zmienną lokalną na stos, maszyna JVM ma ich kilka. Rozkazy iload, lload, fload, i dloadnacisnąć lokalne zmienne typu int, długie, float, double i, odpowiednio, na stos.

Umieszczanie stałych na stosie

Wiele rozkazów umieszcza stałe na stosie. Kody operacyjne wskazują stałą wartość do naciśnięcia na trzy różne sposoby. Stała wartość jest albo niejawna w samym kodzie operacyjnym, występuje po kodzie operacyjnym w strumieniu kodu bajtowego jako operand lub jest pobierana z puli stałej.

Niektóre rozkazy same w sobie wskazują typ i stałą wartość do przekazania. Na przykład iconst_1kod operacji informuje maszynę JVM, aby przekazała liczbę całkowitą jeden. Takie kody bajtowe są zdefiniowane dla niektórych powszechnie używanych liczb różnych typów. Te instrukcje zajmują tylko 1 bajt w strumieniu kodu bajtowego. Zwiększają wydajność wykonywania kodu bajtowego i zmniejszają rozmiar strumieni kodu bajtowego. W poniższej tabeli przedstawiono kody operacyjne, które wypychają wartości typu int i float:

Kod operacji Operand (y) Opis
iconst_m1 (Żaden) wypycha int -1 na stos
iconst_0 (Żaden) wypycha int 0 na stos
iconst_1 (Żaden) wypycha int 1 na stos
iconst_2 (Żaden) wypycha int 2 na stos
iconst_3 (Żaden) wypycha int 3 na stos
iconst_4 (Żaden) wypycha int 4 na stos
iconst_5 (Żaden) wypycha int 5 na stos
fconst_0 (Żaden) wypycha float 0 na stos
fconst_1 (Żaden) wypycha pływak 1 na stos
fconst_2 (Żaden) wypycha pływak 2 na stos

Kody operacyjne pokazane w poprzedniej tabeli wypychają wartości typu int i float, które są wartościami 32-bitowymi. Każdy slot na stosie Java ma 32 bity. Dlatego za każdym razem, gdy int lub float trafia na stos, zajmuje jedno miejsce.

Rozkazy pokazane w następnej tabeli push długie i podwójne. Długie i podwójne wartości zajmują 64 bity. Za każdym razem, gdy long lub double jest umieszczany na stosie, jego wartość zajmuje dwa pola na stosie. Kody, które wskazują konkretną długą lub podwójną wartość do przekazania, przedstawiono w poniższej tabeli:

Kod operacji Operand (y) Opis
lconst_0 (Żaden) umieszcza długie 0 na stosie
lconst_1 (Żaden) wypycha długie 1 na stos
dconst_0 (Żaden) umieszcza podwójne 0 na stosie
dconst_1 (Żaden) umieszcza podwójną 1 na stosie

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) wypycha int z lokalnej zmiennej pozycji zero
iload_1 (Żaden) wypycha int z pozycji pierwszej zmiennej lokalnej
iload_2 (Żaden) wypycha int z pozycji drugiej zmiennej lokalnej
iload_3 (Żaden) wypycha int z pozycji trzeciej zmiennej lokalnej
fload vindex wypycha float z lokalnej zmiennej pozycji vindex
fload_0 (Żaden) wypycha liczbę zmiennoprzecinkową z pozycji zerowej lokalnej zmiennej
fload_1 (Żaden) wypycha float z pozycji pierwszej zmiennej lokalnej
fload_2 (Żaden) wypycha pływak z pozycji drugiej zmiennej lokalnej
fload_3 (Żaden) wypycha pływak z pozycji trzeciej zmiennej lokalnej

Następna tabela przedstawia instrukcje, które umieszczają na stosie zmienne lokalne typu long i double. Te instrukcje przenoszą 64 bity z lokalnej sekcji zmiennych ramki stosu do sekcji argumentów.