Conceptos básicos de bytecode

Bienvenido a otra entrega de "Under The Hood". Esta columna les da a los desarrolladores de Java una idea de lo que sucede debajo de sus programas Java en ejecución. El artículo de este mes da un vistazo inicial al conjunto de instrucciones de código de bytes de la máquina virtual Java (JVM). El artículo cubre los tipos primitivos operados por códigos de bytes, códigos de bytes que se convierten entre tipos y códigos de bytes que operan en la pila. Los artículos posteriores discutirán otros miembros de la familia de códigos de bytes.

El formato del código de bytes

Los códigos de bytes son el lenguaje de máquina de la máquina virtual Java. Cuando una JVM carga un archivo de clase, obtiene un flujo de códigos de bytes para cada método de la clase. Los flujos de códigos de bytes se almacenan en el área de métodos de la JVM. Los códigos de bytes de un método se ejecutan cuando ese método se invoca durante la ejecución del programa. Se pueden ejecutar por interpretación, compilación justo a tiempo o cualquier otra técnica que haya elegido el diseñador de una JVM en particular.

El flujo de código de bytes de un método es una secuencia de instrucciones para la máquina virtual Java. Cada instrucción consta de un código de operación de un byte seguido de cero o más operandos . El código de operación indica la acción a realizar. Si se requiere más información antes de que la JVM pueda realizar la acción, esa información se codifica en uno o más operandos que siguen inmediatamente al código de operación.

Cada tipo de código de operación tiene un mnemónico. En el estilo del lenguaje ensamblador típico, los flujos de códigos de bytes de Java se pueden representar por sus mnemónicos seguidos por cualquier valor de operando. Por ejemplo, la siguiente secuencia de códigos de bytes se puede desensamblar en mnemónicos:

// Flujo de código de bytes: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Desmontaje: 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 

El conjunto de instrucciones de código de bytes fue diseñado para ser compacto. Todas las instrucciones, excepto dos que se ocupan del salto de tabla, están alineadas en límites de bytes. El número total de códigos de operación es lo suficientemente pequeño como para que los códigos de operación ocupen solo un byte. Esto ayuda a minimizar el tamaño de los archivos de clase que pueden viajar a través de las redes antes de ser cargados por una JVM. También ayuda a mantener pequeño el tamaño de la implementación de JVM.

Todo el cálculo en la JVM se centra en la pila. Debido a que la JVM no tiene registros para almacenar valores arbitrarios, todo debe insertarse en la pila antes de que pueda usarse en un cálculo. Por lo tanto, las instrucciones de código de bytes operan principalmente en la pila. Por ejemplo, en la secuencia de código de bytes anterior, una variable local se multiplica por dos empujando primero la variable local en la pila con la iload_0instrucción, luego empujando dos en la pila con iconst_2. Después de que ambos enteros se hayan insertado en la pila, la imulinstrucción extrae los dos enteros de la pila, los multiplica y devuelve el resultado a la pila. El resultado se extrae de la parte superior de la pila y se almacena de nuevo en la variable local por elistore_0instrucción. La JVM se diseñó como una máquina basada en pilas en lugar de una máquina basada en registros para facilitar la implementación eficiente en arquitecturas con pocos registros como Intel 486.

Tipos primitivos

La JVM admite siete tipos de datos primitivos. Los programadores de Java pueden declarar y utilizar variables de estos tipos de datos, y los códigos de bytes de Java operan sobre estos tipos de datos. Los siete tipos primitivos se enumeran en la siguiente tabla:

Tipo Definición
byte entero de complemento a dos con signo de un byte
short entero de complemento a dos con signo de dos bytes
int Entero en complemento a dos con signo de 4 bytes
long Entero en complemento a dos con signo de 8 bytes
float Flotador de precisión simple IEEE 754 de 4 bytes
double Flotador de doble precisión IEEE 754 de 8 bytes
char Carácter Unicode sin firmar de 2 bytes

Los tipos primitivos aparecen como operandos en flujos de código de bytes. Todos los tipos primitivos que ocupan más de 1 byte se almacenan en orden big-endian en el flujo de código de bytes, lo que significa que los bytes de orden superior preceden a los bytes de orden inferior. Por ejemplo, para insertar el valor constante 256 (hexadecimal 0100) en la pila, usaría el sipushcódigo de operación seguido de un operando corto. El abreviado aparece en el flujo de código de bytes, que se muestra a continuación, como "01 00" porque la JVM es big-endian. Si la JVM fuera little-endian, el corto aparecería como "00 01".

// Secuencia de código de bytes: 17 01 00 // Desmontaje: sipush 256; // 17 01 00

Los códigos de operación de Java generalmente indican el tipo de sus operandos. Esto permite que los operandos sean ellos mismos, sin necesidad de identificar su tipo en la JVM. Por ejemplo, en lugar de tener un código de operación que empuja una variable local a la pila, la JVM tiene varias. Opcodes iload, lload, fload, y dloadempujan las variables locales de tipo int, long, float, y doble, respectivamente, en la pila.

Empujando constantes en la pila

Muchos códigos de operación empujan constantes a la pila. Los códigos de operación indican el valor constante a presionar de tres formas diferentes. El valor constante está implícito en el código de operación en sí, sigue al código de operación en el flujo de código de bytes como un operando o se toma del grupo constante.

Algunos códigos de operación por sí mismos indican un tipo y un valor constante para presionar. Por ejemplo, el iconst_1código de operación le dice a la JVM que introduzca el valor entero uno. Dichos códigos de bytes se definen para algunos números de varios tipos que se introducen comúnmente. Estas instrucciones ocupan solo 1 byte en el flujo de código de bytes. Aumentan la eficiencia de la ejecución de códigos de bytes y reducen el tamaño de los flujos de códigos de bytes. Los códigos de operación que empujan entrantes y flotantes se muestran en la siguiente tabla:

Código de operación Operando (s) Descripción
iconst_m1 (ninguna) empuja int -1 en la pila
iconst_0 (ninguna) empuja int 0 en la pila
iconst_1 (ninguna) empuja int 1 en la pila
iconst_2 (ninguna) empuja int 2 en la pila
iconst_3 (ninguna) empuja int 3 en la pila
iconst_4 (ninguna) empuja int 4 en la pila
iconst_5 (ninguna) empuja int 5 en la pila
fconst_0 (ninguna) empuja el flotador 0 a la pila
fconst_1 (ninguna) empuja el flotador 1 hacia la pila
fconst_2 (ninguna) empuja el flotador 2 hacia la pila

The opcodes shown in the previous table push ints and floats, which are 32-bit values. Each slot on the Java stack is 32 bits wide. Therefore each time an int or float is pushed onto the stack, it occupies one slot.

The opcodes shown in the next table push longs and doubles. Long and double values occupy 64 bits. Each time a long or double is pushed onto the stack, its value occupies two slots on the stack. Opcodes that indicate a specific long or double value to push are shown in the following table:

Opcode Operand(s) Description
lconst_0 (none) pushes long 0 onto the stack
lconst_1 (none) pushes long 1 onto the stack
dconst_0 (none) pushes double 0 onto the stack
dconst_1 (none) pushes double 1 onto the stack

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) empuja int desde la posición cero de la variable local
iload_1 (ninguna) empuja int desde la posición de variable local uno
iload_2 (ninguna) empuja int desde la posición de variable local dos
iload_3 (ninguna) empuja int desde la posición de variable local tres
fload vindex empuja el flotador desde la posición variable local vindex
fload_0 (ninguna) empuja el flotador desde la posición cero de la variable local
fload_1 (ninguna) empuja el flotador desde la posición uno de la variable local
fload_2 (ninguna) empuja el flotador desde la posición dos de la variable local
fload_3 (ninguna) empuja el flotador desde la posición de variable local tres

La siguiente tabla muestra las instrucciones que empujan las variables locales de tipo largo y doble a la pila. Estas instrucciones mueven 64 bits desde la sección de variable local del marco de pila a la sección de operando.