Dasar-dasar bytecode

Selamat datang di bagian lain dari "Under The Hood." Kolom ini memberikan gambaran sekilas kepada pengembang Java tentang apa yang terjadi di bawah program Java mereka yang sedang berjalan. Artikel bulan ini membahas sekilas set instruksi bytecode dari mesin virtual Java (JVM). Artikel ini mencakup tipe primitif yang dioperasikan oleh bytecode, bytecode yang mengkonversi antar tipe, dan bytecode yang beroperasi pada stack. Artikel selanjutnya akan membahas anggota lain dari keluarga bytecode.

Format bytecode

Bytecode adalah bahasa mesin dari mesin virtual Java. Saat JVM memuat file kelas, JVM mendapat satu aliran bytecode untuk setiap metode di kelas tersebut. Aliran bytecode disimpan di area metode JVM. Bytecode untuk metode dijalankan ketika metode itu dipanggil selama menjalankan program. Mereka dapat dieksekusi dengan intepretation, just-in-time compiling, atau teknik lain yang dipilih oleh perancang JVM tertentu.

Aliran bytecode metode adalah urutan instruksi untuk mesin virtual Java. Setiap instruksi terdiri dari opcode satu byte diikuti oleh nol atau lebih operan . Opcode menunjukkan tindakan yang harus diambil. Jika informasi lebih lanjut diperlukan sebelum JVM dapat mengambil tindakan, informasi tersebut dikodekan menjadi satu atau lebih operand yang segera mengikuti opcode.

Setiap jenis opcode memiliki mnemonic. Dalam gaya bahasa assembly tipikal, aliran bytecode Java dapat direpresentasikan oleh mnemoniknya diikuti oleh nilai operan apa pun. Misalnya, aliran bytecode berikut dapat dibongkar menjadi mnemonik:

// Aliran Bytecode: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Disassembly: 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 instruksi bytecode dirancang agar kompak. Semua instruksi, kecuali dua yang berhubungan dengan lompatan tabel, disejajarkan pada batas byte. Jumlah total opcode cukup kecil sehingga opcode hanya menempati satu byte. Ini membantu meminimalkan ukuran file kelas yang mungkin berjalan melintasi jaringan sebelum dimuat oleh JVM. Ini juga membantu menjaga ukuran implementasi JVM tetap kecil.

Semua komputasi di JVM berpusat di stack. Karena JVM tidak memiliki register untuk menyimpan nilai acak, semuanya harus didorong ke tumpukan sebelum dapat digunakan dalam penghitungan. Oleh karena itu, instruksi bytecode beroperasi terutama di stack. Misalnya, dalam urutan bytecode di atas, variabel lokal dikalikan dua dengan terlebih dahulu mendorong variabel lokal ke tumpukan dengan iload_0instruksi, lalu mendorong dua ke tumpukan dengan iconst_2. Setelah kedua bilangan bulat didorong ke tumpukan, imulinstruksi secara efektif mengeluarkan dua bilangan bulat dari tumpukan, mengalikannya, dan mendorong hasilnya kembali ke tumpukan. Hasilnya dimunculkan dari bagian atas tumpukan dan disimpan kembali ke variabel lokal olehistore_0petunjuk. JVM didesain sebagai mesin berbasis stack daripada mesin berbasis register untuk memfasilitasi implementasi yang efisien pada arsitektur register-poor seperti Intel 486.

Tipe primitif

JVM mendukung tujuh tipe data primitif. Pemrogram Java dapat mendeklarasikan dan menggunakan variabel dari tipe data ini, dan bytecode Java beroperasi pada tipe data ini. Tujuh tipe primitif dicantumkan dalam tabel berikut:

Tipe Definisi
byte satu-byte menandatangani integer komplemen dua
short dua-byte menandatangani integer komplemen dua
int 4-byte menandatangani integer komplemen dua
long 8-byte menandatangani bilangan bulat komplemen dua
float Float presisi tunggal IEEE 754 4-byte
double Pelampung presisi ganda IEEE 754 8-byte
char 2-byte unsigned Unicode character

Tipe primitif muncul sebagai operan dalam aliran bytecode. Semua tipe primitif yang menempati lebih dari 1 byte disimpan dalam urutan big-endian dalam aliran bytecode, yang berarti byte tingkat tinggi mendahului byte urutan rendah. Misalnya, untuk mendorong nilai konstan 256 (hex 0100) ke tumpukan, Anda akan menggunakan sipushopcode diikuti dengan operan pendek. Pendek muncul di aliran bytecode, ditampilkan di bawah, sebagai "01 00" karena JVM adalah big-endian. Jika JVM adalah little-endian, pendeknya akan muncul sebagai "00 01".

// Aliran Bytecode: 17 01 00 // Pembongkaran: sipush 256; // 17 01 00

Opcode Java umumnya menunjukkan jenis operannya. Ini memungkinkan operand menjadi dirinya sendiri, tanpa perlu mengidentifikasi tipenya ke JVM. Misalnya, alih-alih memiliki satu opcode yang mendorong variabel lokal ke tumpukan, JVM memiliki beberapa. Opcodes iload, lload, fload, dan dloadmendorong variabel lokal tipe int, panjang, float, dan double, masing-masing, ke stack.

Mendorong konstanta ke tumpukan

Banyak opcode mendorong konstanta ke tumpukan. Opcode menunjukkan nilai konstan untuk didorong dalam tiga cara berbeda. Nilai konstanta tersirat dalam opcode itu sendiri, mengikuti opcode dalam aliran bytecode sebagai operan, atau diambil dari kumpulan konstan.

Beberapa opcode dengan sendirinya menunjukkan tipe dan nilai konstan untuk didorong. Misalnya, iconst_1opcode memberi tahu JVM untuk memasukkan nilai integer satu. Bytecode tersebut ditentukan untuk beberapa nomor yang umumnya didorong dari berbagai jenis. Instruksi ini hanya menempati 1 byte dalam aliran bytecode. Mereka meningkatkan efisiensi eksekusi bytecode dan mengurangi ukuran aliran bytecode. Opcode yang mendorong int dan float ditunjukkan pada tabel berikut:

Opcode Operand (s) Deskripsi
iconst_m1 (tidak ada) mendorong int -1 ke tumpukan
iconst_0 (tidak ada) mendorong int 0 ke stack
iconst_1 (tidak ada) mendorong int 1 ke tumpukan
iconst_2 (tidak ada) mendorong int 2 ke tumpukan
iconst_3 (tidak ada) mendorong int 3 ke tumpukan
iconst_4 (tidak ada) mendorong int 4 ke tumpukan
iconst_5 (tidak ada) mendorong int 5 ke stack
fconst_0 (tidak ada) mendorong float 0 ke tumpukan
fconst_1 (tidak ada) mendorong float 1 ke atas tumpukan
fconst_2 (tidak ada) mendorong float 2 ke atas tumpukan

Opcode yang ditampilkan di tabel sebelumnya mendorong int dan float, yang merupakan nilai 32-bit. Setiap slot pada tumpukan Java memiliki lebar 32 bit. Oleh karena itu, setiap kali int atau float didorong ke stack, ia menempati satu slot.

Opcode yang ditunjukkan pada tabel berikutnya mendorong long dan double. Nilai panjang dan ganda menempati 64 bit. Setiap kali long atau double didorong ke tumpukan, nilainya menempati dua slot di tumpukan. Opcode yang menunjukkan nilai panjang atau ganda tertentu untuk didorong ditampilkan dalam tabel berikut:

Opcode Operand (s) Deskripsi
lconst_0 (tidak ada) mendorong 0 panjang ke tumpukan
lconst_1 (tidak ada) mendorong panjang 1 ke atas tumpukan
dconst_0 (tidak ada) mendorong dua kali lipat 0 ke tumpukan
dconst_1 (tidak ada) mendorong dobel 1 ke tumpukan

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) mendorong int dari posisi variabel lokal nol
iload_1 (tidak ada) mendorong int dari posisi variabel lokal satu
iload_2 (tidak ada) mendorong int dari posisi variabel lokal dua
iload_3 (tidak ada) mendorong int dari posisi variabel lokal tiga
fload vindex mendorong float dari vindex posisi variabel lokal
fload_0 (tidak ada) mendorong float dari posisi variabel lokal nol
fload_1 (tidak ada) mendorong float dari posisi variabel lokal satu
fload_2 (tidak ada) mendorong float dari posisi variabel lokal dua
fload_3 (tidak ada) mendorong float dari posisi variabel lokal tiga

Tabel berikutnya menunjukkan instruksi yang mendorong variabel lokal bertipe long dan double ke stack. Instruksi ini memindahkan 64 bit dari bagian variabel lokal frame stack ke bagian operand.