Bytecode

Revisión del 18:37 28 jul 2019 de Carlos idict (discusión | contribuciones) (Texto reemplazado: «<div align="justify">» por «»)
(dif) ← Revisión anterior | Revisión actual (dif) | Revisión siguiente → (dif)
Bytecode
Información sobre la plantilla
Concepto:Formato binario de código ejecutable por la máquina virtual de java

Bytecode. Código intermedio más abstracto que el código máquina. Habitualmente es tratado como un archivo binario que contiene un programa ejecutable similar a un módulo objeto, que es un archivo binario producido por el compilador cuyo contenido es el código objeto o código máquina.

Introducción

JVM bytecode es el formato binario de código ejecutable por la máquina virtual de java. Algo así como el famoso lenguaje máquina "assembler", pero para la VM de java.

El bytecode recibe su nombre porque usualmente cada códigode operación tiene una longitud de un byte, si bien la longitud del código de las instrucciones varía. Cada instrucción tiene un código de operación entre 0 y 255 seguido de parámetros tales como los registros o las direcciones de memoria. Esta sería la descripción de un caso típico, si bien la especificación del bytecode depende ampliamente del lenguaje.

Como código intermedio, se trata de una forma de salida utilizada por los implementadores de lenguajes para reducir la dependencia respecto del hardware específico y facilitar la interpretación. Menos frecuentemente se utiliza el bytecode como código intermedio en un compilador. Algunos sistemas, llamados traductores dinámicos o compiladores just-in-time traducen el bytecode a código máquina inmediatamente antes de su ejecución para mejorar la velocidad de ejecución.

Los programas en bytecode suelen ser interpretados por un intérprete de bytecode (en general llamado máquina virtual, dado que es análogo a un ordenador). Su ventaja es su portabilidad: el mismo código binario puede ser ejecutado en diferentes plataformas y arquitecturas. Es la misma ventaja que presentan los lenguajes interpretados.

Sin embargo, como el bytecode es en general menos abstracto, más compacto y más orientado a la máquina que un programa pensado para su modificación por humanos, su rendimiento suele ser mejor que el de los lenguajes interpretados. A causa de esa mejora en el rendimiento, muchos lenguajes interpretados, de hecho, se compilan para convertirlos en bytecode y después son ejecutados por un intérprete de bytecode.

Entre esos lenguajes se encuentran Perl, Gambas, PHP y Python. El código Java se suele trasmitir como bytecode a la máquina receptora, que utiliza un compilador just-in-time para traducir el bytecode en código máquina antes de su ejecución.

Son asimismo interesantes los denominados p-Codes, similares a bytecodes pero cuyos códigos de operación pueden constar de más de un byte y pueden ser variables en tamaño, como los opcodes de muchas CPUs. Estos códigos trabajan a muy alto nivel, incluyendo instrucciones del estilo de «imprime esta cadena» o «borra la pantalla». Por ejemplo, BASICutiliza p-Codes.

OpCode's e Instrucciones

Este lenguaje (porque en definitiva es un lenguaje, si bien de bajo nivel y binario, es un lenguaje) consta básicamente de instrucciones, cada instrucción tiene un código que la identifica llamado opcode y opcionalmente parámetros. El opcode se expresa en forma de bytes, pero existe una convención de nombres cortos para poder hacer más legible el bytecode.

Entonces, veamos un ejemplo. El siguiente código que suma dos valores:

public void sumar() 
{
int a = 2;
int b = 3;
int resultado = a + b;
}

Se traduce al siguiente bytecode: iconst_2 istore 1 iconst_3 istore 2

iload 1 iload 2 iadd istore 3 return

maxstack 2 maxlocals 4

Acá vemos un par de cosas. Cada linea tiene una instrucción con su respectivo opcode en formato "legible". · iconst_X: declara una constante con el valor x y pone su valor en el stack · istore Y: guarda el valor del stack en una variable local identificada por Y o En nuestro caso se declaran las dos constantes 2, y 3 y se asignan a variables locales 1 y 2 respectivamente · iload: se utiliza para cargar en el stack el valor de una variable, en nuestro caso se carga el valor de 1 y 2. · iadd: luego esta operación especial sirve para sumar dos integers (de ahí su nombre que empieza con la i) o los operandos tienen que estar previamente seteados en el stack o esto además guarda el resultado en el stack · luego nuevamente istore 3 guarde el valor del stack en una nueva variable local 3. En nuestro ejemplo este valor es el resultado de la suma. · return no hace falta explicar esto. · maxstack & maxlocals:

Stack-Oriented model

Como vimos en el ejemplo anterior aparece la idea de stack. Esto es importante de al menos mencionar, la JVM y su bytecode está diseñada en base a un stack y no de registros. Como es por ejemplo el modelo de assembler. Este modelo está relacionado con la idea del modelo de objetos (llamadas a métodos).

En realidad es un tema de bastante bajo nivel, pero digamos que esto le permite a la VM poder ejecutar más eficientemente en procesadores que no tienen un conjunto de registros extendidos.

A nivel práctico esto quiere decir que la mayoría de las instrucciones operan sobre el stack, por ejemplo en nuestro caso la suma obtiene los operandos popeandolos del stack. Quiere decir que nos vamos a hartar de ver manipulación del stack cuando veamos bytecodes. Esto hace al bytecode un poco más dificil de leer y más verborrágico.

Hola Mundo (invokevirtual) Veamos como se traduce el famoso hola mundo de esto:

public class HelloWorld
{
public static void main(String[] args) 
{System.out.println("Hello, world!");}
}
A bytecode:
getstatic java/lang/System.out : Ljava/io/PrintStream;
ldc "Hello, world!"
invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
return

Donde:

  • gestatic: accede a la variable de instancia out de System y mete la referencia en el stack
  • ldc: apila en el stack el string "Hello, world!"
  • invokevirtual: invoca el método identificado por la interfaz PrintStream, y firma println(String):void consumiendo de la pila:

o el último elemento agregado (el string) como parámetro. o el anterior, como el receptor. Invoke* Existen varios opcode para la invocación de métodos:

  • invokevirtual
  • invokestatic
  • invokeinterface
  • invokespecial
  • invokedynamic (nuevo en java7)
  • invokevirtual

Como ya vimos en el ejemplo de "Hola Mundo", el invokevirtual se utiliza para invocar métodos haciéndo uso del mecanismo de dispatching dinámico para localizar y ejecutar la implementación del método de acuerdo a la clase concreta del receptor. Este dispatching se hace sobre la jerarquía de la clase.

Algo importante a tener en cuenta es que esta instrucción se utiliza para invocar métodos de instancia declarados en clases y no en interfaces. Para este último caso existe la instrucción invokeinterface. invokeinterface Bastante similar al invokevirtual se utiliza para invocar un método, declarado en una interface. De nuevo se ejecutará el dispatching dinámico sobre el receptor. Veamos un ejemplo: void test(Enumeration enum) {

boolean x = enum.hasMoreElements();
...

} En este caso Enumeration es una interface y estamos invocando el método hasMoreElements() Esto se traduce al siguiente bytecode: aload_1 ; agrega la variable local 1 (el argumento al método) en la pila invokeinterface java/util/Enumeration/hasMoreElements()Z 1 istore_2 ; guarda el resultado en la variable local 2 (la x) invokespecial

Se utiliza para invocar un método puntual, sin realizar dispatching dinámico. Es decir para invocar métodos "no-virtuales". En java decimos que todos los métodos son virtuales por default, porque generalmente el bytecode que escribimos se traduce a instrucciones invokevirtual y/o invokeinterface. En otros lenguajes como C# se da lo contrario y hay que especificar qué métodos queremos que sean virtuales. En fin, volviendo a java, el tema es que hay ciertas invocaciones especiales donde no hay dispatching. Estos son los casos: · invocaciones a constructores de la superclase: o super() o super(arg1, arg2) · invocaciones a métodos privados: ya que no se pueden redefinir en subclases, ni tampoco pueden estar sobrescribiendo métodos de la superclase. No hay dispatching. · invocaciones a métodos de la superclase: o Ej: super.toString() invokestatic Se utiliza para invocar métodos static. Obviamente no hay dispatching. invokedynamic

Es una instrucción nueva en la JVM versión 7, que muchos lenguajes están esperando, porque permitirá invocar un método en forma dinámica, es decir, con dispatching, pero sin necesidad de especificar la clase que lo declara y la firma del método. Esto va a dar el soporte necesario para lenguajes dinámicos que hoy en día igualmente se construyen sobre la JVM pero con muchas limitaciones o con "truquitos" para evadir la naturaleza estática y los checkeos de java. Muchas veces esto causa un pérdida considerable de performance de estos lenguajes o consumo de memoria.

Fuentes