พื้นฐาน Bytecode

ยินดีต้อนรับสู่อีกงวดของ "Under The Hood" คอลัมน์นี้ช่วยให้นักพัฒนา Java มองเห็นสิ่งที่เกิดขึ้นภายใต้โปรแกรม Java ที่ทำงานอยู่ บทความประจำเดือนนี้จะดูเบื้องต้นเกี่ยวกับชุดคำสั่ง bytecode ของ Java virtual machine (JVM) บทความนี้ครอบคลุมประเภทดั้งเดิมที่ดำเนินการโดย bytecodes, bytecodes ที่แปลงระหว่างชนิดและ bytecodes ที่ทำงานบนสแต็ก บทความต่อไปจะกล่าวถึงสมาชิกคนอื่น ๆ ในตระกูล bytecode

รูปแบบ bytecode

Bytecodes เป็นภาษาเครื่องของเครื่องเสมือน Java เมื่อ JVM โหลดไฟล์คลาสจะได้รับหนึ่งสตรีมของ bytecodes สำหรับแต่ละวิธีในคลาส สตรีม bytecodes ถูกเก็บไว้ในพื้นที่วิธีการของ JVM bytecodes สำหรับเมธอดจะดำเนินการเมื่อมีการเรียกใช้เมธอดนั้นในระหว่างการรันโปรแกรม พวกเขาสามารถดำเนินการได้โดย intepretation การรวบรวมแบบทันเวลาหรือเทคนิคอื่น ๆ ที่ได้รับเลือกโดยผู้ออกแบบของ JVM โดยเฉพาะ

bytecode stream ของเมธอดคือลำดับของคำสั่งสำหรับเครื่องเสมือน Java การเรียนการสอนแต่ละประกอบด้วยหนึ่งไบต์opcodeตามด้วยศูนย์หรือมากกว่าตัวถูกดำเนินการ opcode ระบุการดำเนินการที่ต้องดำเนินการ หากต้องการข้อมูลเพิ่มเติมก่อนที่ JVM จะสามารถดำเนินการได้ข้อมูลนั้นจะถูกเข้ารหัสเป็นตัวถูกดำเนินการอย่างน้อยหนึ่งรายการซึ่งเป็นไปตาม opcode ทันที

opcode แต่ละประเภทมีตัวช่วยในการจำ ในรูปแบบภาษาแอสเซมบลีทั่วไปสตรีมของโค้ด Java bytecodes สามารถแสดงได้ด้วยตัวช่วยจำตามด้วยค่าตัวถูกดำเนินการใด ๆ ตัวอย่างเช่นสตรีมของ bytecodes ต่อไปนี้สามารถถอดประกอบเป็นตัวช่วยจำ:

// สตรีม 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 

ชุดคำสั่ง bytecode ได้รับการออกแบบให้มีขนาดกะทัดรัด คำแนะนำทั้งหมดยกเว้นสองข้อที่เกี่ยวข้องกับการกระโดดโต๊ะจะจัดแนวตามขอบเขตไบต์ จำนวน opcodes ทั้งหมดมีขนาดเล็กเพียงพอเพื่อให้ opcodes ใช้งานได้เพียงหนึ่งไบต์ ซึ่งช่วยลดขนาดไฟล์คลาสที่อาจเดินทางข้ามเครือข่ายก่อนที่ JVM จะโหลด นอกจากนี้ยังช่วยให้ขนาดของการติดตั้ง JVM มีขนาดเล็ก

การคำนวณทั้งหมดในศูนย์ JVM บนสแต็ก เนื่องจาก JVM ไม่มีรีจิสเตอร์สำหรับจัดเก็บค่า Abitrary ทุกอย่างจึงต้องถูกพุชลงบนสแต็กก่อนจึงจะสามารถใช้ในการคำนวณได้ คำสั่ง Bytecode จึงทำงานบนสแต็กเป็นหลัก ยกตัวอย่างเช่นในลำดับ bytecode ข้างต้นตัวแปรท้องถิ่นคูณสองเป็นครั้งแรกโดยการผลักดันตัวแปรท้องถิ่นบนสแต็คที่มีการเรียนการสอนแล้วผลักดันทั้งสองลงบนสแต็คที่มีiload_0 iconst_2หลังจากที่จำนวนเต็มทั้งสองถูกผลักลงบนสแต็กแล้วimulคำสั่งจะดึงจำนวนเต็มสองจำนวนออกจากสแต็กอย่างมีประสิทธิภาพคูณและส่งผลลัพธ์กลับไปที่สแต็ก ผลลัพธ์จะโผล่ออกมาจากด้านบนของสแต็กและเก็บกลับไปที่ตัวแปรโลคัลโดยไฟล์istore_0คำแนะนำ. JVM ได้รับการออกแบบให้เป็นเครื่องที่ใช้สแต็กแทนที่จะเป็นเครื่องที่ใช้การลงทะเบียนเพื่ออำนวยความสะดวกในการใช้งานที่มีประสิทธิภาพบนสถาปัตยกรรมที่มีการลงทะเบียนไม่ดีเช่น Intel 486

ประเภทดั้งเดิม

JVM รองรับข้อมูลพื้นฐานเจ็ดประเภท โปรแกรมเมอร์ Java สามารถประกาศและใช้ตัวแปรของชนิดข้อมูลเหล่านี้และ Java bytecodes จะทำงานกับชนิดข้อมูลเหล่านี้ ประเภทดั้งเดิมเจ็ดประเภทแสดงอยู่ในตารางต่อไปนี้:

ประเภท คำจำกัดความ
byte หนึ่งไบต์ลงนามจำนวนเต็มเสริมของสอง
short สองไบต์ลงนามจำนวนเต็มเสริมของสอง
int 4 ไบต์ลงนามจำนวนเต็มเสริมของสอง
long 8 ไบต์ลงนามจำนวนเต็มเสริมของสอง
float 4 ไบต์ IEEE 754 single-precision float
double 8 ไบต์ IEEE 754 ลูกลอยที่มีความแม่นยำสองเท่า
char อักขระ Unicode ที่ไม่ได้ลงนาม 2 ไบต์

ชนิดดั้งเดิมจะปรากฏเป็นตัวถูกดำเนินการในสตรีม bytecode ประเภทดั้งเดิมทั้งหมดที่ครอบครองมากกว่า 1 ไบต์จะถูกจัดเก็บในลำดับบิ๊กเอนด์ในสตรีมไบต์โค้ดซึ่งหมายถึงไบต์ลำดับที่สูงกว่าก่อนไบต์ลำดับที่ต่ำกว่า ตัวอย่างเช่นในการพุชค่าคงที่ 256 (ฐานสิบหก 0100) ไปยังสแต็กคุณจะใช้sipushopcode ตามด้วยตัวถูกดำเนินการสั้น ๆ ข้อความสั้น ๆ จะปรากฏในสตรีม bytecode ที่แสดงด้านล่างเป็น "01 00" เนื่องจาก JVM เป็นแบบ Big-endian หาก JVM เป็นผู้มีส่วนได้ส่วนเสียน้อยตัวย่อจะปรากฏเป็น "00 01"

// สตรีม Bytecode: 17 01 00 // การแยกชิ้นส่วน: sipush 256; // 17 01 00 น

โดยทั่วไปแล้วรหัสจาวาจะระบุประเภทของตัวถูกดำเนินการ สิ่งนี้ช่วยให้ตัวถูกดำเนินการเป็นตัวของตัวเองโดยไม่จำเป็นต้องระบุประเภทของพวกเขาใน JVM ตัวอย่างเช่นแทนที่จะมี opcode เดียวที่ดันตัวแปรโลคัลไปยังสแต็ก JVM มีหลายตัว opcodes iload, lload, floadและdloadผลักดันตัวแปรท้องถิ่นชนิด int ยาวลอยและสองตามลำดับบนสแต็ค

ผลักค่าคงที่ไปยังสแตก

opcodes จำนวนมากจะผลักค่าคงที่ไปยังสแตก Opcodes ระบุค่าคงที่ที่จะผลักดันในสามวิธีที่แตกต่างกัน ค่าคงที่เป็นนัยใน opcode เองตามด้วย opcode ในสตรีม bytecode เป็นตัวถูกดำเนินการหรือนำมาจากกลุ่มค่าคงที่

opcodes บางตัวระบุประเภทและค่าคงที่ที่จะผลักดัน ตัวอย่างเช่นiconst_1opcode บอกให้ JVM พุชค่าจำนวนเต็มหนึ่ง รหัส bytecodes ดังกล่าวถูกกำหนดไว้สำหรับตัวเลขที่ผลักโดยทั่วไปบางประเภท คำแนะนำเหล่านี้ใช้เพียง 1 ไบต์ในสตรีม bytecode เพิ่มประสิทธิภาพของการดำเนินการ bytecode และลดขนาดของสตรีม bytecode opcodes ที่พุช ints และ float แสดงในตารางต่อไปนี้:

Opcode ตัวดำเนินการ คำอธิบาย
iconst_m1 (ไม่มี) ดัน int -1 ไปยังสแต็ก
iconst_0 (ไม่มี) ดัน int 0 ไปยังสแต็ก
iconst_1 (ไม่มี) ดัน int 1 ไปยังสแต็ก
iconst_2 (ไม่มี) ดัน int 2 ไปยังสแต็ก
iconst_3 (ไม่มี) ดัน int 3 ไปยังสแต็ก
iconst_4 (ไม่มี) ดัน int 4 ไปยังสแต็ก
iconst_5 (ไม่มี) ดัน int 5 ไปยังสแต็ก
fconst_0 (ไม่มี) ดันลอย 0 ไปที่สแต็ก
fconst_1 (ไม่มี) ดันลอย 1 ไปที่สแต็ก
fconst_2 (ไม่มี) ดันลอย 2 ไปที่สแต็ก

opcodes ที่แสดงในตารางก่อนหน้าจะพุช ints และ float ซึ่งเป็นค่า 32 บิต แต่ละสล็อตบน Java stack กว้าง 32 บิต ดังนั้นทุกครั้งที่ผลัก int หรือ float เข้าสู่สแต็กจะมีพื้นที่หนึ่งช่อง

opcodes ที่แสดงในตารางถัดไปดัน longs และ doubles ค่าแบบยาวและแบบคู่ใช้ 64 บิต แต่ละครั้งที่ดันยาวหรือสองครั้งลงบนสแต็กค่าของมันจะอยู่ที่สองช่องบนสแต็ก Opcodes ที่ระบุค่า long หรือ double ที่ต้องการจะพุชแสดงในตารางต่อไปนี้:

Opcode ตัวดำเนินการ คำอธิบาย
lconst_0 (ไม่มี) ดัน 0 ยาวลงบนสแต็ก
lconst_1 (ไม่มี) ดัน 1 ยาวลงบนสแต็ก
dconst_0 (ไม่มี) ดัน 0 สองเท่าไปยังสแต็ก
dconst_1 (ไม่มี) ดัน 1 คู่ลงบนสแต็ก

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) ผลัก int จากศูนย์ตำแหน่งตัวแปรท้องถิ่น
iload_1 (ไม่มี) ผลัก int จากตำแหน่งตัวแปรท้องถิ่นที่หนึ่ง
iload_2 (ไม่มี) ผลัก int จากตำแหน่งตัวแปรท้องถิ่น 2
iload_3 (ไม่มี) ผลัก int จากตำแหน่งตัวแปรท้องถิ่นที่สาม
fload น้ำวน ผลักลอยจากกระแสน้ำวนตำแหน่งตัวแปรท้องถิ่น
fload_0 (ไม่มี) ผลักลอยจากศูนย์ตำแหน่งตัวแปรท้องถิ่น
fload_1 (ไม่มี) ผลักลอยจากตำแหน่งตัวแปรท้องถิ่นที่หนึ่ง
fload_2 (ไม่มี) ผลักลอยจากตำแหน่งตัวแปรท้องถิ่น 2
fload_3 (ไม่มี) ดันลอยจากตำแหน่งตัวแปรท้องถิ่นสาม

ตารางถัดไปแสดงคำแนะนำที่พุชตัวแปรโลคัลชนิดยาวและเพิ่มเป็นสองเท่าบนสแต็ก คำแนะนำเหล่านี้ย้าย 64 บิตจากส่วนตัวแปรโลคัลของสแต็กเฟรมไปยังส่วนตัวถูกดำเนินการ