Bases du bytecode

Bienvenue dans un autre épisode de "Under The Hood". Cette colonne donne aux développeurs Java un aperçu de ce qui se passe sous leurs programmes Java en cours d'exécution. L'article de ce mois jette un premier regard sur le jeu d'instructions bytecode de la machine virtuelle Java (JVM). L'article couvre les types primitifs exploités par des bytecodes, des bytecodes qui convertissent entre les types et des bytecodes qui opèrent sur la pile. Les articles suivants traiteront d'autres membres de la famille de bytecode.

Le format bytecode

Les bytecodes sont le langage machine de la machine virtuelle Java. Lorsqu'une JVM charge un fichier de classe, elle obtient un flux de bytecodes pour chaque méthode de la classe. Les flux de bytecodes sont stockés dans la zone de méthode de la JVM. Les bytecodes d'une méthode sont exécutés lorsque cette méthode est invoquée au cours de l'exécution du programme. Ils peuvent être exécutés par interprétation, compilation juste à temps ou toute autre technique choisie par le concepteur d'une JVM particulière.

Le flux de bytecode d'une méthode est une séquence d'instructions pour la machine virtuelle Java. Chaque instruction se compose d'un opcode d' un octet suivi de zéro ou plusieurs opérandes . L'opcode indique l'action à entreprendre. Si plus d'informations sont nécessaires avant que la JVM puisse entreprendre l'action, ces informations sont codées en un ou plusieurs opérandes qui suivent immédiatement l'opcode.

Chaque type d'opcode a un mnémonique. Dans le style de langage d'assemblage typique, les flux de bytecodes Java peuvent être représentés par leurs mnémoniques suivis de toutes les valeurs d'opérande. Par exemple, le flux suivant de bytecodes peut être désassemblé en mnémoniques:

// Flux d'octets: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Démontage: 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 

Le jeu d'instructions bytecode a été conçu pour être compact. Toutes les instructions, à l'exception de deux qui traitent du saut de table, sont alignées sur des limites d'octets. Le nombre total d'opcodes est suffisamment petit pour que les opcodes n'occupent qu'un octet. Cela permet de minimiser la taille des fichiers de classe susceptibles de circuler sur les réseaux avant d'être chargés par une machine virtuelle Java. Cela permet également de réduire la taille de l'implémentation JVM.

Tous les calculs de la JVM sont centrés sur la pile. Étant donné que la JVM n'a pas de registres pour stocker des valeurs abitrary, tout doit être poussé sur la pile avant de pouvoir être utilisé dans un calcul. Les instructions de bytecode opèrent donc principalement sur la pile. Par exemple, dans la séquence de bytecode ci-dessus, une variable locale est multipliée par deux en poussant d'abord la variable locale sur la pile avec l' iload_0instruction, puis en poussant deux sur la pile avec iconst_2. Une fois que les deux entiers ont été poussés sur la pile, l' imulinstruction fait sortir les deux entiers de la pile, les multiplie et repousse le résultat sur la pile. Le résultat est sorti du haut de la pile et stocké dans la variable locale par leistore_0instruction. La JVM a été conçue comme une machine basée sur la pile plutôt que comme une machine basée sur des registres pour faciliter une mise en œuvre efficace sur des architectures pauvres en registres telles que l'Intel 486.

Types primitifs

La JVM prend en charge sept types de données primitifs. Les programmeurs Java peuvent déclarer et utiliser des variables de ces types de données, et les bytecodes Java fonctionnent sur ces types de données. Les sept types primitifs sont répertoriés dans le tableau suivant:

Type Définition
byte Entier complément à deux signé sur un octet
short Entier complément à deux signé sur deux octets
int Entier complémentaire à deux signé de 4 octets
long Entier complémentaire à deux signé de 8 octets
float Flotteur simple précision IEEE 754 4 octets
double Flotteur double précision IEEE 754 8 octets
char Caractère Unicode non signé de 2 octets

Les types primitifs apparaissent sous forme d'opérandes dans les flux de bytecode. Tous les types primitifs qui occupent plus d'un octet sont stockés dans l'ordre big-endian dans le flux de bytecode, ce qui signifie que les octets d'ordre supérieur précèdent les octets d'ordre inférieur. Par exemple, pour pousser la valeur constante 256 (hex 0100) sur la pile, vous utiliseriez l' sipushopcode suivi d'un opérande court. Le court apparaît dans le flux de bytecode, illustré ci-dessous, comme "01 00" car la JVM est big-endian. Si le JVM était petit-boutiste, le court apparaîtrait comme "00 01".

// Flux de bytecode: 17 01 00 // Dissassembly: sipush 256; // 17 01 00

Les opcodes Java indiquent généralement le type de leurs opérandes. Cela permet aux opérandes d'être simplement eux-mêmes, sans avoir besoin d'identifier leur type auprès de la JVM. Par exemple, au lieu d'avoir un opcode qui pousse une variable locale sur la pile, la JVM en a plusieurs. Opcodes iload, lload, floadet dloadpousser les variables locales de type int, long, float et double, respectivement, sur la pile.

Pousser des constantes sur la pile

De nombreux opcodes poussent des constantes sur la pile. Les opcodes indiquent la valeur constante à pousser de trois manières différentes. La valeur constante est soit implicite dans l'opcode lui-même, suit l'opcode dans le flux de bytecode en tant qu'opérande, soit est extraite du pool de constantes.

Certains opcodes indiquent eux-mêmes un type et une valeur constante à pousser. Par exemple, l' iconst_1opcode indique à la JVM de pousser la valeur entière un. De tels codes d'octets sont définis pour certains nombres communément poussés de différents types. Ces instructions n'occupent qu'un octet dans le flux de bytecode. Ils augmentent l'efficacité de l'exécution du bytecode et réduisent la taille des flux de bytecode. Les opcodes qui poussent ints et floats sont indiqués dans le tableau suivant:

Opcode Opérande (s) La description
iconst_m1 (aucun) pousse int -1 sur la pile
iconst_0 (aucun) pousse int 0 sur la pile
iconst_1 (aucun) pousse int 1 sur la pile
iconst_2 (aucun) pousse int 2 sur la pile
iconst_3 (aucun) pousse int 3 sur la pile
iconst_4 (aucun) pousse int 4 sur la pile
iconst_5 (aucun) pousse int 5 sur la pile
fconst_0 (aucun) pousse le flotteur 0 sur la pile
fconst_1 (aucun) pousse le flotteur 1 sur la pile
fconst_2 (aucun) pousse le flotteur 2 sur la pile

Les opcodes montrés dans le tableau précédent poussent des entiers et des flottants, qui sont des valeurs 32 bits. Chaque emplacement de la pile Java a une largeur de 32 bits. Par conséquent, chaque fois qu'un int ou un float est poussé sur la pile, il occupe un emplacement.

Les opcodes indiqués dans le tableau suivant poussent les longs et les doubles. Les valeurs longues et doubles occupent 64 bits. Chaque fois qu'un long ou double est poussé sur la pile, sa valeur occupe deux emplacements sur la pile. Les opcodes qui indiquent une valeur longue ou double spécifique à pousser sont indiqués dans le tableau suivant:

Opcode Opérande (s) La description
lconst_0 (aucun) pousse le long 0 sur la pile
lconst_1 (aucun) pousse le long 1 sur la pile
dconst_0 (aucun) pousse le double 0 sur la pile
dconst_1 (aucun) pousse le double 1 sur la pile

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) pousse int à partir de la position zéro de la variable locale
iload_1 (aucun) pousse int depuis la position de variable locale un
iload_2 (aucun) pousse int depuis la position de variable locale deux
iload_3 (aucun) pousse int depuis la position de variable locale trois
fload vindex pousse le flotteur de la position variable locale vindex
fload_0 (aucun) pousse le flotteur à partir de la position zéro de la variable locale
fload_1 (aucun) pousse le flotteur de la position variable locale un
fload_2 (aucun) pousse le flotteur de la position variable locale deux
fload_3 (aucun) pousse le flotteur de la position variable locale trois

Le tableau suivant montre les instructions qui poussent les variables locales de type long et double sur la pile. Ces instructions déplacent 64 bits de la section de variable locale de la trame de pile à la section d'opérande.