Bayt kodunun temelleri

"Under The Hood" un başka bir bölümüne hoş geldiniz. Bu sütun Java geliştiricilerine, çalışan Java programlarının altında neler olup bittiğine dair bir fikir verir. Bu ayın makalesi, Java sanal makinesinin (JVM) bayt kodu komut kümesine ilk bakışını ele alıyor. Makale, bayt kodlarla çalıştırılan ilkel türleri, türler arasında dönüştürülen bayt kodlarını ve yığın üzerinde çalışan bayt kodlarını kapsar. Sonraki makaleler, bayt kodu ailesinin diğer üyelerini tartışacaktır.

Bayt kodu biçimi

Bayt kodlar, Java sanal makinesinin makine dilidir. Bir JVM bir sınıf dosyası yüklediğinde, sınıftaki her yöntem için bir bayt kodu akışı alır. Bayt kodu akışları, JVM'nin yöntem alanında saklanır. Bir yöntemin bayt kodları, bu yöntem programı çalıştırırken çalıştırıldığında çalıştırılır. Yorumlama, tam zamanında derleme veya belirli bir JVM'nin tasarımcısı tarafından seçilen başka herhangi bir teknikle yürütülebilirler.

Bir yöntemin bayt kodu akışı, Java sanal makinesi için bir talimatlar dizisidir. Her komut, bir baytlık işlem kodu ve ardından sıfır veya daha fazla işlenen içerir . İşlem kodu, yapılacak eylemi gösterir. JVM'nin eylemi gerçekleştirebilmesi için daha fazla bilgi gerekiyorsa, bu bilgi işlem kodunu hemen takip eden bir veya daha fazla işlenen halinde kodlanır.

Her işlem kodu türünün bir anımsatıcı vardır. Tipik birleştirme dili stilinde, Java bayt kodlarının akışları, bunların anımsatıcıları ve ardından herhangi bir işlenen değeri ile temsil edilebilir. Örneğin, aşağıdaki bayt kodu akışı anımsatıcılara ayrılabilir:

// Bayt kodu akışı: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Demontaj: 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 

Bayt kodu komut seti kompakt olacak şekilde tasarlanmıştır. Tablo atlamayla ilgilenen iki komut dışındaki tüm talimatlar bayt sınırları üzerinde hizalanır. İşlem kodlarının toplam sayısı yeterince küçüktür, böylece işlem kodları yalnızca bir bayt kaplar. Bu, bir JVM tarafından yüklenmeden önce ağlar arasında dolaşan sınıf dosyalarının boyutunu en aza indirmeye yardımcı olur. Ayrıca JVM uygulamasının boyutunu küçük tutmaya yardımcı olur.

JVM'deki tüm hesaplamalar yığın üzerindedir. JVM, abitrary değerleri depolamak için kayıtlara sahip olmadığından, bir hesaplamada kullanılmadan önce her şeyin yığına itilmesi gerekir. Bayt kodu komutları bu nedenle öncelikle yığın üzerinde çalışır. Örneğin, yukarıdaki bayt kodu dizisinde bir yerel değişken, önce yerel değişkeni iload_0komutla yığının üzerine iterek, ardından ikisini ile yığının üzerine iterek ikiyle çarpılır iconst_2. Her iki tamsayı da yığının üzerine itildikten sonra, imulkomut, iki tamsayıyı yığından etkili bir şekilde çıkarır, onları çoğaltır ve sonucu yığına geri iter. Sonuç yığının tepesinden çıkarılır ve yerel değişkene geri kaydedilir.istore_0talimat. JVM, Intel 486 gibi kaydı zayıf mimarilerde verimli uygulamayı kolaylaştırmak için kayıt tabanlı bir makine yerine yığın tabanlı bir makine olarak tasarlanmıştır.

İlkel türler

JVM, yedi ilkel veri türünü destekler. Java programcıları bu veri türlerinin değişkenlerini bildirebilir ve kullanabilir ve Java bayt kodları bu veri türleri üzerinde çalışır. Yedi ilkel tür aşağıdaki tabloda listelenmiştir:

Tür Tanım
byte bir baytlık iki tamamlayıcı tamsayıyı imzaladı
short iki baytlık iki tamamlayıcı tamsayı imzaladı
int 4 baytlık iki tamamlayıcı tamsayıyı imzaladı
long 8 baytlık iki tamamlayıcı tamsayıyı imzaladı
float 4 bayt IEEE 754 tek duyarlıklı kayan nokta
double 8 bayt IEEE 754 çift duyarlıklı kayan nokta
char 2 baytlık işaretsiz Unicode karakteri

İlkel türler, bayt kodu akışlarında işlenenler olarak görünür. 1 bayttan fazla kaplayan tüm ilkel türler, bayt kodu akışında büyük sırayla depolanır; bu, yüksek sıralı baytların düşük sıralı baytlardan önce geldiği anlamına gelir. Örneğin, 256 (onaltılık 0100) sabit değerini yığına itmek için, sipushişlem kodunu ve ardından kısa bir operand kullanırsınız. Kısa, aşağıda gösterilen bayt kodu akışında "01 00" olarak görünür, çünkü JVM büyüktür. JVM küçük bir endian olsaydı, kısa "00 01" olarak görünürdü.

// Bayt kodu akışı: 17 01 00 // Sökme: sipush 256; // 17 01 00

Java işlem kodları genellikle işlenenlerinin türünü belirtir. Bu, işlenenlerin JVM'ye türlerini tanımlamaya gerek kalmadan sadece kendileri olmalarına izin verir. Örneğin, yerel bir değişkeni yığına iten bir işlem koduna sahip olmak yerine, JVM'de birkaç tane vardır. Opcodes iload, lload, floadve dloadyığını üzerine sırasıyla int, uzun, şamandıra, ve çift, lokal değişkenler itin.

Sabitleri yığına itmek

Birçok işlem kodu, sabitleri yığına iter. İşlem kodları, üç farklı şekilde itilecek sabit değeri gösterir. Sabit değer, işlem kodunun kendisinde örtüktür, bayt kodu akışındaki işlem kodunu bir işlenen olarak izler veya sabit havuzdan alınır.

Bazı işlem kodları kendi başlarına, itilecek bir tür ve sabit değer belirtir. Örneğin, iconst_1işlem kodu JVM'ye bir tamsayı değerini itmesini söyler. Bu tür bayt kodları, çeşitli türlerde yaygın olarak itilen bazı sayılar için tanımlanır. Bu talimatlar, bayt kodu akışında yalnızca 1 bayt kaplar. Bayt kodu yürütme verimliliğini artırır ve bayt kodu akışlarının boyutunu azaltır. Girişleri ve yüzenleri iten işlem kodları aşağıdaki tabloda gösterilmiştir:

İşlem kodu Operand (lar) Açıklama
iconst_m1 (Yok) int -1'i yığına iter
iconst_0 (Yok) int 0'ı yığına iter
iconst_1 (Yok) int 1'i yığının üzerine iter
iconst_2 (Yok) int 2'yi yığına iter
iconst_3 (Yok) int 3'ü yığına iter
iconst_4 (Yok) int 4'ü yığına iter
iconst_5 (Yok) int 5'i yığının üzerine iter
fconst_0 (Yok) float 0'ı yığının üzerine iter
fconst_1 (Yok) şamandırayı 1 yığının üzerine iter
fconst_2 (Yok) float 2'yi yığının üzerine iter

Önceki tabloda gösterilen işlem kodları, 32 bitlik değerler olan girişleri ve kayan değerleri iter. Java yığınındaki her yuva 32 bit genişliğindedir. Bu nedenle, bir int veya float yığının üzerine her itildiğinde, bir yuvayı kaplar.

Bir sonraki tabloda gösterilen işlem kodları, uzun ve iki katına çıkar. Uzun ve çift değerler 64 bit kaplar. Yığına her uzun veya ikili itildiğinde, değeri yığında iki yuva kaplar. Belirli bir uzun veya çift değeri gösteren işlem kodları aşağıdaki tabloda gösterilmektedir:

İşlem kodu Operand (lar) Açıklama
lconst_0 (Yok) uzun 0'ı yığının üzerine iter
lconst_1 (Yok) uzun 1'i yığının üzerine iter
dconst_0 (Yok) yığına çift 0 iter
dconst_1 (Yok) çift ​​1'i yığının üzerine iter

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 yerel değişken konumu sıfırdan iter
iload_1 (Yok) int yerel değişken bir konumdan iter
iload_2 (Yok) int yerel değişken konumu iki'den iter
iload_3 (Yok) int yerel değişken konum üçten iter
fload vindex float'ı yerel değişken konumundan iter vindex
fload_0 (Yok) float'ı yerel değişken konumu sıfırdan iter
fload_1 (Yok) float'ı yerel değişken bir pozisyondan iter
fload_2 (Yok) ikinci yerel değişken konumundan float iter
fload_3 (Yok) float'ı yerel değişken pozisyon üçten iter

Sonraki tablo, long ve double türündeki yerel değişkenleri yığına iten talimatları gösterir. Bu komutlar 64 biti yığın çerçevesinin yerel değişken bölümünden işlenen bölümüne taşır.