Download 11. Intérprete de bytecodes.

Document related concepts
no text concepts found
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 al do 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
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.
2.
3.
Java Virtual Machine (JVM)
Run-time bytecode interpretation
Java bytecode
Y el modo JIT:
Figura 11.4: modo JIT de ejecución.
Donde cada uno de los elementos son:
1.
2.
3.
4.
Java bytecode
Run-time JIT compilation
Native RISC instructions
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 emp leo 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
ACONST_NULL
ICONST_M1
ICONST_0
ICONST_1
ICONST_2
ICONST_3
ICONST_4
=
=
=
=
=
=
=
=
0x00,
0x01,
0x02,
0x03,
0x04,
0x05,
0x06,
0x07,
ICONST_5
LCONST_0
LCONST_1
FCONST_0
FCONST_1
FCONST_2
DCONST_0
DCONST_1
=
=
=
=
=
=
=
=
0x08,
0x09,
0x0A,
0x0B,
0x0C,
0x0D,
0x0E,
0x0F,
BIPUSH
SIPUSH
LDC
LDC_W
LDC2_W
ILOAD
LLOAD
FLOAD
=
=
=
=
=
=
=
=
0x10,
0x11,
0x12,
0x13,
0x14,
0x15,
0x16,
0x17,
DLOAD
ALOAD
ILOAD_0
ILOAD_1
ILOAD_2
ILOAD_3
LLOAD_0
LLOAD_1
=
=
=
=
=
=
=
=
0x18,
0x19,
0x1A,
0x1B,
0x1C,
0x1D,
0x1E,
0x1F,
LLOAD_2
LLOAD_3
FLOAD_0
FLOAD_1
FLOAD_2
FLOAD_3
DLOAD_0
DLOAD_1
=
=
=
=
=
=
=
=
0x20,
0x21,
0x22,
0x23,
0x24,
0x25,
0x26,
0x27,
DLOAD_2
DLOAD_3
ALOAD_0
ALOAD_1
ALOAD_2
ALOAD_3
IALOAD
LALOAD
=
=
=
=
=
=
=
=
0x28,
0x29,
0x2A,
0x2B,
0x2C,
0x2D,
0x2E,
0x2F,
FALOAD
DALOAD
AALOAD
BALOAD
CALOAD
SALOAD
ISTORE
=
=
=
=
=
=
=
0x30,
0x31,
0x32,
0x33,
0x34,
0x35,
0x36,
LSTORE
= 0x37,
FSTORE
DSTORE
ASTORE
ISTORE_0
ISTORE_1
ISTORE_2
ISTORE_3
LSTORE_0
=
=
=
=
=
=
=
=
0x38,
0x39,
0x3A,
0x3B,
0x3C,
0x3D,
0x3E,
0x3F,
LSTORE_1
LSTORE_2
LSTORE_3
FSTORE_0
FSTORE_1
FSTORE_2
FSTORE_3
DSTORE_0
=
=
=
=
=
=
=
=
0x40,
0x41,
0x42,
0x43,
0x44,
0x45,
0x46,
0x47,
DSTORE_1
DSTORE_2
DSTORE_3
ASTORE_0
ASTORE_1
ASTORE_2
ASTORE_3
IASTORE
=
=
=
=
=
=
=
=
0x48,
0x49,
0x4A,
0x4B,
0x4C,
0x4D,
0x4E,
0x4F,
LASTORE
FASTORE
DASTORE
AASTORE
BASTORE
CASTORE
SASTORE
POP
=
=
=
=
=
=
=
=
0x50,
0x51,
0x52,
0x53,
0x54,
0x55,
0x56,
0x57,
POP2
DUP
DUP_X1
DUP_X2
DUP2
DUP2_X1
DUP2_X2
SWAP
=
=
=
=
=
=
=
=
0x58,
0x59,
0x5A,
0x5B,
0x5C,
0x5D,
0x5E,
0x5F,
IADD
LADD
FADD
DADD
ISUB
LSUB
FSUB
DSUB
=
=
=
=
=
=
=
=
0x60,
0x61,
0x62,
0x63,
0x64,
0x65,
0x66,
0x67,
IMUL
LMUL
FMUL
DMUL
IDIV
=
=
=
=
=
0x68,
0x69,
0x6A,
0x6B,
0x6C,
LDIV
FDIV
DDIV
= 0x6D,
= 0x6E,
= 0x6F,
IREM
LREM
FREM
DREM
INEG
LNEG
FNEG
DNEG
=
=
=
=
=
=
=
=
0x70,
0x71,
0x72,
0x73,
0x74,
0x75,
0x76,
0x77,
ISHL
LSHL
ISHR
LSHR
IUSHR
LUSHR
IAND
LAND
=
=
=
=
=
=
=
=
0x78,
0x79,
0x7A,
0x7B,
0x7C,
0x7D,
0x7E,
0x7F,
IOR
LOR
IXOR
LXOR
IINC
I2L
I2F
I2D
=
=
=
=
=
=
=
=
0x80,
0x81,
0x82,
0x83,
0x84,
0x85,
0x86,
0x87,
L2I
L2F
L2D
F2I
F2L
F2D
D2I
D2L
=
=
=
=
=
=
=
=
0x88,
0x89,
0x8A,
0x8B,
0x8C,
0x8D,
0x8E,
0x8F,
D2F
I2B
I2C
I2S
LCMP
FCMPL
FCMPG
DCMPL
=
=
=
=
=
=
=
=
0x90,
0x91,
0x92,
0x93,
0x94,
0x95,
0x96,
0x97,
DCMPG
IFEQ
IFNE
IFLT
IFGE
IFGT
IFLE
IF_ICMPEQ
=
=
=
=
=
=
=
=
0x98,
0x99,
0x9A,
0x9B,
0x9C,
0x9D,
0x9E,
0x9F,
IF_ICMPNE
IF_ICMPLT
IF_ICMPGE
= 0xA0,
= 0xA1,
= 0xA2,
IF_ICMPGT
IF_ICMPLE
IF_ACMPEQ
IF_ACMPNE
GOTO
=
=
=
=
=
JSR
RET
TABLESWITCH
LOOKUPSWITCH
IRETURN
LRETURN
FRETURN
DRETURN
0xA3,
0xA4,
0xA5,
0xA6,
0xA7,
=
=
=
=
=
=
=
=
ARETURN
RETURN
GETSTATIC
PUTSTATIC
GETFIELD
PUTFIELD
INVOKEVIRTUAL
INVOKESPECIAL
0xA8,
0xA9,
0xAA,
0xAB,
0xAC,
0xAD,
0xAE,
0xAF,
=
=
=
=
=
=
=
=
0xB0,
0xB1,
0xB2,
0xB3,
0xB4,
0xB5,
0xB6,
0xB7,P
INVOKESTATIC
INVOKEINTERFACE
UNUSED_BA
NEW
NEWARRAY
ANEWARRAY
ARRAYLENGTH
ATHROW
=
=
=
=
=
=
=
=
0xB8,
0xB9,
0xBA,
0xBB,
0xBC,
0xBD,
0xBE,
0xBF,
CHECKCAST
INSTANCEOF
MONITORENTER
MONITOREXIT
WIDE
MULTIANEWARRAY
IFNULL
IFNONNULL
=
=
=
=
=
=
=
=
0xC0,
0xC1,
0xC2,
0xC3,
0xC4,
0xC5,
0xC6,
0xC7,
GOTO_W
JSR_W
BREAKPOINT
= 0xC8,
= 0xC9,
= 0xCA,
/*=========================================================
================
* Fast bytecodes (used internally by the system
*
only if FASTBYTECODES flag is on)
*==========================================================
=============*/
GETFIELD_FAST
GETFIELDP_FAST
GETFIELD2_FAST
PUTFIELD_FAST
PUTFIELD2_FAST
GETSTATIC_FAST
GETSTATICP_FAST
=
=
=
=
=
=
=
0xCB,
0xCC,
0xCD,
0xCE,
0xCF,
0xD0,
0xD1,
GETSTATIC2_FAST
PUTSTATIC_FAST
PUTSTATIC2_FAST
UNUSED_D5
INVOKEVIRTUAL_FAST
INVOKESPECIAL_FAST
=
=
=
=
=
=
0xD2,
0xD3,
0xD4,
0xD5,
0xD6,
0xD7,
INVOKESTATIC_FAST
INVOKEINTERFACE_FAST
NEW_FAST
ANEWARRAY_FAST
MULTIANEWARRAY_FAST
CHECKCAST_FAST
INSTANCEOF_FAST
=
=
=
=
=
=
=
0xD8,
0xD9,
0xDA,
0xDB,
0xDC,
0xDD,
0xDE,
CUSTOMCODE
= 0xDF,
LASTBYTECODE
} ByteCode ;
= 0xDF
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 ve r 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
OBJECT synchronized
= fp->previousIp;
= 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
OBJECT synchronized
= fp->previousIp;
= 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; /*
#endif
Instruction pointer (program counter) */
#if SPISLOCAL
#undef sp
register cell* sp; /*
#endif
#if LPISLOCAL
#undef lp
register cell* lp; /*
#endif
#if FPISLOCAL
#undef fp
register FRAME fp; /*
#endif
#if CPISLOCAL
#undef cp
register CONSTANTPOOL
#endif
Execution stack pointer */
Local variable pointer */
Current frame pointer */
cp; /*
Constant pool pointer */
#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
#define
#else
#define
#define
#endif
SAVEIP
ip_global = ip; CLEAR(ip);
RESTOREIP ip = ip_global; CLEAR(ip_global);
SAVEIP
/**/
RESTOREIP /**/
/*====================================================================
=====
* SAVEFP/RESTOREFP
*=====================================================================
==*/
#if FPISLOCAL
#define SAVEFP
#define RESTOREFP
#else
#define SAVEFP
#define RESTOREFP
#endif
fp_global = fp; CLEAR(fp);
fp = fp_global; CLEAR(fp_global);
/**/
/**/
/*====================================================================
=====
* SAVESP/RESTORESP
*=====================================================================
==*/
#if SPISLOCAL
#define SAVESP
#define RESTORESP
#else
#define SAVESP
#define RESTORESP
#endif
sp_global = sp; CLEAR(sp);
sp = sp_global; CLEAR(sp_global);
/**/
/**/
/*====================================================================
=====
* SAVELP/RESTORELP
*=====================================================================
==*/
#if LPISLOCAL
#define SAVELP
#define RESTORELP
#else
#define SAVELP
#define RESTORELP
#endif
lp_global = lp; CLEAR(lp);
lp = lp_global; CLEAR(lp_global);
/**/
/**/
/*====================================================================
=====
* 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;
cell*
gs_sp;
cell*
gs_lp;
CONSTANTPOOL gs_cp;
FRAME
gs_fp;
};
{
/*
/*
/*
/*
/*
Instruction pointer (program counter) */
Execution stack pointer */
Local variable pointer */
Constant pool pointer */
Current frame pointer */
extern struct GlobalStateStruct GlobalState;
#define
#define
#define
#define
#define
ip_global
sp_global
lp_global
cp_global
fp_global
GlobalState.gs_ip
GlobalState.gs_sp
GlobalState.gs_lp
GlobalState.gs_cp
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:
•
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.
INSTRUCTIONPROFILE:
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 :
•
INC_BYTECODE :
•
DO_VERY_EXCESSIVE_GARBAGE_COLLECTION
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.
esta operación activa cuando la opción INSTRUMENT de
configuración de la maquina virtual lo esta incrementa el contador de bytecodes
ejecutados.
: 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
o
o
o
o
o
•
NullPointerException.
ArrayIndexOutOfBoundException.
ArithmetycException.
ArrayStoreException.
ClassCastException.
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.