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_0
instrucción, luego empujando dos en la pila con iconst_2
. Después de que ambos enteros se hayan insertado en la pila, la imul
instrucció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_0
instrucció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 sipush
có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 dload
empujan 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_1
có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.