Asas kod bytec

Selamat datang ke ansuran lain "Under The Hood." Lajur ini memberikan pengembang Java sekilas tentang apa yang sedang terjadi di bawah program Java mereka yang sedang berjalan. Artikel bulan ini melihat awal set arahan bytecode mesin maya Java (JVM). Artikel ini merangkumi jenis primitif yang dikendalikan oleh kod bytek, kod bytek yang menukar antara jenis, dan kod bytek yang beroperasi di timbunan. Artikel seterusnya akan membincangkan ahli keluarga bytecode yang lain.

Format kod bytec

Bytecodes adalah bahasa mesin mesin maya Java. Apabila JVM memuat fail kelas, ia mendapat satu aliran kod bytek untuk setiap kaedah dalam kelas. Aliran kod byte disimpan di kawasan kaedah JVM. Kod bytes untuk kaedah dijalankan apabila kaedah tersebut digunakan semasa menjalankan program. Mereka dapat dilaksanakan dengan intepretasi, kompilasi tepat waktu, atau teknik lain yang dipilih oleh pereka JVM tertentu.

Aliran bytecode kaedah adalah urutan arahan untuk mesin maya Java. Setiap arahan terdiri daripada opcode satu-byte diikuti oleh operan sifar atau lebih . Opcode menunjukkan tindakan yang perlu diambil. Sekiranya lebih banyak maklumat diperlukan sebelum JVM dapat mengambil tindakan, maklumat tersebut dikodkan ke dalam satu atau lebih operan yang segera mengikuti opcode.

Setiap jenis opcode mempunyai mnemonik. Dalam gaya bahasa perhimpunan khas, aliran kod bytava Java dapat diwakili oleh mnemonik mereka diikuti oleh nilai operan apa pun. Sebagai contoh, aliran bykod berikut boleh dibongkar menjadi mnemonik:

// Strim bytecode: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Pembongkaran: 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 arahan bytecode dirancang agar ringkas. Semua arahan, kecuali dua yang berkaitan dengan melompat meja, diselaraskan pada batas bait. Jumlah opkod cukup kecil sehingga opkod hanya menempati satu bait. Ini membantu meminimumkan ukuran fail kelas yang mungkin bergerak di seluruh rangkaian sebelum dimuat oleh JVM. Ini juga membantu menjaga ukuran pelaksanaan JVM kecil.

Semua pengiraan di JVM berpusat pada timbunan. Kerana JVM tidak memiliki daftar untuk menyimpan nilai abitrari, semuanya harus didorong ke tumpukan sebelum dapat digunakan dalam perhitungan. Oleh itu, arahan Bytecode beroperasi terutamanya pada timbunan. Sebagai contoh, dalam urutan bytecode di atas pemboleh ubah tempatan didarabkan dengan dua dengan terlebih dahulu menolak pemboleh ubah tempatan ke tumpukan dengan iload_0arahan, kemudian mendorong dua ke timbunan dengan iconst_2. Setelah kedua-dua bilangan bulat didorong ke tumpukan, imularahan itu secara efektif mengeluarkan dua bilangan bulat dari timbunan, mengalikannya, dan mendorong hasilnya kembali ke tumpukan. Hasilnya muncul dari bahagian atas timbunan dan disimpan kembali ke pemboleh ubah tempatan olehistore_0arahan. JVM direka sebagai mesin berasaskan timbunan dan bukan mesin berasaskan daftar untuk memudahkan pelaksanaan yang cekap pada seni bina yang kurang daftar seperti Intel 486.

Jenis primitif

JVM menyokong tujuh jenis data primitif. Pengaturcara Java dapat menyatakan dan menggunakan pemboleh ubah dari jenis data ini, dan bytecode Java beroperasi pada jenis data ini. Tujuh jenis primitif disenaraikan dalam jadual berikut:

Jenis Definisi
byte satu-bait menandatangani bilangan bulat pelengkap dua
short two-byte menandatangani integer pelengkap dua
int 4-bait menandatangani bilangan bulat pelengkap dua
long 8-byte menandatangani bilangan bulat pelengkap dua
float Float ketepatan tunggal 4-byte IEEE 754
double Float ketepatan berkembar 8-byte IEEE 754
char Watak Unicode 2-bait yang tidak ditandatangani

Jenis primitif muncul sebagai operan dalam aliran kod bytec. Semua jenis primitif yang menempati lebih daripada 1 bait disimpan dalam urutan endian besar dalam aliran bytecode, yang bermaksud bait tertib lebih tinggi mendahului bait tertib rendah. Sebagai contoh, untuk mendorong nilai tetap 256 (hex 0100) ke timbunan, anda akan menggunakan sipushopcode diikuti dengan operasi pendek. Pendek muncul di aliran bytecode, ditunjukkan di bawah, sebagai "01 00" kerana JVM adalah endian besar. Sekiranya JVM kecil, orang pendek akan muncul sebagai "00 01".

// Aliran kod bytec: 17 01 00 // Pembongkaran: sipush 256; // 17 01 00

Opkod Java umumnya menunjukkan jenis operan mereka. Ini membolehkan operan menjadi diri mereka sendiri, tanpa perlu mengenal pasti jenisnya kepada JVM. Sebagai contoh, daripada mempunyai satu opcode yang mendorong pemboleh ubah tempatan ke tumpukan, JVM mempunyai beberapa. Opcodes iload, lload, fload, dan dloadmenolak pembolehubah tempatan jenis int, panjang, apungan, dan dua, masing-masing, ke dalam tindanan.

Menolak pemalar ke timbunan

Banyak opkod mendorong pemalar ke timbunan. Opkod menunjukkan nilai berterusan untuk mendorong dalam tiga cara yang berbeza. Nilai pemalar sama ada tersirat dalam opcode itu sendiri, mengikuti opcode dalam aliran bytecode sebagai operan, atau diambil dari kumpulan tetap.

Beberapa opkod dengan sendirinya menunjukkan jenis dan nilai tetap untuk mendorong. Sebagai contoh, iconst_1opcode memberitahu JVM untuk menolak satu nilai integer. Bytecodes seperti itu didefinisikan untuk sebilangan besar jenis yang didorong dari pelbagai jenis. Arahan ini hanya menggunakan 1 bait dalam aliran kod bytek. Mereka meningkatkan kecekapan pelaksanaan bytecode dan mengurangkan ukuran aliran bytecode. Opkod yang mendorong int dan apungan ditunjukkan dalam jadual berikut:

Kod Op Operan Penerangan
iconst_m1 (tiada) menolak int -1 ke timbunan
iconst_0 (tiada) menolak int 0 ke timbunan
iconst_1 (tiada) menolak int 1 ke timbunan
iconst_2 (tiada) menolak int 2 ke timbunan
iconst_3 (tiada) menolak int 3 ke timbunan
iconst_4 (tiada) menolak int 4 ke timbunan
iconst_5 (tiada) menolak int 5 ke timbunan
fconst_0 (tiada) menolak apungan 0 ke timbunan
fconst_1 (tiada) menolak apungan 1 ke timbunan
fconst_2 (tiada) menolak apungan 2 ke timbunan

Opkod yang ditunjukkan dalam jadual sebelumnya mendorong int dan apungan, yang merupakan nilai 32-bit. Setiap slot pada timbunan Java mempunyai lebar 32 bit. Oleh itu setiap kali int atau float ditolak ke stack, ia menempati satu slot.

Opkod yang ditunjukkan dalam jadual seterusnya mendorong panjang dan berganda. Nilai panjang dan berganda menempati 64 bit. Setiap kali panjang atau berganda didorong ke tumpukan, nilainya menempati dua slot pada timbunan. Opkod yang menunjukkan nilai panjang atau berganda tertentu untuk mendorong ditunjukkan dalam jadual berikut:

Kod Op Operan Penerangan
lconst_0 (tiada) menolak panjang 0 ke timbunan
lconst_1 (tiada) menolak panjang 1 ke timbunan
dconst_0 (tiada) menolak double 0 ke timbunan
dconst_1 (tiada) menolak double 1 ke timbunan

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) menolak int dari kedudukan pemboleh ubah tempatan sifar
iload_1 (tiada) menolak int dari kedudukan pemboleh ubah tempatan satu
iload_2 (tiada) menolak int dari kedudukan dua pemboleh ubah tempatan
iload_3 (tiada) menolak int dari kedudukan pemboleh ubah tempatan tiga
fload vindex tolakan tolakan dari kedudukan berubah tempatan vindex
fload_0 (tiada) tolakan tolak dari kedudukan pemboleh ubah tempatan sifar
fload_1 (tiada) tolakan tolakan dari kedudukan pemboleh ubah tempatan satu
fload_2 (tiada) tolakan tolakan dari kedudukan pemboleh ubah tempatan dua
fload_3 (tiada) tolakan tolakan dari kedudukan pemboleh ubah tempatan tiga

Jadual seterusnya menunjukkan arahan yang mendorong pemboleh ubah tempatan jenis panjang dan berganda ke timbunan. Arahan ini memindahkan 64 bit dari bahagian pemboleh ubah tempatan bingkai timbunan ke bahagian operasi.