8. Inicio y terminación de la KVM. Cuando se instala la KVM y en general cualquier maquina virtual en un sistema operativo, se ubica en algún punto de la estructura de directorios del sistema un archivo (usualmente de nombre kvm.exe) que es el que se debe ejecutar para iniciar la maquina virtual. El proceso de inicialización de la maquina virtual Java y en particular de la KVM es un proceso relativamente complejo pues conlleva una actuación sobre casi todos los módulos que la integran. Además dichas actuaciones dependen en gran medida del entorno de ejecución y del sistema operativo en el cual este instalado la maquina virtual. Este proceso de inicialización como se verá en el siguiente apartado, es el punto de comienzo para el flujo genérico de ejecución de la KVM. Es decir, actúa como la función main típica de entrada de cualquier programa escrito en C. La KVM suministra dos mecanismos para el inicio de la maquina virtual dependiendo del tipo de plataforma en la que nos encontremos: • • A través de la línea de comandos del sistema. A través de interfaces que son sean la línea de comandos como puede ser una interfaz GUI, un micro-browser, etc… El primero de los mecanismo es el proceso típico de ejecución de un archivo, en esta caso la maquina virtual. Es decir, desde la línea de comandos se ejecuta el archivo kvm.exe pasándole los parámetros que se deseen separados por espacios. El segundo mecanismo es en realidad una máscara para el primer mecanismo, es decir esta pensado para aquellos sistemas que dispongan de una interfaz mas amigable que la línea de comandos para arrancar la maquina virtual. El proceso de finalización de la maquina virtual, por otro lado, es fundamental pues con el se ha de asegurar que se liberan todos los recursos que se habían ocupado durante la ejecución de la KVM. 8.1. Flujo genérico de la KVM. La operación que se encarga de controlar el flujo de ejecución de la KVM para cualquier tipo de plataforma es la siguiente: int StartJVM(int argc, char* argv[]) Como vemos tiene los mismos parámetros que una función main puesto que actúa como tal. Dentro de esta operación se comprueba primero si existe alguna clase a ejecutar dentro de los parámetros que se han pasado (argv): if (argc <= 0 || argv[0] == NULL) { AlertUser(KVM_MSG_MUST_PROVIDE_CLASS_NAME); return -1; } Si existe tal clase se invoca a la operación interna KVM_start que inicia la maquina virtual y empieza a ejecutar la clase que ha sido pasada como parámetro. returnValue = KVM_Start(argc, argv); Cuando se termina la ejecución de la KVM sea por el motivo que sea se invoca la operación KVM_cleanUp (para finalizar correctamente todas las gestiones de la maquina virtual con el sistema como por ejemplo la memoria): KVM_Cleanup(); Finalmente se devuelve al sistema el valor de retorno que se obtuvo al finalizar la ejecución de la maquina virtual y que indica si ha existido algún error en su ejecución: return returnValue; 8.2. Inicio privado de la maquina virtual. La operación que se encarga de iniciar la maquina virtual como hemos comentado en al apartado anterior es: int KVM_Start(int argc, char* argv[]) Esta operación simplemente invoca de forma iterativa las operaciones de inicialización de cada uno de los módulos que componen la KVM. Así se ejecutan por este orden los siguientes módulos: • Creación de la memoria ROM: sólo si la opción ROMIZING esta activa: CreateROMImage(); • Inicialización de operaciones I/O asíncronas: solo AYNCHRONOUS_NATIVE_FUNCTIONS esta activa: InitalizeAsynchronousIO(); • Inicializa el módulo de interfaces nativas: InitializeNativeCode(); • Inicializa el núcleo de la maquina virtual: InitializeVM(); • Inicializa las variables globales: InitializeGlobals(); si la opción • Inicializa los variables para datos estadísticos (profiling): InitializeProfiling(); • Inicializa el gestor de memoria: InitializeMemoryManagement(); • Inicializa las hashtables que se emplean de forma auxiliar para almacenamiento de datos: InitializeHashtables(); • Inicializa la cache del sistema: InitializeInlineCaching(); • Inicializa el módulo Loader: InitializeClassLoading(); • Inicializa el sistema de clases Java y sus estructuras internas: InitializeJavaSystemClasses(); • Inicializa el verificador de clases: InitializeVerifier(); • Inicializa el gestor de eventos del sistema: InitializeEvents(); El siguiente paso a seguir es la inicialización de la KVM es cargar la clase principal por la cual comienza la ejecución que sería la clase que contiene la función main, para ello se invoca a la función auxiliar loadMainClass. En caso de que no se encuentre la clase se alerta al usuario y se sale de la maquina virtual devolviendo un código de error -1: mainClass = loadMainClass(argv[0]); if (!mainClass) { sprintf(str_buffer, KVM_MSG_CLASS_NOT_FOUND_1STRPARAM, argv[0]); AlertUser(str_buffer); returnValue = 1; } Si se ha podido cargar la clase que contiene el método main el siguiente paso es mediante la función auxiliar readCommandLineArguments parseamos los parámetros pasados por línea de comandos por el usuario a un array: ARRAY arguments = readCommandLineArguments(argc - 1, argv + 1); Una vez que ya hemos cargado la función main se inicializa el módulo de gestión de hilos de la KVM (con el array de argumentos antes creado y la clase main) y en caso de que este activado el depurador de inicializa dicho módulo: InitializeThreading(mainClass, arguments); #if ENABLE_JAVA_DEBUGGER if (debuggerActive) { InitDebugger(); } #endif Si bien las clases de sistema ya han sido cargadas e inicializadas anteriormente es conveniente asegurarse de que esto ha sucedido así por lo que se inicializan las clases del sistema Java y se ponen en la pila de ejecución: initializeClass(JavaLangOutOfMemoryError); initializeClass(JavaLangSystem); initializeClass(JavaLangString); initializeClass(JavaLangThread); initializeClass(JavaLangClass); Una vez más si el depurador esta activado y ejecutándose se lanza un evento que indica al usuario que la maquina virtual acaba de arrancar: #if ENABLE_JAVA_DEBUGGER if (vmDebugReady) { setEvent_VMInit(); if (CurrentThread == NIL) { CurrentThread = removeFirstRunnableThread(); loadExecutionEnvironment(CurrentThread); } sendAllClassPrepares(); } #endif Finalmente se arranca el intérprete que es el que comienza a ejecutar los bytecodes de los programas Java creados y ya compilados previamente por el usuario: Interpret(); Todo el bloque de inicialización de la KVM forma un bloque del tipo TRYCATCH para que de esta forma si se produce un error en alguno de los procesos de inicialización que conlleva capture el error y devuelva el código correspondiente sin seguir inicializando la maquina virtual. 8.3. Finalización privada de la KVM. El proceso de finalización de la KVM se ejecuta automáticamente desde el flujo genérico de ejecución una vez que la maquina virtual ha terminado de ejecutar todos los programas o se ha producido un error. La operación es la siguiente: void KVM_Cleanup() El proceso comienza lanzando el evento de finalización de la maquina virtual si el depurador esta activo y ejecutándose, se cierra el depurador y se eliminan todos los breakpoints que pudieran existir: #if ENABLE_JAVA_DEBUGGER if (vmDebugReady) { setEvent_VMDeath(); CloseDebugger(); clearAllBreakpoints(); } #endif Seguidamente se invocan las operaciones de finalización de los módulos que en su funcionamiento implican uso de recursos de sistema (memoria, dispositivos I/O,…): • Finalización genérica de la maquina virtual: FinalizeVM(); • Finalización de la memoria caché: FinalizeInlineCaching(); • Finalización de funciones nativas: FinalizeNativeCode(); • Finalización del sistema Java de clases: FinalizeJavaSystemClasses(); • Finalización del módulo Loader: FinalizeClassLoading(); • Finalización del módulo de gestión de memoria: FinalizeMemoryManagement(); • Destrucción de la memoria ROM: DestroyROMImage(); • Finalización de la estructuras auxiliares hashtables: FinalizeHashtables(); 8.4. Inicio de la KVM desde línea de comandos. Para todos aquellos sistemas operativos o dispositivos sobre los cuales se va a instalar y ejecutar la KVM que soporten línea de comandos (caso de Windows o Linux) se emplea una función genérica main de arranque. Es decir la KVM se iniciaría desde la función main contenida en main.h. Al arrancar la maquina virtual primero se configuran una serie de parámetros que son: • El uso del módulo JAM (Java Application Manager): #if USE_JAM char *jamInstalledAppsDir = "./instapps"; #endif JamEnabled = FALSE; JamRepeat = FALSE; • El tamaño de memoria por defecto que se va a usar: RequestedHeapSize = DEFAULTHEAPSIZE; • El flag que se emplea para habilitar la suspensión de la maquina virtual desde el módulo de depuración: #if ENABLE_JAVA_DEBUGGER suspend = TRUE; #endif El siguiente paso es leer de la línea de comandos los distintos parámetros que se pasan (por ejemplo java –version –jar Prueba.jar) y que permiten configurar como se ejecutara la maquina virtual: • Para que muestre por salida estándar la versión actual de la KVM sin arrancarla: if (strcmp(argv[1], "-version") == 0) { fprintf(stdout, "Version: %s\n", BUILD_VERSION); exit(1); } • Para que muestre por la salida estándar el conjunto de parámetros de arranque disponibles sin arrancar la KVM. Se hace uso de la funció n auxiliar printHelpText: if (strcmp(argv[1], "-help") == 0) { printHelpText(); exit(0); } • Para ejecutar o no el depurador (activarlo o no se hace a través de banderas de compilación de la propia maquina virtual): if (strcmp(argv[1], "-debugger") == 0) { debuggerActive = TRUE; argv++; argc--; } • Para permitir que sea suspendido o no la ejecución: if (strcmp(argv[1], "-suspend") == 0) { suspend = TRUE; argv++; argc--; } else if (strcmp(argv[1], "-nosuspend") == 0) { suspend = FALSE; argv++; argc--; } • Para especificar los puertos de depuración que se van a usar: if ((strcmp(argv[1], "-port") == 0) && (argc > 2)) { debuggerPort = (short)atoi(argv[2]); argv+=2; argc -=2; } • Para permitir o no el algoritmo de recolección de basura límite: if (strcmp(argv[1], "-excessivegc") == 0) { excessivegc = TRUE; argv++; argc--; } • Para modificar el tamaño de memoria que se va a emplear: if ((strcmp(argv[1], "-heapsize") == 0) && (argc > 2)) { char *endArg; long heapSize = strtol(argv[2], &endArg, 10); switch (*endArg) { case '\0': break; case 'k': case 'K': heapSize <<= 10; break; case 'm': case 'M': heapSize <<= 20; break; default: printHelpText(); exit(1); } } Dicho tamaño se debe encontrar entre un rango que abarca un valor mínimo de 16 kilobytes hasta 64 megabytes que es el tamaño máximo que permite el recolector de basura. Sin embargo hay que tener en cuenta que en la práctica el recolector de basura funciona mejor con memorias más pequeñas: if (heapSize < 16 * 1024) { fprintf(stderr, KVM_MSG_USES_16K_MINIMUM_MEMORY "\n"); heapSize = 16 * 1024; } if (heapSize > 64 * 1024 * 1024) { fprintf(stderr, KVM_MSG_USES_64M_MAXIMUM_MEMORY "\n"); heapSize = 64 * 1024 * 1024; } /* Para que sea divisible por 4 */ heapSize -= heapSize%CELL; argv+=2; argc -=2; RequestedHeapSize = heapSize; • Para especificar el classpath: if ((strcmp(argv[1], "-classpath") == 0) && argc > 2) { if (JamEnabled) { fprintf(stderr, KVM_MSG_CANT_COMBINE_CLASSPATH_OPTION_WITH_JAM_OPTION); exit(1); } UserClassPath = argv[2]; argv+=2; argc -=2; } • Para especificar el nombre de usuario que invoca la maquina virtual: if (strcmp(argv[1], userName) == 0) { varName = 1; argv++; argc--; } • Para especificar si se quiere una traza completa por salida estándar de la ejecución de la maquina virtual: if (strcmp(argv[1], "-traceall") == 0) { #define TURN_ON_OPTION(varName, userName) varName = 1; FOR_EACH_TRACE_FLAG(TURN_ON_OPTION) argv++; argc--; } • Para especificar el uso de Jam: if (strcmp(argv[1], "-jam") == 0) { JamEnabled = TRUE; argv++; argc--; } else if (JamEnabled && (strcmp(argv[1], "-repeat") == 0)) { #if ENABLE_JAVA_DEBUGGER if (debuggerActive) { fprintf(stderr, KVM_MSG_CANT_COMBINE_DEBUGGER_OPTION_WITH_REPEAT_OPTION); exit(1); } #endif JamRepeat = TRUE; argv++; argc--; } Por supuesto se comprueba que el depurador no esta activo pues es incompatible con el uso de Jam. • Para especificar el directorio de aplicaciones en caso de que el JAM este activado: if (JamEnabled && (strcmp(argv[1], "-appsdir") == 0) && argc > 2) { jamInstalledAppsDir = argv[2]; argv+=2; argc-=2; } Seguidamente se leen de las variables del entorno el classpath que se ha de usar en caso de que este no haya sido indicado mediante parámetro por línea de comandos: if (UserClassPath == NULL && !JamEnabled) { UserClassPath = getenv("classpath"); if (UserClassPath == NULL) { UserClassPath = getenv("CLASSPATH"); if (UserClassPath == NULL) { UserClassPath = "."; } } } Si el Jam ha sido habilitado se inicializa dicho módulo y se carga la URL que necesita dicho módulo leyendo de la línea de comandos: if (JamEnabled) { if (argc != 1 ) { fprintf(stderr, KVM_MSG_EXPECTING_HTTP_OR_FILE_WITH_JAM_OPTION); exit(1); } JamInitialize(jamInstalledAppsDir); do { result = JamRunURL(argv[0], JamRepeat); if (result == JAM_RETURN_ERR) { break; } } while(JamRepeat); JamFinalize(); } Finalmente se invoca la operación genérica de inicialización de la KVM: result = StartJVM(argc, argv); Si esta activada la opción de generación de estadísticas se imprime por la salida estándar la información recogida: #if ENABLEPROFILING printProfileInfo(); #endif Si no se ha especificado ningún archivo a ejecutar se ejecuta la función printHelpText para mostrar por la salida estándar la información de invocación de la maquina virtual: if (result == -1) printHelpText(); 8.5. Funciones auxiliares. Hemos visto a lo largo del capítulo algunas funciones auxiliares en las cuales se han apoyado las operaciones principales de inicialización de la maquina virtual. Veámoslas en detalle empezando por la operación de parseo de línea de comandos: static ARRAY readCommandLineArguments(int argc, char* argv[]) Mediante esta operación transformándolos parámetros pasados por línea de comandos (argv) en una estructura de tipo ARRAY interna. Para ello primero se crea un array de clases cuyos elementos sean de tipo JavaLangString y de tamaño 1 y se toma el número de argumentos que hemos de copiar: ARRAY_CLASS arrayClass = getArrayClass(1, JavaLangString, 0); ARRAY stringArray; int numberOfArguments = argc; int argCount = (numberOfArguments > 0) ? numberOfArguments : 0; int i; Se inicializa el array de clases anteriormente creado como una matriz de cadenas de caracteres: IS_TEMPORARY_ROOT(stringArray, argCount)); instantiateArray(arrayClass, Se recorren cada uno de los argumentos añadiendo al array la cadena de caracteres leída de argv previamente parseada a formato interno de la KVM mediante la función instantiateString: for (i = 0; i < numberOfArguments; i++) { stringArray->data[i].cellp = (cell *)instantiateString(argv[i], strlen(argv[i])); } Finalmente se devuelve el array de strings: return stringArray; Otra de las operaciones que hemos citado es la operación: static INSTANCE_CLASS loadMainClass(char* className) Esta operación se encarga como su nombre indica de cargar la clase que lleve la función main dado que esta clase es cargada de forma diferente al resto (no se invoca al loader). Primero se comprueba que no sea un array la clase pasada como parámetro: if (*className == '[') return NULL; Se sustituyen los . del la ruta de la clase por / para poder navegar a través de los paquetes en los que integrado la clase. replaceLetters(className, '.', '/'); Finalmente se obtiene la clase mediante la operación getClass que forma parte del módulo de gestión de estructuras internas de la KVM y de loader: return (INSTANCE_CLASS)getClass(className); 8.6. Conclusiones. En este capítulo se han descrito en profundidad las tareas que se han de ejecutar y completar cuando la maquina virtual es arrancada. Se ha podido comprobar como a la hora de iniciar la maquina virtual se pueden emplear dos mecanismos si bien ambos usan un método intrínseco de inicialización. Dicho método se basa en la lectura de los parámetros que se emplean para la ejecución de la KVM. Así por ejemplo se realiza una primera comprobación acerca de si se ha especificado un archivo de clase a ejecutar. Seguidamente se van invocando de forma individual cada una de las operaciones de inicialización del sistema por este orden: • • • • • • • Gestión de memoria. HashTables (estructuras de datos auxiliares). Caché. Loader. Sistema de clases Java. Verificador. Módulo de gestión de eventos. Posteriormente se inicializan las clases de sistema si es que no esta activa la opción de que dispone la KVM para precargalas y se invoca al módulo Loader para que cargue la clase principal por la que debe comenzar la ejecución del programa Java. Solo cuando la clase principal esta debidamente cargada, se inicia el módulo de gestión de hilos, procurando que la clase a ejecutar en el hilo principal sea la correspondiente a la clase principal pasada como parámetro en el arranque de la maquina virtual. Finalmente se ejecuta el intérprete de bytecodes que es el punto en el cual se implementa la ejecución real del contenido del programa del archivo .class Otro de los aspectos fundamentales en el proceso de arranque/parada de la maquina virtual es la finalización de la misma. Es muy importante que cuando se finalice la maquina virtual el sistema operativo quede exactamente en el mismo estado en el que estaba antes de ejecutarse sobre todo en lo que se refiere a procesos de bajo nivel arrancados y ocupación de memoria.