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_0
instruksi, lalu mendorong dua ke tumpukan dengan iconst_2
. Setelah kedua bilangan bulat didorong ke tumpukan, imul
instruksi 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_0
petunjuk. 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 sipush
opcode 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 dload
mendorong 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_1
opcode 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.