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_0
arahan, kemudian mendorong dua ke timbunan dengan iconst_2
. Setelah kedua-dua bilangan bulat didorong ke tumpukan, imul
arahan 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_0
arahan. 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 sipush
opcode 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 dload
menolak 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_1
opcode 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.