lunes, 15 de noviembre de 2010

Organización de la JVM

Para comenzar, la demora en las últimas entradas se debió a dos libros, de más interesantes, con los que me topé: Programming for the Java virtual machine, de Joshua Engel (1999) y Modern compiler implementation in Java, Second Edition de Andrew W. Appel (2002). Luego de dedicar un tiempo a su lectura, espero poder organizar de mejor manera los temas a exponer en esta secuencia.

Una máquina de pila
Vale la pena iniciar aclarando que la JVM, a diferencia por ejemplo de la arquitectura x86, sigue el concepto de las máquinas basadas en pilas (stack machines). Citando al libro del dragón:
En una máquina basada en pila, las operaciones se realizan metiendo operandos en una pila y después llevando a cabo las operaciones con los operandos en la parte superior de la pila. (...) Estas máquinas desaparecieron casi por completo, ya que se creía que la organización de la pila era demasiado limitante y requería demasiadas operaciones de intercambio y copiado. No obstante, las arquitecturas basadas en pila revivieron con la introducción de la Máquina virtual de Java (JVM).
El hecho de que la estructura de la JVM no sea diseñado hacia ninguna arquitectura en particular provee una visión más abstracta de la memoria. En lugar de dar acceso al nivel de bits y bytes de la memoria, la JVM trata la memoria como una colleción de objetos.

Esto permite un mejor control sobre qué programas pueden acceder a qué partes de la memoria. Los desarrolladores de la JVM crearon un conjunto de reglas que deben aplicar a todos los programas que se ejecuten y que aseguran a los usuarios que los programas no intentarán dañar el sistema. El algoritmo que lleva a cabo todo este control recibe el nombre de verificación y se ejecuta antes que el mismo programa por seguridad.

 Organización de la JVM
Conceptualmente la JVM se divide en cuatro espacios diferentes:
  • Class area: que almacena las constantes y el código.
  • Java stack: que mantiene registro de qué invocaciones a métodos se han hecho y la data asociada a cada método. 
  • Heap: donde se almacenan los objetos.
  • Native method stack: consultar aquí.
Nos compete únicamente la información relacionada las primeras dos áreas.

Class area
Esta área almacena las clases que se cargan en el sistema. Las implementaciones de los métodos se mantienen en un method area y las constantes en una runtime constant pool. Para cada clase se almacena propiedades como:
  • Superclass
  • Lista de interfaces
  • Lista de campos (fields)
  • Lista de métodos y de constantes
La runtime constant pool es una representación de la tabla constant_pool en un archivo class (detalles en entradas posteriores) que contiene varios tipos de constantes, desde literales numéricas que se conocen durante la compilación hasta referencias a métodos y campos que deben resolverse durante la ejecución.

La runtime constant pool para cada clase se asigna de la method area y se construye cuando la clase es creada por la JVM

Java stack
Cada vez que se invoca un método, un nuevo espacio de datos llamado stack frame es creado. Colectivamente, el conjunto de stack frames se les llama java stack y existe uno de estos por cada thread de la JVM.

El frame hasta arriba de la java stack muestra el lugar actual de ejecución y se llama el frame activo. Cuando un método termina su ejecución y retorna, el frame activo desaparece y el frame por debajo de ese vuelve a ser activo.

Cada stack frame tiene una pila de operandos, un arreglo de variables locales, una referencia a la runtime constant pool de la clase a la que pertenece y un puntero a la instrucción que se está ejecutando (PC), que se encuentra en la method area. Es de notar que todos los threads de la JVM comparten la method area, aunque cada uno mantenga su propia pila de frames, es decir que un frame solo puede ser referenciado por un thread. Estos frames se utilizan para almacenar datos y resultados parciales, retornar valores de métodos y disparar las excepciones.

El tamaño del arreglo de variables locales y la pila de operandos para un frame deben determinarse en tiempo de compilación y proveerse junto con el código para el método asociado a cada frame. Por lo mismo, el tamaño del frame como estructura de datos puede ser variable para cada método.
  

Arreglo de variables locales
El tamaño de este arreglo para cada frame debe proveerse en la representación binaria de una clase o interfaz, además del código asociado con el frame. Una sola localidad puede almacenar valores de tipo boolean, byte, char, short, int, float, reference o returnAddress (punteros a opcodes de instrucciones JVM, utilizados por las instrucciones de bifurcación a subrutinas). Un par de variables locales pueden almacenar un valor de tipo long o double. En este punto el lector habrá notado que nos referimos a tipos dentro de la JVM. Esta es otra característica que diferencia a la JVM de otras arquitecturas: las instrucciones (con dos excepciones) estan asociadas a tipos particulares de datos.

Las posiciones en este arreglo están indexadas, siendo 0 la posición de la primera variable local. Un valor de tipo long o double ocupa dos posiciones consecutivas, aunque solo puede ser direccionado usando el valor más pequeño (esto, entre otras cosas, lo garantiza el algoritmo de verificación).

Durante la invocación a un método, los parametros se reciben en este arreglo. En el caso particular de la invocación a un método de instancia (el único de interés aquí) la variable local 0 recibe siempre una referencia del objeto sobre el que se está invocando el método (this en el lenguaje Java). 

Pilas de operandos (operand stacks)
Cada frame contiene una pila cuyo tamaño máximo debe determinarse en tiempo de compilación, similar al arreglo de variables locales.

La pila de operandos esta vacía cuando el frame que la contiene se crea. La JVM provee instrucciones para cargar constantes o variables locales o de instancia en este stack. Otras instrucciones toman operandos de esta pila, operan sobre ellos y regresan el resultado a la pila. Esta pila también se utiliza para preparar parámetros que deban ser pasados a métodos, y recibir los valores de retorno de los métodos.

Cada entrada en esta pila puede contener un valor de cualquier tipo de la JVM, incluyendo long o double.

Es importante observar que la estructura del Java stack dentro de la JVM implementa hasta cierto punto y simplifica las nociones que se tratan en el capítulo 7.2 'Asignación de espacio en la pila' del libro del dragón: en concreto el tema de los registros de activación por cada invocación a un método. El que la JVM se encargue de crear y manipular frames por cada invocación a método, además de las estructuras internas a cada frame que permiten almacenar y trabajar con los datos locales, implica un gran cambio con respecto a arquitecturas en donde la memoria se ve como una gran arreglo de bytes direccionables.

No hay comentarios:

Publicar un comentario