11. intérprete de bytecodes. -...

36
11. Intérprete de bytecodes. En los primeros capítulos se han introducido conceptos básicos relacionados con la tecnología Java y en particular con la tecnología móvil J2ME. Uno de estos conceptos es la maquina virtual que es precisamente el objeto principal de estudio de este proyecto. De entre los distintos módulos que integran la KVM (maquina virtual de la plataforma J2ME) se han analizado dos de ellos. Por un lado se ha visto como representa internamente la maquina virtual los elementos Java y por otro l ado se ha estudiado la gestión que se hace de la memoria que emplea. En este capítulo se abordará uno de los módulos mas importantes de la KVM, el intérprete de bytecodes. El intérprete de bytecodes se puede ver como el motor de ejecución de la maquina virtual pues es el módulo que ejecuta las instrucciones que componen los programas Java. Dichas instrucciones se denominan bytecodes y son generados en el proceso de compilación de los archivos Java. De esta forma, el intérprete de bytecodes ejecuta de forma secuencial los bytecodes que forman los archivos de clases empleando para ello un registro denominado contador de programas. Dicho registro apunta al siguiente bytecode en ser ejecutado. La KVM dispone de un juego de bytecodes y cada uno de ellos ejecuta una serie de tareas de distinta naturaleza. Así por ejemplo el bytecode de salto condicional (una instrucción if) comprueba si se ha cumplido una condición y en caso de que así fuera incrementa el contador de programas el número de posiciones necesarias. 11.1. Introducción. Dentro de este capítulo vamos a tratar de explicar el funcionamiento y la solución que la KVM aporta a uno de los pilares de funcionamiento de los denominados lenguajes interpretados y es precisamente la interpretación del código que se ge nera tras el proceso de compilación. Comentaremos brevemente las diferencias existentes entre los lenguajes compilados e interpretados para tomar contacto con el contenido del capítulo sin profundizar en discusiones acerca de la diferencia de rendimiento entre ambos puesto que dicha discusión queda totalmente fuera del objetivo de esta documentación. También se introducirá el concepto de compilación Just-In-Time que es una técnica híbrida que combina compilación e interpretación en el propio terminal que ejecuta las aplicaciones Java. El intérprete de bytecodes de la maquina virtual se podría ver como el motor de ejecución de la misma, puesto que es el módulo que se encarga de ejecutar las instrucciones que el programador le indica en el código que genera. Así el intérprete

Upload: phungdang

Post on 24-Jan-2019

215 views

Category:

Documents


0 download

TRANSCRIPT

11. Intérprete de bytecodes.

En los primeros capítulos se han introducido conceptos básicos relacionados con la tecnología Java y en particular con la tecnología móvil J2ME. Uno de estos conceptos es la maquina virtual que es precisamente el objeto principal de estudio de este proyecto.

De entre los distintos módulos que integran la KVM (maquina virtual de la

plataforma J2ME) se han analizado dos de ellos. Por un lado se ha visto como representa internamente la maquina virtual los elementos Java y por otro lado se ha estudiado la gestión que se hace de la memoria que emplea.

En este capítulo se abordará uno de los módulos mas importantes de la KVM, el

intérprete de bytecodes. El intérprete de bytecodes se puede ver como el motor de ejecución de la maquina virtual pues es el módulo que ejecuta las instrucciones que componen los programas Java. Dichas instrucciones se denominan bytecodes y son generados en el proceso de compilación de los archivos Java.

De esta forma, el intérprete de bytecodes ejecuta de forma secuencial los

bytecodes que forman los archivos de clases empleando para ello un registro denominado contador de programas. Dicho registro apunta al siguiente bytecode en ser ejecutado.

La KVM dispone de un juego de bytecodes y cada uno de ellos ejecuta una serie

de tareas de distinta naturaleza. Así por ejemplo el bytecode de salto condicional (una instrucción if) comprueba si se ha cumplido una condición y en caso de que así fuera incrementa el contador de programas el número de posiciones necesarias.

11.1. Introducción.

Dentro de este capítulo vamos a tratar de explicar el funcionamiento y la solución que la KVM aporta a uno de los pilares de funcionamiento de los denominados lenguajes interpretados y es precisamente la interpretación del código que se genera tras el proceso de compilación.

Comentaremos brevemente las diferencias existentes entre los lenguajes

compilados e interpretados para tomar contacto con el contenido del capítulo sin profundizar en discusiones acerca de la diferencia de rendimiento entre ambos puesto que dicha discusión queda totalmente fuera del objetivo de esta documentación.

También se introducirá el concepto de compilación Just-In-Time que es una

técnica híbrida que combina compilación e interpretación en el propio terminal que ejecuta las aplicaciones Java.

El intérprete de bytecodes de la maquina virtual se podría ver como el motor de

ejecución de la misma, puesto que es el módulo que se encarga de ejecutar las instrucciones que el programador le indica en el código que genera. Así el intérprete

recorre secuencialmente la lista de bytecodes presentes en el archivo .class correspondiente al hilo activo en ese momento.

Pasado un cierto tiempo o si el hilo se detiene por un bytecode introducido por

el usuario al realizar el programa Java, se salvaguarda el entorno de ejecución y se pasa a ejecutar los bytecodes de otro hilo con su propio entorno de ejecución.

El entorno de ejecución lo forman una serie de registros de la maquina virtual.

Dichos registros son diferentes dependiendo del hilo que este ejecutando puesto que cada hilo ejecuta un subconjunto de bytecodes diferentes.

El intérprete de bytecodes en la KVM se ejecuta mediante un flujo principal que

recorre y ejecuta los bytecodes del hilo principal activo en ese momento. Superado un tiempo máximo de ejecución del hilo, se invoca al planificador de hilos para que el intérprete ejecute los bytecodes de otro de los hilos y ese otro hilo pase a estar activo.

En cada tiempo de ejecución de bytecodes, el algoritmo del intérprete va

ejecutando cada uno de los bytecodes. Para cada uno de ellos analiza el tipo del bytecode y actúa en consecuencia según este.

11.1.1. Lenguajes compilados e interpretados.

Un lenguaje compilado es término un tanto impreciso para referirse a un lenguaje de programación que típicamente se implementa mediante un compilador. Esto implica que una vez escrito el programa, éste se traduce a partir de su código fuente por medio de un compilador en un archivo ejecutable para una determinada plataforma (por ejemplo Solaris para Sparc, Windows NT para Intel, etc.).

Los lenguajes compilados son lenguajes de alto nivel en los que las instrucciones se traducen del lenguaje utilizado a código máquina para una ejecución rápida. Por el contrario un lenguaje interpretado es aquel en el que las instrucciones se traducen o interpretan una a una siendo típicamente unas 10 veces más lentos que los programas compilados.

Es teóricamente posible escribir un compilador o un intérprete para cualquier

lenguaje, sin embargo en algunos lenguajes una u otra implementación es más sencilla porque se diseñaron con una implementación en particular en mente.

Algunos entornos de programación incluyen los dos mecanismos, primero el código fuente se traduce a un código intermedio que luego se interpreta en una máquina virtual, pero que también puede compilarse justo antes de ejecutarse. La máquina virtual y los compiladores Just in Time de Java son un ejemplo de ello. Algunos ejemplos típicos de lenguajes compilados:

• Fortran • La familia de lenguajes de C, incluyendo C++ y Objective C pero no Java. • Ada, Pascal (incluyendo su dialecto Delphi) • Algol

Los lenguajes interpretados (o lenguajes de script) forman un subconjunto de

los lenguajes de programación, que incluye a aquellos lenguajes cuyos programas son habitualmente ejecutados en un intérprete en vez de compilados. Sin embargo, la definición de un lenguaje de programación es independiente de cómo se ejecuten los programas en él escritos, ya sea mediante una compilación previa o a través de un intérprete.

Figura 11.1: Lenguajes compilados vs interpretados.

11.1.2. Just in Time Compiler.

Las aplicaciones que se crean en grandes empresas deben ser más efectivas que eficientes; es decir, conseguir que el programa funcione y el trabajo salga adelante es más importante que el que lo haga eficientemente. Esto no es una crítica, es una realidad de la programación corporativa. Al ser un lenguaje más simple que cualquiera de los que ahora están en el cajón de los programadores, Java permite a éstos concentrarse en la mecánica de la aplicación, en vez de pasarse horas y horas incorporando APIs para el control de las ventanas, controlando minuciosamente la memoria, sincronizando los ficheros de cabecera y corrigiendo los agónicos mensajes del linker. Java tiene su propio toolkit para interfaces, maneja por sí mismo la memoria que utilice la aplicación, no permite ficheros de cabecera separados (en aplicaciones puramente Java) y solamente usa enlace dinámico.

Muchas de las implementaciones de Java actuales son puros intérpretes. Los byte-codes son interpretados por el sistema run-time de Java, la Máquina Virtual Java (JVM), sobre el ordenador del usuario. Aunque ya hay ciertos proveedores que ofrecen compiladores nativos Just-In-Time (JIT). Si la Máquina Virtual Java dispone de un compilador instalado, las secciones (clases) del byte-code de la aplicación se compilarán hacia la arquitectura nativa del ordenador del usuario. La integración de los compiladores JIT con la maquina virtual Java se puede representar mediante la siguiente figura:

Figura 11.2: Just-In-Time Compiler.

Los programas Java en ese momento rivalizarán con el rendimiento de

programas en C++. Los compiladores JIT no se utilizan en la forma tradicional de un compilador; los programadores no compilan y distribuyen binarios Java a los usuarios. La compilación JIT tiene lugar a partir del byte-code Java, en el sistema del usuario, como una parte (opcional) del entorno run-time local de Java.

Muchas veces, los programadores corporativos, ansiosos por exprimir al máximo la eficiencia de su aplicación, empiezan a hacerlo demasiado pronto en el ciclo de vida de la aplicación. Java permite algunas técnicas innovadoras de optimización. Por ejemplo, Java es inherentemente multithreaded, a la vez que ofrece posibilidades de multithread como la clase Thread y mecanismos muy sencillos de usar de sincronización; Java en sí utiliza threads. Los desarrolladores de compiladores inteligentes pueden utilizar esta característica de Java para lanzar un thread que compruebe la forma en que se está utilizando la aplicación. Más específicamente, este thread podría detectar qué métodos de una clase se están usando con más frecuencia e invocar a sucesivos niveles de optimización en tiempo de ejecución de la aplicación. Cuanto más tiempo esté corriendo la aplicación o el applet, los métodos estarán cada vez más optimizados (Guava de Softway es de este tipo).

Si un compilador JIT está embebido en el entorno run-time de Java, el programador no se preocupa de hacer que la aplicación se ejecute óptimamente. Siempre he pensado que en los Sistemas Operativos tendría que aplicarse esta filosofía; un optimizador progresivo es un paso más hacia esta idea.

Podemos ver la diferencia que existe entre el modo Java de interpretación:

Figura 11.3: Ejecución lenguaje interpretado.

Donde cada uno de los elementos son:

1. Java Virtual Machine (JVM) 2. Run-time bytecode interpretation 3. Java bytecode

Y el modo JIT:

Figura 11.4: modo JIT de ejecución.

Donde cada uno de los elementos son:

1. Java bytecode 2. Run-time JIT compilation 3. Native RISC instructions 4. Java Virtual Machine (JVM)

11.2. Entorno del lenguaje de programación Java.

El entorno del lenguaje de programación Java al ser un leguaje mixto entre un lenguaje puramente compilado y uno puramente interpretado presenta el siguiente entorno de ejecución:

Figura 11.5: Entorno del lenguaje Java.

Java es un lenguaje de programación creado por Sun Microsystems para poder funcionar en distintos tipos de procesadores. Es un lenguaje orientado a objetos. Su sintaxis es muy parecida a la de C o C++, e incorpora como propias algunas características que en otros lenguajes son extensiones: gestión de hilos, ejecución remota, etc.

El código Java, una vez compilado, puede llevarse sin modificación alguna sobre cualquier sistema operativo (Windows, Linux, Mac OS X, IBM, ...), y ejecutarlo allí. Esto se debe a que el código se compila a un lenguaje intermedio (llamado bytecodes) independiente de la máquina. Este lenguaje intermedio es interpretado por el intérprete Java, denominado Java Virtual Machine (JVM), que deberá existir en la plataforma en la que queramos ejecutar el código. La siguiente figura ilustra el proceso.

El código fuente se "compila" a un código de bytes de alto nivel independiente de la máquina. Este código (byte-codes) está diseñado para ejecutarse en una máquina hipotética que es implementada por un sistema run-time, que sí es dependiente de la máquina. El hecho de que la ejecución de los programas Java sea realizada por un intérprete, en lugar de ser código nativo, ha generado la suposición de que los

programas Java son más lentos que programas escritos en otros lenguajes compilados (como C o C++). Aunque esto es cierto en algunos casos, se ha avanzado mucho en la tecnología de interpretación de bytecodes y en cada nueva versión de Java se introducen optimizaciones en este funcionamiento. En la última versión de Java, 1.5 (ahora todavía en beta), se introduce una nueva JVM servidora que queda residente en el sistema. Esta máquina virtual permite ejecutar más de un programa Java al mismo tiempo, mejorando mucho el manejo de la memoria. Por último, es posible encontrar bastantes benchmarks en donde los programas Java son más rápidos que programas C++ en algunos aspectos.

11.3. Funcionamiento de la maquina virtual.

Vamos a comentar a continuación de forma breve cual es el funcionamiento de la maquina virtual si bien dicha explicación se puede encontrar con un mayor nivel de detalle en capítulo 4 Modelo de funcionamiento de la maquina virtual. La estructura de la maquina virtual se puede simplificar como sigue:

Figura 11.6: Funcionamiento maquina virtual KVM.

A partir de los archivos .class ya compilados el cargador de clases (loader) es encarga como su propio nombre indica de cargar las clases en la maquina virtual reservando para ello la memoria necesaria para los datos en las áreas destinadas a tal efecto (runtime data areas). Cada hilo creado durante la ejecución de la maquina virtual comparte este área con el resto de los hilos pero cada hilo tiene sus propios registros de funcionamiento. Es decir cada hilo tiene su propio registro IP o contador de instrucciones que indica cual es la instrucción a nivel de bytecodes que debe ser ejecutada y por supuesto su propia pila de ejecución (accesible por el registro SP). La pila de ejecución del hilo mantiene la información de ejecución de dicho hilo tal como los métodos que se han de ejecutar junto son sus parámetros y sus valores de retorno si lo tienen, parámetros relacionados con el sincronismo entre hilos, etc.…

Precisamente existen una serie de registros que son empleados por el interprete para poder completar el flujo de bytecodes correspondientes a un hilo (.class):

• IP (instruction pointer): es el puntero que indica la instrucción que se esta ejecutando.

• SP (stack pointer): puntero a la pila de ejecución del hilo. • LP (locals pointers): punteros locales necesarios para parámetros auxiliares del

hilo. • FR (frame pointer): puntero a la lista de marcos de ejecución de los métodos del

hilo. • CP (constant pool pointer): puntero a la constante de almacenaje de datos del

hilo.

Estos registros al ser propios de cada hilo Java que se ejecuta en la maquina virtual. Por ello en aplicaciones multihilo cada vez que un hilo que se estaba ejecutando pasa a estar suspendido debe mantener una copia del valor de los registros antes de ser suspendido para volver a cárgalos en la maquina virtual cuando vuelva a ejecutarse.

11.4. Implementación del intérprete de bytecodes en la KVM.

11.4.1. Introducción.

El interprete de bytecodes que emplea la KVM es muy similar al que emplea la maquina virtual Java al igual que ocurre con el resto de los módulos. Sin embargo mantiene una serie de diferencias que aumentan su rendimiento parámetro que como ya hemos comentado en mas de una ocasión resulta esencial en los dispositivos autónomos.

El intérprete que empleaba la versión de la KVM hasta la versión 1.0 ha sido

completamente reestructurado y modificado para aumentar sin aún cabe más su rendimiento sin que por ello se vea afectada la portabilidad, otro de los aspectos también muy influyentes en los dispositivos a los cuales va dirigida la KVM. En el último apartado de este capítulo se comentara brevemente estas diferencias. En cuanto a la estructura que mantiene la implementación de SUN del interpréte de la KVM se encuentra recogida en tres ficheros cada uno con un contenido claramente diferenciado:

• Execute.c: en este fichero se encuentra el código correspondiente el bucle de ejecución principal del intérprete.

• Interpret.c: almacena el código de las distintas funciones auxiliares de relativa complejidad que el intérprete necesita en su ejecución.

• Bytecode.c: este fichero es en realidad un listado de todos los bytecodes disponibles en esta versión de la KVM junto con las operaciones que implican cada uno de ellos.

Esta estructura permite separar la definición de bytecodes de lo que es el

interprete lo cual facilita el empleo de otras técnicas de interpretación alternativas

usando el mismo conjunto de bytecodes o bien modificar el conjunto de bytecodes (añadiendo nuevos o modificando los existentes).

El intérprete de bytecodes se ejecuta mediante un flujo principal que recorre y

ejecuta los bytecodes del hilo principal activo en ese momento. Superado un tiempo máximo de ejecución del hilo se invoca al planificador de hilos para que el intérprete ejecute los bytecodes de otro de los hilos.

En cada tiempo de ejecución de bytecodes, el algoritmo del intérprete va

ejecutando cada uno de los bytecodes. Para cada uno de ellos analiza el tipo del bytecode y actúa en consecuencia según este.

Advertir al lector que este módulo esta muy relacionado con la ejecución a bajo

nivel de los programas Java en los sistemas con la complejidad que ello conlleva. Gran parte del módulo son configuraciones de registros del sistema y otros parámetros.

11.4.2. Registros empleados por el intérprete de bytecodes.

Antes de explicar el funcionamiento del intérprete en la KVM es conveniente

explicar o al menos comentar de forma breve una seria de conceptos y terminologías que se van a usar frecuentemente. Estos conceptos están relacionados con la ejecución genérica de programas en las computadoras y más en particular con la visión que SUN emplea para representarlos.

Existen una serie de registros que se emplean a menudo en el intérprete para

controlar el flujo del mismo y que son propios de cada hilo de ejecución:

• IP (instruction pointer): es el puntero que indica la instrucción que se esta ejecutando.

• SP (stack pointer): puntero a la pila de ejecución del hilo. • LP (locals pointers): punteros locales necesarios para el hilo. • FR (frame pointer): puntero al marco de ejecución de los métodos. • CP (constant pool pointer): puntero a la constante de almacenaje de datos del

hilo.

Estos registros son propios de cada hilo Java que se ejecuta en la maquina virtual. Por ello en aplicaciones multihilo cada vez que un hilo que se estaba ejecutando pasa a estar suspendido debe mantener una copia del valor de los registros antes de ser suspendido para volver a cárgalos en la maquina virtual cuando vuelva a ejecutarse.

11.4.3. Conjunto de bytecodes de la KVM.

Como ya se ha comentado anteriormente el conjunto de bytecodes disponibles en la versión de Sun del intérprete que se esta estudiando se encuentra recogido en el fichero bytecodes.c. Un listado de los distintos bytecodes de que se dispone se encuentra recogido en un enumerador denominado bytecodes:

typedef enum {

NOP = 0x00, ACONST_NULL = 0x01, ICONST_M1 = 0x02, ICONST_0 = 0x03, ICONST_1 = 0x04, ICONST_2 = 0x05, ICONST_3 = 0x06, ICONST_4 = 0x07, ICONST_5 = 0x08, LCONST_0 = 0x09, LCONST_1 = 0x0A, FCONST_0 = 0x0B, FCONST_1 = 0x0C, FCONST_2 = 0x0D, DCONST_0 = 0x0E, DCONST_1 = 0x0F, BIPUSH = 0x10, SIPUSH = 0x11, LDC = 0x12, LDC_W = 0x13, LDC2_W = 0x14, ILOAD = 0x15, LLOAD = 0x16, FLOAD = 0x17, DLOAD = 0x18, ALOAD = 0x19, ILOAD_0 = 0x1A, ILOAD_1 = 0x1B, ILOAD_2 = 0x1C, ILOAD_3 = 0x1D, LLOAD_0 = 0x1E, LLOAD_1 = 0x1F, LLOAD_2 = 0x20, LLOAD_3 = 0x21, FLOAD_0 = 0x22, FLOAD_1 = 0x23, FLOAD_2 = 0x24, FLOAD_3 = 0x25, DLOAD_0 = 0x26, DLOAD_1 = 0x27, DLOAD_2 = 0x28, DLOAD_3 = 0x29, ALOAD_0 = 0x2A, ALOAD_1 = 0x2B, ALOAD_2 = 0x2C, ALOAD_3 = 0x2D, IALOAD = 0x2E, LALOAD = 0x2F, FALOAD = 0x30, DALOAD = 0x31, AALOAD = 0x32, BALOAD = 0x33, CALOAD = 0x34, SALOAD = 0x35, ISTORE = 0x36,

LSTORE = 0x37, FSTORE = 0x38, DSTORE = 0x39, ASTORE = 0x3A, ISTORE_0 = 0x3B, ISTORE_1 = 0x3C, ISTORE_2 = 0x3D, ISTORE_3 = 0x3E, LSTORE_0 = 0x3F, LSTORE_1 = 0x40, LSTORE_2 = 0x41, LSTORE_3 = 0x42, FSTORE_0 = 0x43, FSTORE_1 = 0x44, FSTORE_2 = 0x45, FSTORE_3 = 0x46, DSTORE_0 = 0x47, DSTORE_1 = 0x48, DSTORE_2 = 0x49, DSTORE_3 = 0x4A, ASTORE_0 = 0x4B, ASTORE_1 = 0x4C, ASTORE_2 = 0x4D, ASTORE_3 = 0x4E, IASTORE = 0x4F, LASTORE = 0x50, FASTORE = 0x51, DASTORE = 0x52, AASTORE = 0x53, BASTORE = 0x54, CASTORE = 0x55, SASTORE = 0x56, POP = 0x57, POP2 = 0x58, DUP = 0x59, DUP_X1 = 0x5A, DUP_X2 = 0x5B, DUP2 = 0x5C, DUP2_X1 = 0x5D, DUP2_X2 = 0x5E, SWAP = 0x5F, IADD = 0x60, LADD = 0x61, FADD = 0x62, DADD = 0x63, ISUB = 0x64, LSUB = 0x65, FSUB = 0x66, DSUB = 0x67, IMUL = 0x68, LMUL = 0x69, FMUL = 0x6A, DMUL = 0x6B, IDIV = 0x6C,

LDIV = 0x6D, FDIV = 0x6E, DDIV = 0x6F, IREM = 0x70, LREM = 0x71, FREM = 0x72, DREM = 0x73, INEG = 0x74, LNEG = 0x75, FNEG = 0x76, DNEG = 0x77, ISHL = 0x78, LSHL = 0x79, ISHR = 0x7A, LSHR = 0x7B, IUSHR = 0x7C, LUSHR = 0x7D, IAND = 0x7E, LAND = 0x7F, IOR = 0x80, LOR = 0x81, IXOR = 0x82, LXOR = 0x83, IINC = 0x84, I2L = 0x85, I2F = 0x86, I2D = 0x87, L2I = 0x88, L2F = 0x89, L2D = 0x8A, F2I = 0x8B, F2L = 0x8C, F2D = 0x8D, D2I = 0x8E, D2L = 0x8F, D2F = 0x90, I2B = 0x91, I2C = 0x92, I2S = 0x93, LCMP = 0x94, FCMPL = 0x95, FCMPG = 0x96, DCMPL = 0x97, DCMPG = 0x98, IFEQ = 0x99, IFNE = 0x9A, IFLT = 0x9B, IFGE = 0x9C, IFGT = 0x9D, IFLE = 0x9E, IF_ICMPEQ = 0x9F, IF_ICMPNE = 0xA0, IF_ICMPLT = 0xA1, IF_ICMPGE = 0xA2,

IF_ICMPGT = 0xA3, IF_ICMPLE = 0xA4, IF_ACMPEQ = 0xA5, IF_ACMPNE = 0xA6, GOTO = 0xA7, JSR = 0xA8, RET = 0xA9, TABLESWITCH = 0xAA, LOOKUPSWITCH = 0xAB, IRETURN = 0xAC, LRETURN = 0xAD, FRETURN = 0xAE, DRETURN = 0xAF, ARETURN = 0xB0, RETURN = 0xB1, GETSTATIC = 0xB2, PUTSTATIC = 0xB3, GETFIELD = 0xB4, PUTFIELD = 0xB5, INVOKEVIRTUAL = 0xB6, INVOKESPECIAL = 0xB7,P INVOKESTATIC = 0xB8, INVOKEINTERFACE = 0xB9, UNUSED_BA = 0xBA, NEW = 0xBB, NEWARRAY = 0xBC, ANEWARRAY = 0xBD, ARRAYLENGTH = 0xBE, ATHROW = 0xBF, CHECKCAST = 0xC0, INSTANCEOF = 0xC1, MONITORENTER = 0xC2, MONITOREXIT = 0xC3, WIDE = 0xC4, MULTIANEWARRAY = 0xC5, IFNULL = 0xC6, IFNONNULL = 0xC7, GOTO_W = 0xC8, JSR_W = 0xC9, BREAKPOINT = 0xCA, /*========================================================================= * Fast bytecodes (used internally by the system * only if FASTBYTECODES flag is on) *=======================================================================*/ GETFIELD_FAST = 0xCB, GETFIELDP_FAST = 0xCC, GETFIELD2_FAST = 0xCD, PUTFIELD_FAST = 0xCE, PUTFIELD2_FAST = 0xCF, GETSTATIC_FAST = 0xD0, GETSTATICP_FAST = 0xD1,

GETSTATIC2_FAST = 0xD2, PUTSTATIC_FAST = 0xD3, PUTSTATIC2_FAST = 0xD4, UNUSED_D5 = 0xD5, INVOKEVIRTUAL_FAST = 0xD6, INVOKESPECIAL_FAST = 0xD7, INVOKESTATIC_FAST = 0xD8, INVOKEINTERFACE_FAST = 0xD9, NEW_FAST = 0xDA, ANEWARRAY_FAST = 0xDB, MULTIANEWARRAY_FAST = 0xDC, CHECKCAST_FAST = 0xDD, INSTANCEOF_FAST = 0xDE, CUSTOMCODE = 0xDF, LASTBYTECODE = 0xDF } ByteCode ;

No se van a comentar el funcionamiento de cada uno de los bytecodes que

figuran en esta versión de la KVM porque dicha documentación quedaría fuera de los objetivos de este proyecto. Simplemente veremos algunos bytecodes a modo de ejemplo.

El bytecode de nombre FCMPL es el que realiza la comparación de dos numeros

en coma flotante y su implementación es la siguiente: #if FLOATBYTECODES SELECT(FCMPL) /* Compare float */ double rvalue = *(float*)sp; double lvalue = *(float*)(sp-1); oneLess; *(long*)sp = (lvalue < rvalue) ? -1 : (lvalue == rvalue) ? 0 : (lvalue > rvalue) ? 1 : (TOKEN & 01) ? -1 : 1; DONE(1)

#endif Como se puede ver toma los dos números a compara de la pila de ejecución

referenciada por el registro SP. La operación oneLess es una operación de manipulación rápida de la pila de ejecución decrementando en uno su tamaño. Mediante instrucciones condicionales ternarias guarda en pila de ejecución el entero simbólico de resultado de la comparación de los números. Finaliza el bytecode llamando a la macro DONE cuya explicación se encuentra recogida en apartado 9.3.5.

Otro ejemplo podrían ser los bytecodes que se emplean cuando se ejecuta la

instrucción return en un método y cuya implementación es la siguiente:

#if STANDARDBYTECODES SELECT6(IRETURN, LRETURN, FRETURN, DRETURN, ARETURN, RETURN) /* Return from method */ BYTE* previousIp = fp->previousIp; OBJECT synchronized = fp->syncObject;

TRACE_METHOD_EXIT(fp->thisMethod); if (synchronized != NULL) { char* exitError; if (monitorExit(synchronized, &exitError) == MonitorStatusError) { exception = exitError; goto handleException; } } /* Special case where we are killing a thread */ if (previousIp == KILLTHREAD) { VMSAVE stopThread(); VMRESTORE if (areAliveThreads()) { goto reschedulePoint; } else { return; /* Must be the end of the program */ } } /* Regular case where we pop the stack frame and return data */ if ((TOKEN & 1) == 0) { /* The even ones are all the return of a single value */ cell data = topStack; POP_FRAME pushStack(data); } else if (TOKEN == RETURN) { POP_FRAME } else { /* We don't care whether it's little or big endian. . . */ long t2 = sp[0]; long t1 = sp[-1]; POP_FRAME pushStack(t1); pushStack(t2); } goto reschedulePoint; DONEX

#endif Lo primero que se hace es guardar en previousIp el valor del contador de programas o instrucción que se ejecuto antes de llamar al método y que se encontraba almacenada en el marco de ejecución del método (FP). Además se obtiene el objeto sincronizado en el caso en el cual el método sea declarado así a nivel Java: BYTE* previousIp = fp->previousIp; OBJECT synchronized = fp->syncObject; Seguidamente se genera la traza correspondiente de salida del método: TRACE_METHOD_EXIT(fp->thisMethod);

Si el método es sincronizado libera el monitor asociado al objeto para que este método pueda se accedido desde otros hilos de ejecución y genera la excepción correspondiente si se produce algún error: if (synchronized != NULL) { char* exitError; if (monitorExit(synchronized, &exitError) == MonitorStatusError) { exception = exitError; goto handleException; } }

Si el método que se ejecuta es el de finalización de un hilo, caso en el cual la instrucción de retorno del método es del tipo KILLTHREAD, se invoca al función stopThread que desde el módulo de gestión de hilos finaliza adecuadamente el hilo. Si aún quedan hilos activos se llama al planificador de hilos:

if (previousIp == KILLTHREAD) {

VMSAVE stopThread(); VMRESTORE if (areAliveThreads()) { goto reschedulePoint; } else { return; /* Must be the end of the program */ }

} Ahora bien el caso más común consistiría en eliminar el marco de ejecución que

se ha creado para el método en cuestión (mediante la macro POPFRAME) y añadir a la pila de ejecución el resultado del método (data) si hay resultados a devolver:

if ((TOKEN & 1) == 0) { /* The even ones are all the return of a single value */ cell data = topStack; POP_FRAME pushStack(data); } else if (TOKEN == RETURN) { POP_FRAME } else { /* We don't care whether it's little or big endian. . . */ long t2 = sp[0]; long t1 = sp[-1]; POP_FRAME pushStack(t1); pushStack(t2); }

goto reschedulePoint;

11.4.4. Implementación de SUN del flujo principal del intérprete.

Se recomienda la lectura del apartado anterior para conocer los distintos

registros y parámetros que emplea el intérprete de la KVM para ejecutar los distintos bytecodes.

Como ya se ha comentado en el apartado anterior el flujo principal del intérprete se encuentra recogido en el fichero execute.c y en particular mediante el método siguiente:

void Interpret() Mediante este método se realiza una invocación simple al intérprete del cual

dispone la KVM. Esté método en realidad actúa como una interfaz para el algoritmo real de interpretación de bytecodes que esta contenido en el método FastInterpret():

CurrentNativeMethod = NULL;

START_TEMPORARY_ROOTS IS_TEMPORARY_ROOT(thisObjectGCSafe, NULL); FastInterpret();

END_TEMPORARY_ROOTS Como vemos la invocación al intérprete se realiza tras haber inicializado la lista

de raíces temporales del sistema mediante las macros START_TEMPORARY_ROOTS. Posteriormente se presentan por la salida estándar los resultados obtenidos, estos resultados incluyen:

• Bytecodes: número de bytecodes que se han ejecutado. • Slowcodes : porcentaje de slowcodes encontrados. • Calls : llamadas a métodos realizadas. • Branches : saltos en memoria realizados. • Rescheduled: replanificaciones realizadas.

Iremos viendo cada uno de estos parámetros conforme vayamos avanzando en la

documentación de este complejo proceso de interpretación que lleva a cabo la maquina virtual. Tener en cuenta que estos presentación de resultados solo se lleva a cabo cuando esta activa la macro INSTRUMENT lo cual es útil para depuración de cambios llevados a cabo en el intérprete.

Veamos a continuación cada una de las fases que se ejecutan en el intérprete.

La primera tarea que se realiza es redefinir como variables locales al interprete

los registros IP, SP, LP, FP de la maquina virtual. En realidad solo es necesario hacer locales el IP y el SP pero siempre es mejor acceder a todos de la misma forma y en el mismo estado y por ello se registran las siguientes variables locales a todo el intérprete: #if IPISLOCAL #undef ip register BYTE* ip; /* Instruction pointer (program counter) */ #endif

#if SPISLOCAL #undef sp register cell* sp; /* Execution stack pointer */ #endif #if LPISLOCAL #undef lp register cell* lp; /* Local variable pointer */ #endif #if FPISLOCAL #undef fp register FRAME fp; /* Current frame pointer */ #endif #if CPISLOCAL #undef cp register CONSTANTPOOL cp; /* Constant pool pointer */ #endif #if ENABLE_JAVA_DEBUGGER register BYTE token;

#endif Se puede comprobar como este volcado de los registros solo se realiza si las macros XXXSLOCAL están activas opción derivada de la configuración de una directiva superior LOCALVMREGISTER dentro del fichero main.h. En realidad no es necesario la copia local de los registros para el funcionamiento pero si mejora el rendimiento del intérprete. Se definen en esta fase otras variables globales como una estructura especial que se emplea para representar los tipos long y double en aquellos sistemas cuyo proceso de compilación obliga a emplear registros de 8 bits alineados de manera adecuada (macros NEED_LONG_ALIGNMENT y NEED_LONG_ALIGNMENT). Para finalizar esta fase se actualizan las variables locales de los registros de la maquina virtual (que se habían registrado anteriormente) con los valores actuales de dichos registros mediante la macro VMRESTORE: #define VMRESTORE { \ RESTOREIP \ RESTOREFP \ RESTORESP \ RESTORELP \ RESTORECP \ } Las macros que se encargan precisamente de tomar el valor de los registros de la maquina virtual o modificarlos si es necesario se encuentran en fichero execute.h y son: /*========================================================================= * SAVEIP/RESTOREIP *=======================================================================*/ #if IPISLOCAL

#define SAVEIP ip_global = ip; CLEAR(ip); #define RESTOREIP ip = ip_global; CLEAR(ip_global); #else #define SAVEIP /**/ #define RESTOREIP /**/ #endif /*========================================================================= * SAVEFP/RESTOREFP *=======================================================================*/ #if FPISLOCAL #define SAVEFP fp_global = fp; CLEAR(fp); #define RESTOREFP fp = fp_global; CLEAR(fp_global); #else #define SAVEFP /**/ #define RESTOREFP /**/ #endif /*========================================================================= * SAVESP/RESTORESP *=======================================================================*/ #if SPISLOCAL #define SAVESP sp_global = sp; CLEAR(sp); #define RESTORESP sp = sp_global; CLEAR(sp_global); #else #define SAVESP /**/ #define RESTORESP /**/ #endif /*========================================================================= * SAVELP/RESTORELP *=======================================================================*/ #if LPISLOCAL #define SAVELP lp_global = lp; CLEAR(lp); #define RESTORELP lp = lp_global; CLEAR(lp_global); #else #define SAVELP /**/ #define RESTORELP /**/ #endif /*========================================================================= * SAVECP/RESTORECP *=======================================================================*/ #if CPISLOCAL #define SAVECP cp_global = cp; CLEAR(cp);

#define RESTORECP cp = cp_global; CLEAR(cp_global); #else #define SAVECP /**/ #define RESTORECP /**/ #endif

Donde las variables xx_global tienen el valor actualizado del registro y las variables xx son la copia local del mismo donde xx serían las siglas del registro en cuestión (sp,fp,…). Las variables xx_global forman parte de la estructura global GlobalState:

struct GlobalStateStruct { BYTE* gs_ip; /* Instruction pointer (program counter) */ cell* gs_sp; /* Execution stack pointer */ cell* gs_lp; /* Local variable pointer */ CONSTANTPOOL gs_cp; /* Constant pool pointer */ FRAME gs_fp; /* Current frame pointer */ };

extern struct GlobalStateStruct GlobalState; #define ip_global GlobalState.gs_ip #define sp_global GlobalState.gs_sp #define lp_global GlobalState.gs_lp #define cp_global GlobalState.gs_cp #define fp_global GlobalState.gs_fp

Tener en cuenta que el código correspondiente al interprete es un código fuertemente no estructurado que hace uso de etiquetas a nivel de código para ir saltando de una parte a otra del algoritmo. De esta forma la ejecución comienza por el estado 0 o la replanificación del hilo si la opción de depuración ENABLE_JAVA_DEBUGGER esta activada (lo cual no se da en entornos de producción):

#if ENABLE_JAVA_DEBUGGER goto reschedulePoint; #else goto next0; #endif El siguiente paso en la ejecución del intérprete depende de uno de los parámetros de configuración que es RESCHEDULEATBRANCH. Si esta opción no esta activada como sucedía por defecto en la versión 1.0 de la KVM se fuerza a una replanificación del hilo de ejecución cada vez que se ejecuta un bytecode del mismo: #if !RESCHEDULEATBRANCH next3: ip++; next2: ip++; next1: ip++; next0: reschedulePoint: RESCHEDULE #endif

En cambio si la opción comentada esta activada solo se replanifica el hilo si se activa el punto reschedulePoint:

#if RESCHEDULEATBRANCH reschedulePoint: RESCHEDULE #if ENABLE_JAVA_DEBUGGER goto next0a; #else goto next0; #endif Como se puede observar la invocación del módulo de threading se lleva a cabo a través de la macro RESCHEDULE. Esta macro realiza las siguientes comprobaciones:

• Comprueba si hay una situación de intercambio de hilo de ejecución mediante la función isTimeToResechedule:

• Se salva el estado de los registros de la maquina virtual.

• Se lanza el evento que invoca al planificador de hilos

• Se restaura el valor salvado de los registros de la maquina virtual.

Para obtener una descripción detallada de cómo se lleva a cabo la replanificación de los hilos de ejecución se recomienda la lectura del capítulo correspondiente al análisis de la gestión de hilos de ejecución en la maquina virtual.

La siguiente fase sigue siendo una fase de configuración previa a la ejecución real del bytecode. Esta fase consta de una serie de operaciones encapsuladas en unas macros específicas. Estas macros realizan una salvaguarda del estado actual de los registros de la maquina virtual (operación que ya hemos comentado en este mismo apartado), seguidamente llaman a la operación concreta y vuelve a restaurar los registros de la maquina virtual:

#define OPERACIONESPECIFICA { \ VMSAVE \ OperacionEspecifica(); \ VMRESTORE \ }

Veamos a continuación estas operaciones de configuración:

• INSTRUCTIONPROFILE: mediante esta operación se invoca al método InstructionProfile que realiza una comprobación del estado correcto de la máquina virtual (si esta activada la opción de INCLUDEDEBUGCODE) e incrementa el contador de instrucciones.

void InstructionProfile() {

#if INCLUDEDEBUGCODE /* Check that the VM is doing ok */ checkVMstatus(); #endif /*INCLUDEDEBUGCODE*/ /* Increment the instruction counter */ /* for profiling purposes */ InstructionCounter++; }

Esta operación es útil para mantener estadísticas acerca de las instrucciones a

nivel maquina ejecutadas. Para obtener una descripción detallada del checkeo de la maquina virtual ejecutado mediante la operación checkVMStatus() se recomienda la lectura del apartado 9.3.5 Métodos auxiliares de ayuda al intérprete.

• INSTRUCTIONTRACE: mediante esta operación se invoca al método InstructionTrace() (para una descripción más amplia de este método consultar apartado 9.3.4) que va actualizando y mostrando la traza de los bytecodes que se están ejecutando si las opciones tracebytecodes y INCLUDEDEBUGCODE están activas.

• INC_BYTECODE: esta operación activa cuando la opción INSTRUMENT de configuración de la maquina virtual lo esta incrementa el contador de bytecodes ejecutados.

• DO_VERY_EXCESSIVE_GARBAGE_COLLECTION : si se encuentra activada la opción VERY_EXCESSIVE_GARBAGE_COLLECTION de configuración de la maquina virtual (empleada para fines de depuración principalmente) se fuerza a ejecutar el recolector de basura por cada bytecode ejecutado:

#if VERY_EXCESSIVE_GARBAGE_COLLECTION #define DO_VERY_EXCESSIVE_GARBAGE_COLLECTION { \ VMSAVE \ garbageCollect(0); \ VMRESTORE \ } #else #if ASYNCHRONOUS_NATIVE_FUNCTIONS && EXCESSIVE_GARBAGE_COLLECTION #define DO_VERY_EXCESSIVE_GARBAGE_COLLECTION { \ extern bool_t veryExcessiveGCrequested; \ if (veryExcessiveGCrequested) { \ VMSAVE \ garbageCollect(0); \ VMRESTORE \ veryExcessiveGCrequested = FALSE; \ } \ } #else #define DO_VERY_EXCESSIVE_GARBAGE_COLLECTION /**/ #endif #endif

La invocación al recolector de basura se hace a través del método garbageCollect. Puede obtenerse documentación detallada del funcionamiento de la gestión de memoria de la KVM incluido el recolector de basura en el capítulo 8. Con este último conjunto de operaciones se ha configurado el entorno para poder servir de manera correcta al sistema el bytecode y todas las operaciones que se hagan internamente en él. Para ello se usa un selector tipo switch en base al contenido de del registro IP en este punto de ejecución del interprete. Es llegado a este punto cuando se incluyen la definición de todos los bytecodes de que dispone el sistema: #define STANDARDBYTECODES 1

#define FLOATBYTECODES IMPLEMENTS_FLOAT #define FASTBYTECODES ENABLEFASTBYTECODES #if SPLITINFREQUENTBYTECODES #define INFREQUENTSTANDARDBYTECODES 0 #else #define INFREQUENTSTANDARDBYTECODES 1 #endif #include "bytecodes.c" #undef STANDARDBYTECODES #undef FLOATBYTECODES #undef FASTBYTECODES #undef INFREQUENTSTANDARDBYTECODES

El conjunto de bytecodes se encuentra recogido como ya se ha comentado en un apartado anterior en el fichero bytecode.c y se configura dicho conjunto en base a los valores de las opciones FLOATBYTECODES (para configurar la alineación de los numeros en coma flotante), FASTBYTECODES (para permitir bytecodes reducidos de ejecución rápida), INFREQUENTSTANDARDBYTECODES (para permitir que los bytecodes mas comunes se carguen mas rápidamente). En el apartado 4 de este capítulo se hará un breve resumen de todos los parámetros de configuración de que se dispone en la maquina virtual para controlar la ejecución del interprete y como el usuario puede modificarlos a su gusto para adaptar la implementación de la maquina virtual a su sistema final específico. El conjunto de operaciones a realizar en base al tipo de bytecode que se este ejecutando es limitado y esta incluido en el selector switch antes comentado:

• branchPoint : esta operación solo se encuentra disponible si la configuración COMMONBRANCHING esta activa. En este caso se incrementa el registro IP en una unidad para ejecutar el siguiente bytecode y se ejecuta el planificador de hilos (reschedulePoint):

#if COMMONBRANCHING

branchPoint: { INC_BRANCHES ip += getShort(ip + 1); goto reschedulePoint; }

#endif

Tener en cuenta que se llega a este punto de ejecución en el ciclo de interpretación del bytecode si se ha producido un salto condicional a lo largo de la memoria (por ejemplo una sentencia if). Estos saltos en las instrucciones a ejecutar se realizan a nivel de intérprete mediante la siguiente macro:

#if COMMONBRANCHING

#define BRANCHIF(cond) { if(cond) { goto branchPoint; } else { goto next3; } } #else #define BRANCHIF(cond) { ip += (cond) ? getShort(ip + 1) : 3; goto reschedulePoint; } #endif

De esta manera el salto condicional se realiza si se cumple la condición cond en cuyo caso se ejecutan las dos siguientes instrucciones a la indicada por el registro IP actual. Estas dos instrucciones en realidad son punteros a otros grupos de instrucciones. Si no se cumple la condición de salto se incrementa en 3 el registro IP continuando la ejecución normal.

• callMethod_interface: este operación como su propio nombre indica se ejecuta al realizarse la invocación de un método genérico:

invokerSize = 5; goto callMethod_general;

Esta operación lo que hace es fijar el tamaño del bytecode que se va a ejecutar a través de la variable invokerSize y desplazarnos a la operación callMethod_general que es la que implementa la invocación del método.

• callMethod_virtual, callMethod_static,callMethod_special: estas operación son ejecutadas al invocar métodos Java afectados por algún modificador de visibilidad tales como static o final. Al igual que en callMehod_interface simplemente se especifica el tamaño del bytecode y se llama a callMethod_general.

• callMethod_general: esta operación es ejecutada cuando se realiza una

invocación de un método. Lo primero que se realiza es incrementar el contador de llamadas a métodos mediante la macro INC_CALL. Si el método es nativo del sistema se invoca dicho método a través de la función auxiliar invokeNativeFunction pasándole como parámetro el método a invocar que se encuentra almacenado en el puntero thisMethod:

if (thisMethod->accessFlags & ACC_NATIVE) { ip += invokerSize; VMSAVE invokeNativeFunction(thisMethod); VMRESTORE TRACE_METHOD_EXIT(thisMethod); goto reschedulePoint;

}

Como vemos el registro IP se incrementa tantas veces como indique el tamaño del bytecode que se esta ejecutando. Además se añade a la traza de ejecución la salida del método cuando se produzca mediante la macro TRACE_METHOD_EXIT. Tras haber ejecutado la traza correspondiente se procede a invocar nuevamente al planificador de hilos de ejecución saltando al punto reschedulePoint. Para obtener una explicación detallada de cómo funciona el método invokeNativeFunction se recomienda la lectura del capítulo 13 Uso y gestión de funciones nativas. Si el método no es nativo se comprueba si es abstracto en cuyo caso se genera un error fatal indicando que se esta tratando de ejecutar un método abstracto: if (thisMethod->accessFlags & ACC_ABSTRACT) {

fatalError(KVM_MSG_ABSTRACT_METHOD_INVOKED); }

Si el método no es abstracto ni es nativo se guarda una copia de la referencia al objeto en otro objeto thisObjectGCSafe para que no sea eliminado por el recolector de basura. Además se añade la referencia al método que se desea ejecutar thisMethod al marco de ejecución del hilo mediante el método auxiliar pushFrame:

thisObjectGCSafe = thisObject; VMSAVE res = pushFrame(thisMethod);

VMRESTORE En el caso en el que método se haya añadido de forma correcta la pila de ejecución se marca cual sería la siguiente instrucción a ejecutar cuando se llegara a la instrucción return dentro del método: if (res) {

/* Advance to next inst on return */ fp->previousIp += invokerSize;

} Además si el método ha sido añadido a la pila correctamente y se trata de un método sincronizado (usado como ya es sabido en la gestión de hilos de Java) ha de grabar el monitor del objeto en cuestión para que el resto de los hilos sepan que esta accediendo al método en cuestión y se guarda en el campo syncObject del registro FP una referencia al objeto. Este campo es empleado por el módulo de gestión de hilos que es tratado en profundidad en el capítulo 11: if (res) {

if (thisMethod->accessFlags & ACC_SYNCHRONIZED) { VMSAVE monitorEnter(thisObjectGCSafe); VMRESTORE fp->syncObject = thisObjectGCSafe; }

}

Finalmente se invoca al planificador de hilos.

• handleXXXXXException: se tienen una serie de opciones en el selector del tipo de bytecode que se refieren a los distintos tipos de excepciones que se pueden producir. Simplemente asocian al objeto excepción un objeto del tipo de excepción en concreto que se ha producido y se invoca a la operación handleException que se encarga de gestionar todas las excepciones. Las excepciones que se producen a nivel de ejecución de los bytecodes son:

o NullPointerException. o ArrayIndexOutOfBoundException. o ArithmetycException. o ArrayStoreException. o ClassCastException. o JavaLangError.

• handleException: con esta operación se recogen las distintas excepciones que se hayan producido durante la ejecución del bytecode y mediante el método raiseException (encuadrado dentro del módulo de gestión de logs y errores de la maquina virtual) se eleva la excepción para que llegue al usuario y quede registrada donde sea necesaria:

VMSAVE raiseException(exception); VMRESTORE goto reschedulePoint;

Al elevar la excepción se produce una llamada al planificador de hilos para que este pueda seguir ejecutando otro hilo que este a la espera.

• callSlowInterpret: esta opción solo esta disponible si esta activo la bandera de SPLITINFREQUENCEBYTECODES y la operación que realiza es tomar el bytecode y ejecutar para este bytecode una versión especial del interprete, el SlowInterpret:

int __token = TOKEN;

VMSAVE SlowInterpret(__token); VMRESTORE

goto reschedulePoint; Este intérprete lento se emplea para la ejecución de los bytecodes que se usan con menor frecuencia y es similar en funcionamiento al FastInterpret salvo en que son más lentos pues no usan en su ejecución copias locales de los registros de la maquina virtual (opción por defecto para el FastInterpret) sino que emplea y accede directamente a los registros y además solo ejecuta el bytecode que le es pasado como parámetro.

• default: si el bytecode no es válido se ejecuta esta operación que genera un error

fatal dando como mensaje el bytecode que ha provocado el error:

sprintf(str_buffer, KVM_MSG_ILLEGAL_BYTECODE_1LONGPARAM, (long)TOKEN); fatalError(str_buffer);

break;

11.4.5. Métodos auxiliares de ayuda al intérprete.

En este apartado se pretender comentar de forma breve pero suficientemente aclarativa los distintos métodos auxiliares que el intérprete necesita en su funcionamiento. Estos métodos se encuentran recogidos en el fichero interpret.c.

11.4.5.1. Método checkVMStatus(). Este método como su propio nombre indica realiza una comprobación acerca del

estado de la maquina virtual al menos en los parámetros que afectan directamente al intérprete. Básicamente realiza una comprobación de dos cosas:

• Los registros o sus copias locales si estas han sido habilitadas están apuntando a posiciones en memoria validas:.

• Las pilas de ejecución no esta infradesbordadas o superdesbordadas.

Primero se recorre la pila de ejecución del hilo actual en el cual se ejecutan los bytecodes y se comprueba si el registro sp (SP: snack pointer) se encuentra dentro de la pila marcando como superada la comprobación (valid) si esto es así y sino se genera un error fatal informando al usuario con el correspondiente mensaje:

for (valid = 0, stack = CurrentThread->stack; stack; stack = stack->next) { if (STACK_CONTAINS(stack, sp)) { valid = 1; break; } } if (!valid) { fatalVMError(KVM_MSG_STACK_POINTER_CORRUPTED);

} Para el registro FP que es el que almacena la información relativa al entorno de ejecución del hilo se realiza una comprobación similar a la anterior solo que esta vez se comprueba que dicho registro no se encuentre dentro de la pila de ejecución del hilo y que el puntero al marco de ejecución de hilo no se encuentre por detrás del registro SP:

for (valid = 0, stack = CurrentThread->stack; stack; stack = stack->next) { if (STACK_CONTAINS(stack, (cell*)fp) && (cell*)fp <= sp) { valid = 1; break; } } if (!valid) { fatalVMError(KVM_MSG_FRAME_POINTER_CORRUPTED); }

Para el registro SP que es el que registra en una lista las distintas variables locales que esta usando el hilo se hace exactamente la misma comprobación que para el registro FP:

for (valid = 0, stack = CurrentThread->stack; stack; stack = stack->next) { if (STACK_CONTAINS(stack, lp) && lp <= sp) { valid = 1; break; } } if (!valid) { fatalVMError(KVM_MSG_LOCALS_POINTER_CORRUPTED); } Finalmente se comprueba que hay más de un hilo de ejecución activo en la maquina virtual, si no es así se genera el correspondiente error fatal: if (!areActiveThreads()) { fatalVMError(KVM_MSG_ACTIVE_THREAD_COUNT_CORRUPTED); } Esta última comprobación se basa simplemente en comprobar que los objetos CurrentThread y RunnableThreads referencian al algún hilo real(es decir no tienen valor nulo).

11.4.5.2. Método getByteCodeName().

Como su propio nombre indica mediante este método se obtiene el nombre del

bytecode que le es pasado como parámetro: if (token >= 0 && token <= LASTBYTECODE)

return byteCodeNames[token]; else return "<INVALID>"; Dicho nombre lo obtiene consultando el listado de bytecodes disponibles

(byteCodeNames).

11.4.5.3. Método printRegisterStatus(). Este método muestra por la salida estándar información referente al estado de la

maquina virtual y sus registros. La información que muestra por orden es:

• Valor actual del contador de programa (IP). • Offset entre el contador de programas actual y el método a ejecutar dentro del

marco de ejecución (FP). • Valor siguiente del contador de programa (IP). • Valores de los registros FP y LP. • Tamaño de la pila de ejecución actual y contenido de la misma (SP). • Contenido del marco de ejecución completo apuntado por el registro FP.

11.4.5.4. Método printVMStatus().

Mediante este método se muestra por pantalla un estado completo de la maquina virtual en el momento de ejecución invocando a una serie de funciones auxiliares que muestra información de distintos parámetros de la KVM:

printStackTrace();

printRegisterStatus(); printExecutionStack();

printProfileInfo(); Como se puede observar muestra información acerca de la traza de ejecución del hilo actual, el estado de los registros de la KVM, la pila de ejecución e información estadística variada.

11.4.5.5. Método fatalSlotError().

Esta función es empleado por algunos de los bytecodes para realizar la comprobación previa a la invocación del método de que se han pasado todos los parámetros que se indican en la definición Java de dicho método tanto si el método esta en la caché como si no.

Esta función emplea la información de la clase CONSTANT POOL para hacer

las siguientes comprobaciones:

• Que los parámetros pasados al método son correctos en número. • Que el método invocado existe para esa clase.

Para obtener una descripción detallada acerca de las estructuras que la KVM emplea para representar los distintos elementos Java ya sean clases, objetos, métodos se recomienda una lectura paciente del capítulo 7 Estructuras de ejecución internas.

11.4.5.6. Macros adicionales.

Además de los métodos auxiliares anteriormente comentados existen una serie

de macros que se emplean a lo largo de la ejecución del intérprete y que encapsulan cierto código que es accedido de forma recurrente. Conforme se ha ido explicando el funcionamiento del intérprete se han explicado algunas de estas macros. En este apartado explicaremos algunas de estas macros que se emplean sobre todo desde el código interno de los bytecodes.

• SELECT: las macros select son las que se emplean para definir los bytecodes y simplemente encapsulan un cláusula múltiple del tipo swtich-case en su interior:

• DONE: para terminar la definición de un bytecode incrementando el registro IP. • DONEX: para terminar la definición de un bytecode con un goto. • DONE_R: para terminar la definición de un bytecode e invocar al planificador de

hilos. • CHECKARRAY: para comprobar si el índice con el que se esta accediendo a un

array están dentro de la longitud del array.

• ENDCHECKARRAY: continuación de la macro anterior donde en base al resultado obtenido de CHECKARRAY se eleva a excepción de ArrayIndexOutOfBoundException o no.

• CALL_VIRTUALMETHOD, CALL_STATICMETHOD, CALL_SPECIALMETHOD, CALL_INTERFACEMETHOD: simplemente son un conjunto de goto a las distintas opciones del selector principal de bytecodes del cual hacía uso el FastInterpret.

• CHECK_NOT_NULL: para detectar y elevar la excepción NullPointerException. • TRACE_METHOD_ENTRY: igual que la macro ya comentada con anterioridad

TRACE_METHOD_EXIT solo que esta es invocada al entrar en el método en cuestión que se esta invocando en ese momento.

• POP_FRAME : realiza una invocación al método popFrame() del módulo de gestión de marcos de ejecución de la maquina virtual para obtener un marco de ejecución de un hilo.

• INFREQUENTROUTINE: si la opción de división de bytecodes esta activa esta macro realiza la invocación del SlowInterpret para un bytecode específico.

• CLEAR: para poner a cero una variable. Esta macro se emplea en determinados bytecodes para impedir errores debidos a la no inicialización de las variables.

11.5. Parámetros de configuración del intérprete.

Una vez que se ha estudiado en profundidad el funcionamiento del intérprete al menos en la versión de la KVM que estamos estudiando podemos hacer un balance de los distintos parámetros o puntos de control que se pueden aplicar sobre dicho intérprete.

Como ya hemos comentado en el apartado 9.3 el intérprete de la KVM sufrió

una reestructuración importante en la versión 1.0.2 orientada a mejorar el rendimiento del mismo sin que por ello afectase a su portabilidad. De esta forma se han mantenido una serie de parámetros de configuración que son:

El intérprete de la KVM ha sufrido bastantes cambios y mejoras en cada nueva versión de la KVM. Es por ello que algunas de las opciones que figuran en main.h provienen de distintas versiones de la KVM. Así desde la versión 1.0 tenemos las siguientes opciones de configuración:

• ENABLEFASTBYTECODES o Valores: 0,1(por defecto). o Descripción: habilita o no el método de cache y reemplazo de

byetecodes. Esta opción mejora el rendimiento de maquina virtual en un 10-20%, pero incrementa el tamaño de la maquina virtual en unos pocos kilobytes. A destacar que el reemplazo de bytecodes no puede ser llevado a cabo en aquellas plataformas en la cuales los bytecodes son almacenados en memoria no volátil (como por ejemplo una memoria ROM). Es por ello que esta opción no funciona por ejemplo en una Palm porque en estas es necesario emplear memoria estática.

• VERIFYCONSTANTPOOLINTEGRITY

o Valores: 0,1(por defecto). o Descripción: indica a la maquina virtual si ha de realizar verificación de

tipos de las entradas de estructuras de constantes en tiempo de ejecución cuando se realiza actualización o búsqueda de estructuras complejas de constantes. Como se puede intuir esta opción cuando esta activa (valor por defecto) reduce la velocidad de ejecución de los programas Java, si bien es recomendable dejarla activa por razones se seguridad.

Algunas definiciones y macros adicionales que pueden o no estar presentes son:

• BASETIMESLICE o Valores: numero de bytecodes por segundo. o Descripción: Este valor determina la frecuencia con la cual la maquina

virtual realiza la conmutación de hilos, notificación de eventos y otras operaciones periódicas necesarias. Un valor pequeño reduce la latencia de manejo de eventos y conmutación de hilos, pero causa un funcionamiento mas lento del interprete.

• DOUBLE_REMAINDER (x,y) fmod(x,y) o Valores: macro de código. o Descripción: macro definida en el fichero interpret.h y empleada para

búsqueda del módulo de dos números en coma flotante.

Por ultimo y todavía dentro de las opciones de la KVM desde la versión 1.0 tenemos la siguiente macro: #ifndef SLEEP_UNTIL / # define SLEEP_UNTIL(wakeupTime) / for(;;) { / ulong64 now=CurrentTime_md(); if(11_compare_ge(now, wakeupTime)) { / break; / } / } / #endif /

Esta macro es bastante importante pues provoca que la maquina virtual se duerma cuando no tiene tareas que realizas dejando de consumir de esta manera recursos de la plataforma sobra la que se ejecuta. La implementación por defecto es una espera activa, sin embargo la mayor parte de las plataformas requieren una implementación más eficiente de este mecanismo que permita a la KVM emplear las características específicas de conservación de alimentación del dispositivo en concreto.

A continuación procedemos a comentar las opciones de compilación relativas al

intérprete de la KVM a partir de la versión 1.0.2, la cual mejoro el rendimiento del intérprete en un 15-30% respecto de la versión 1.0 sin que por ello afecte a la portabilidad del código. Esta mejora depende de la plataforma objetivo y de las capacidades de que disponga el compilador de C que se emplee, y se debe principalmente al uso de 4 técnicas específicas:

• Reestructuración del código del intérprete por el cual los registros de la maquina virtual se colocan en variables de C locales en tiempo de ejecución del intérprete.

• Separación de los bytecodes de Java menos comunes en una subrutina de interpretación de estos bytecodes independiente de la que se emplea para los bytecodes más comunes. Esto permite al compilador de C realizar un mejor trabajo de optimización de código para los bytecodes mas frecuentemente empleados.

• Desplazar el test para programación de hilos de Java desde el inicio del bucle de interpretación a la zona donde se interpretan los bytecodes.

• Rellenar el espacio entre bytecodes para permitir al compilador producir mejor código para la mayor parte de las sentencias de transformación del intérprete.

Estas técnicas no son dependientes de las características específicas de un

compilador y son portables a un gran número de compiladores C. Veamos en profundidad como afectan cada una de estas nuevas técnicas al rendimiento del intérprete.

11.5.1. Copia de los registros de la maquina virtual a variables locales en tiempo de ejecución.

Las distintas opciones que podemos configurar relativas a esta aspecto son:

• LOCALVMREGISTER

o Valores: 0, 1(por defecto). o Descripción: Los registros de la KVM (ip,sp,lp,fp,cp) son

accedidos frecuentemente cuando los bytecodes son ejecutados. En la versión 1.0 de la KVM estos registros se definían como variables globales en C. Desde la versión 1.0.2 estos registros se definen aún como variables globales, pero si la opción LOCALVMREGISTER esta activa, estos registros son copiados a variables locales cuando el intérprete se esta ejecutando lo que permite a un buen compilador de C optimizar el tiempo de ejecución del interprete de la maquina virtual.

• IPISLOCAL,SPISLOCAL,LPISLOCAL,CPISLOCAL

o Valores:0,1 o Descripción: estas macros permiten controlar de forma específica

cual de los registros de la maquina virtual debe ser copiado a local. Es útil para aquellas plataformas con muy poco memoria que no dispongan de registros suficientes dándosele la oportunidad en este caso de que puedan elegir que registros van a ser copiados.

Elegir adecuadamente estas opciones requiere un análisis exhaustivo del código

maquina que genera el compilador, por defecto el orden de criticidad es el siguiente: 1. IP (Punteros de instrucción),

2. SP (Punteros de pila). 3. LP (Punteros locales). 4. FP (Punteros de estructura). 5. CP (Punteros de almacén de constantes). Un aspecto importante y que reseñaremos más adelante es que si se emplea el copiado de registros a variables locales y se desea realizar más cambios en el código encargado de implementar los Java bytecodes es muy importante asegurarse de que las copias locales de los registros son cargadas de nuevo en las correspondientes variables globales antes de llamar a funciones que esperen un valor de estas variables globales.

Los registros de la maquina virtual pueden ser salvaguardados a la correspondiente variables global mediante el uso de una macro especial y que estudiaremos mas adelante denominada VMSAVE. Para realizar el volcado a las variables locales se emplea la macro VMRESTORE previa llamada a la función monitorExit ().

11.5.2. Separación bytecodes poco frecuentes en un algoritmo de interpretación independiente.

El intérprete de la versión 1.0 de la KVM tiene el código para el procesado de

todos los bytecodes en una única y larga sentencia de tipo switch. Sin embargo, un gran número de Java bytecodes son ejecutados raramente. Si el código para los más frecuentes y para los menos frecuentes bytecodes son alojados en rutinas separadas, el compilador de C puede ofrecer un mejor trabajo optimizando pequeños interpretes por separado. Además esto facilita al compilador encontrar registros hardware para los registros de la maquina virtual mas fácilmente cuando la opción LOCALVMREGISTER esta activa. La opción de configuración de este aspecto es:

• SPLITINFREQUENCEBYTECODES o Valores: 0, 1(por defecto). o Descripción: hablita la opción para separar los interpretes de bytecodes

tal y como se ha comentado.

El código para el procesado de los bytecodes se encuentra contenido en el

archivo bytecodes.c y se compila de forma selectiva mediante el empleo de una serie de macros internas: STANDARDBYTECODES, INFREQUENTSTANDARDBYTECODES, FLOATBYTECODES, FASTBYTECODES). Estas 4 macros son empleadas para controlar el desarrollo de los bytecodes apropiados en las subrutinas correctas.

El código contenido en bytecodes.c es ejecutado desde el archivo execute.c, Si la

opción SPLITINFREQUENCYBYTECODES esta activa, el archivo bytecode.c es incluido dos veces en execute.c: la primera de ellas por la rutina SlowInterpret() y otra mas por la rutina Interpret().

11.5.3. Migración de la invocación del planificador de hilos a los puntos de bifurcación del código.

El antiguo interprete de la KVM 1.0 testaba la necesidad de reprogramación de

hilos de ejecución(es decir, la conmutación entre hilos) después de la ejecución de cada bytecode. El rendimiento del intérprete fue mejorado en un 5% cambiando este test de forma que el test es ejecutado después de la ejecución cada bifurcación, goto, o retorno de bytecode.

La reprogramación de hilos en el viejo intérprete se producía cuando un cierto

número de bytecodes son ejecutados, numero que por defecto eran 100 veces la prioridad del hilo. En el intérprete de la versión 1.0.2 se produce 1000 veces por cada llamada, puntos de bifurcación o bytecodes de retorno son ejecutados. Las opciones que configuran esta característica de la KVM es:

• RESCHEDULEATBRANCH o Valores: 0,1(por defecto). o Descripción: activa o desactiva el mecanismo de programación de hilos

en los puntos de bifurcación. • TIMESLICEFACTOR

o Valores: numero entero. El valor que toma por defecto es 1000 cuando RESCHEDULEATBRANCH esta activo y 10000 en caso contrario.

o Descripción: factor de multiplicación para el calculo del tiempo entre reprogramaciones del calendario de conmutación de hilos.

• BASETIMESLICE.

o Valores: se fija a el valor de TIMESLICEFACTOR o Descripción: determina la velocidad a la cual la KVM realiza la

conmutación, notificación y otras operaciones periódicas de mantenimiento de hilos de ejecución. Un numero pequeño reduce la latencia en el manejo de eventos y la conmutación de hilos pero reduce la velocidad de ejecución del interprete.

11.5.4. Configuración del espacio de bytecodes.

La especificación de la maquina virtual de Java define 200 bytecodes Standard, mas 4 reservados para futuros usos inmediatos. Sin embargo, muchos compiladores de C producen un código ejecutable mejor cuando el tamaño de la tabla de bytecodes es 256. Para poder aprovechar esta circunstancia en el archivo main.h se incluye la siguiente bandera:

• PADTABLE o Valores: 1, 0(por defecto). o Descripción: fija el tamaño total de la tabla de bytecodes en 200 o 256.

11.6. Conclusiones.

En este capítulo se ha examinado el funcionamiento del intérprete de bytecodes. El intérprete conforma el núcleo de ejecución de la maquina virtual puesto que los bytecodes es el resultado que se obtiene de compilar los programas Java. Cabe reseñar como este módulo esta íntimamente relacionado con el planificador de hilos puesto que es este último quien se encarga de configurar el entorno de ejecución de la maquina virtual. De esta forma el intérprete de bytecodes mediante un algoritmo principal ejecuta de forma secuencial los bytecodes que conforman la clase correspondiente al hilo que se esta ejecutando en ese momento. Cuando se supera el tiempo máximo de ejecución para el hilo o bien se fuerza una conmutación de hilos se detiene la ejecución del flujo principal del intérprete y se realiza la siguiente operación:

• Salvaguardan los valores de los registros de la maquina virtual. • Se invoca al planificador de hilos. • Se restauran los valores de los registros correspondientes al hilo que se ejecuta

ahora. • Se vuelve a ejecutar el intérprete con los bytecodes del nuevo hilo.

A lo largo de capítulo se ha reseñado el uso que el intérprete hace de una serie de registros de la KVM. Estos registros tienen un conjunto de valores específicos para cada conjunto de bytecodes, es decir para cada hilo. Estos registros mantienen información como por ejemplo que bytecode ha de ser ejecutado o los punteros a la pila de ejecución de métodos del hilo:

• IP (instruction pointer): es el puntero que indica la instrucción que se esta

ejecutando. • SP (stack pointer): puntero a la pila de ejecución del hilo. • LP (locals pointers): punteros locales necesarios para el hilo. • FR (frame pointer): puntero al marco de ejecución de los métodos. • CP (constant pool pointer): puntero a la constante de almacenaje de datos del

hilo.

El flujo principal de ejecución del intérprete contenido en slowInterpret lee de forma secuencial cada uno de los bytecodes correspondientes al hilo activo y para cada uno de ellos opera de la siguiente forma:

• Se analiza de forma secuencial cada uno de los bytecodes y para cada uno de

ellos se realizan las siguientes operaciones. • Se realizan tareas de logging y generación de información estadística. Se invoca

al recolector de basura si se ha configurado para ello. • En base al tipo de bytecode a ejecutar se contemplan diferentes opciones:

o Punto de salto: se ejecuta cuando se produce una bifurcación en el código Java.

o Invocación de un método que es ejecutado cuando nos encontramos con un bytecode de invocación de un método.

o Gestión de excepciones que es invocada cuando se produce una excepción no controlada.

o Normal: ejecución por defecto del bytecode.

A lo largo del capítulo se ha podido observar como se ejecutan los bytecodes que componen los archivos de clases previamente cargados por el módulo loader. Al final del mismo se da también una descripción acerca de cómo se puede configurar.