16. Módulo cargador de clases (Loader). A lo largo de los capítulos vistos anteriormente se ha podido ver el flujo completo de ejecución de un programa Java en la maquina vitual y como se realizan las tareas principales: • • • • • • • Arranque de la maquina virtual. Gestión de memoria. Interprete de bytecodes. Gestión de hilos. Gestión de eventos. Gestión de errores. Gestión de excepciones. En este capítulo y el siguiente se abordará un aspecto relativamente diferenciado y separado del funcionamiento estándar de la maquina virtual: carga de clases y verificación de las mismas. En particular en este apartado se abarcará la carga de clases. El módulo cargador de clases básicamente se encarga de inicializar las estructuras de datos internas que emplea la KVM en su ejecuc ión a partir de un archivo de clases Java compilado. Además constituye una barrera de seguridad en el modelo de 4 etapas que emplea la maquina virtual Java y al que se hará un breve repaso en la introducción de este capítulo. La maquina virtual dispone de un cardar de clases por defecto si bien da la posibilidad de crear un cargador de clases personalizado empleando leguaje de alto nivel Java. Precisamente esta opción se comentará en el segundo apartado del capítulo. Finalmente se abarcará el funcionamiento del cargador de clases de la KVM que es implementado a través de una única operación: cargar un archivo de clases Java invocando una operación individual para cada clase. Así cuando la maquina virtual se pone en funcionamiento, busca en la línea de comandos los archivos de clases que ha de cargar y recorriéndolos uno a uno carga las clases de estos archivos a través del loader. 16.1. Introducción. En la mayor parte de las plataformas tecnológicas de ejecución de aplicaciones existe un módulo especial destinado a realizar la operación de carga de clases en el momento en el cual se crea una instancia de dicha clase antes de que se ejecute el código contenido en la clase. En la imagen siguiente se muestra la ubicación del cargador de clases para una platafo rma web si bien es extensible al resto de plataformas tales como .NET, J2ME, etc. Figura 16.1: Ubicación del class Loader en una plataforma. Esta operación consiste en: • • • • Leer del sistema de ficheros el archivo que contiene el código de la clase. Transformar dicha clase a una estructura de información que pueda entender el entorno de ejecución de la plataforma. Verificar la construcción de la clase. También puede realizar la creación de la instancia a partir de la clase. El cargador de clases normalmente emplea un modelo de delegación que permite configurar los distintos directorios o repositorios de clases en los que buscar. De esta forma cada nivel del modelo se encarga de cargar las clases correspondiente al repositorio de dicho nivel. Así por ejemplo el modelo de delegación de clases en Java es: Figura 16.2: Modelo de delegación de carga de clases de Java. 16.2. Modelo de las 4 etapas de seguridad en Java. El modelo de seguridad de Java se conoce como modelo del patio de juegos (Sandbox model), aludiendo a esos rectángulos con arena donde se deja jugando a los niños pequeños, de manera que puedan hacer lo que quieran dentro del mismo, pero no puedan salir al exterior. En concreto, este modelo se implementa mediante la construcción de cuatro barreras o líneas de defensa: • • • • Primera línea de defensa: Características del lenguaje/compilador Segunda línea de defensa: Verificador de código de bytes Tercera línea de defensa: Cargador de clases Cuarta línea de defensa: Gestor de Seguridad Es importante señalar que aunque se hable de barreras de defensa, no se trata de barreras sucesivas. Es decir, no se trata de que si se traspasa la primera barrera, hay que superar la segunda, y luego la tercera y por fin la última, como muros uno detrás de otros. Más bien hay que imaginar una fortaleza con cuatro muros, y basta que se penetre uno de ellos para que la fortaleza caiga en manos del enemigo. Así que más que de líneas de defensa, habría que hablar de varios frentes. 16.2.1. Las características del lenguaje. Java fue diseñado con las siguientes ideas en mente: • • • Evitar errores de memoria Imposibilitar acceso al SO Evitar que caiga la máquina sobre la que corre Con el fin de llevar a la práctica estos objetivos, se implementaron las siguientes características: • Ausencia de punteros: protege frente a imitación de objetos, violación de encapsulación, acceso a áreas protegidas de memoria, ya que el programador no podrá referenciar posiciones de memoria específicas no reservadas, a diferencia de lo que se puede hacer en C y C++. • Gestión de memoria: ya no se puede gestionar la memoria de forma tan directa como en C, (no hay malloc ). En cambio, se instancian objetos, no se reserva memoria directamente (new siempre devuelve un handler), minimizando así la interacción del programador con la memoria y con el SO. • Recogida de basura: el programador ya no libera la memoria manualmente mediante free (fuente muy común de errores en C y C++, que podía llegar a producir el agotamiento de la memoria del sistema). El recogedor de basura de Java se encarga de reclamar la memoria usada por un objeto una vez que éste ya no es accesible o desaparece. Así, al ceder parte de la gestión de memoria a Java en vez de al programador, se evitan las grietas de memoria (no reclamar espacio que ya no es usado más) y los punteros huérfanos (liberar espacio válido antes de tiempo). • Arrays con comprobación de límites: en Java los arrays son objetos, lo cual les confiere ciertas funciones muy útiles, como la comprobación de límites. Para cada sub índice, Java comprueba si se encuentra en el rango definido según el número de elementos del array, previniendo así que se referencien elementos fuera de límite • Referencias a objetos fuertemente tipadas: impide conversiones de tipo y castings para evitar accesos fuera de límites de memoria (resolución en compilación) • Casting seguro: sólo se permite casting entre ciertas primitivas de lenguaje (ints, longs) y entre objetos de la misma rama del árbol de herencia (uno desciende del otro y no al revés), en tiempo de ejecución • Control de métodos y variables de clases: las variables y los métodos declarados privados sólo son accesibles por la clase o subclases herederas de ella y los declarados como protegidos, sólo por la clase • Métodos y clases final: las clases y los métodos (e incluso los datos miembro) declarados como final no pueden ser modificados o sobrescritos. Una clase declarada final no puede ser ni siquiera extendida. Pero, ¿qué ocurriría si modifico un compilador de C para producir códigos de byte de Java, pasando por alto todas las protecciones suministradas por el lenguaje y el compilador de Java que acabamos de describir? 16.2.2. El verificador. Sólo permite ejecutar código de bytes de programas Java válidos, buscando intentos de: • • • • • fabricar punteros, ejecutar instrucciones en código nativo, llamar a métodos con parámetros no válidos, usar variables antes de inicializarlas, etc. El verificador efectúa cuatro pasadas sobre cada fichero de clase: • • • • En la primera, se valida el formato del fichero En la segunda, se comprueba que no se instancien subclases de clases final En la tercera, se verifica el código de bytes: la pila, registros, argumentos de métodos, opcodes En la cuarta, se finaliza el proceso de verificación, realizándose los últimos tests Si el verificador aprueba un fichero .class, se le supone que cumple ya con las siguientes condiciones: • • • • Acceso a registros y memoria válidos No hay overflow o underflow de pila Consistencia de tipo en parámetros y valores devueltos No hay conversiones de tipos ni castings ilegales Aunque estas comprobaciones sucesivas deberían garantizar que sólo se ejecutarán applets legales, ¿qué pasaría si la applet carga una clase propia que reemplace a otra crítica del sistema, por ejemplo SecurityManager. Para evitarlo, se erigió la tercera línea defensa, el cargador de clases. 16.2.3. Cargador de clases. A la hora de ejecutarse las applets en nuestra máquina, se consideran tres dominios con diferentes niveles de seguridad: • • • La máquina local (el más seguro) La red local guardada por el firewall (seguro) La Internet (inseguro) En este contexto, no se permite a una clase de un domino de seguridad inferior sustituir a otra de un dominio superior, con el fin de evitar que una applet cargue una de sus clases para reemplazar una clase crítica del sistema, soslayando así las restricciones de seguridad de esa clase. Este tipo de ataque se imposibilita asignando un espacio de nombres distinto para clases locales y para clases cargadas de la Red. Siempre se accede antes a las clases del sistema, en vez de a clases del mismo nombre cargadas desde una applet. Además, las clases de un dominio no pueden acceder métodos no públicos de clases de otros dominios. Aun así, ¿sería posible que algún recurso del sistema resultase fácilment e accesible por cualquier clase? Para evitarlo, se creó la cuarta línea de defensa, el gestor de seguridad. 16.2.4. Modulo de seguridad. La gestión de seguridad la realiza la clase abstracta SecurityManager, que limita lo que las applets pueden o no hacer. Para prevenir que sea modificada por una applet maliciosa, no puede ser extendida por las applets. Entre sus funciones de vigilancia, se encuentran el asegurar que las applets no acceden al sistema de ficheros, no abren conexiones a través de Internet, no acceden al sistema, etc. 16.3. El cargador de clases en la maquina virtual Java. El cargador de clases es el responsable de encontrar y cargar los bytecodes que definen las clase. Una vez que se cargan, los bytecodes son verificados antes de que se puedan crear las clases reales. Los cargadores de clases son a su vez clases Java, pero, ¿como se carga el primero?. En principio una máquina virtual Java debe incluir un cargador de clases primario, que es el encargado de arrancar el sistema de carga de clases. Este cargador estará escrito en un lenguaje como el C y no aparece en el contexto Java. El cargador primario carga las clases del sistema de archivos local de modo dependiente del sistema. El cargador de clases cumple también varias tareas relacionadas con la seguridad: • • • El responsable de cargar las clases del paquete java.* es el cargador primario, de hecho, todas las clases de este paquete tienen un cargador de clases null. Este hecho es importante para la seguridad por dos razones: en primer lugar nos garantiza que se cargaran correctamente, algo muy importante, ya que son básicas para el correcto funcionamiento del sistema, y en segundo también nos asegura que se cargarán del sistema de archivos local, lo que evita que una aplicación remota pueda reemplazarlas. El cargador de clases proporciona espacios de nombres diferentes para clases cargadas de orígenes diferentes, lo que evita que haya colisiones de nombres entre clases cargadas desde orígenes distintos. Clases cargadas de fuentes diferentes no pueden comunicarse dentro del espacio de la máquina virtual, lo que evita que programas no fiables obtengan información de otros que si lo son. La implementación por defecto del ClassLoader del JDK busca las clases según los siguientes pasos: 1. Comprueba que la clase no está cargada. 2. Si la clase no está cargada y el cargador actual tiene un cargador padre, se la pide a este y si no al cargador principal. 3. Llama a un método personalizable para intentar encontrar la clase de otra forma. Si después de estos pasos la clase no se ha encontrado se lanza una excepción ClassNotFound. El sistema determina el tipo de cargador a emplear del siguiente modo: • • • • Cuando se carga una aplicación, se emplea una nueva instancia de la clase URLClassLoader. Cuando se carga un applet, se emplea una nueva instancia de la clase AppletClassLoader. Cuando se llama directamente al método java.lang.Class.ForName, se emplea el cargador principal. Si la solicitud de carga la realiza una clase existente, se emplea el cargador de esa clase. Para crear un ClassLoader personalizado basta implementar el método en una subclase: loadClass protected abstract Class loadClass(String throws ClassNotFoundException name, boolean resolve) En el método hay que realizar cinco operaciones: 1. Comprobar si la clase está cargada 2. Si no lo está, cargamos los datos de la clase según el método que queramos (por ejemplo mediante una consulta a una base de datos). 3. Llamamos al método defineClass() para convertir los bytes en una clase. 4. Resolver la clase invocando el método resolveClass(). 5. Retornamos la nueva clase creada. A continuación se presenta un esquema del método loadClass comentado antes: // método loadClass() protected Class loadClass(String nom, boolean res) throws ClassNotFoundException { // -- Paso 1 -Class c = findLoadedClass (nom); if (c == null) { try { c = findSystemClass (nom); } catch (Exception e) { // Ignoramos excepciones } } if (c == null) { // -- Paso 2 -byte datos[] = cargarClase(nom); // -- Paso 3 -c = defineClass (nom, datos, 0, datos.length); if (c == null) throw new ClassNotFoundException (nom); // -- Paso 4 -if (res) resolveClass (c); } // -- Paso 5 -return c; } Para usar el cargador se escribiría un fragmento de código similar al siguiente: ClassLoader cargador = new MiClassLoader(parametros); Class c = cargador.loadClass ("MiClase", true); MiClase mc = (MiClase)c.newInstance(); Para evitar tener que escribir nuestro propio cargador de clases el JDK 1.2 introduce el URLClassLoader, que es una subclase de SecureClassLoader. Con esta clase podemos cargar cualquier clase que pueda ser localizada mediante un URL (file:, http:, jar:, etc). Si lo que necesita el programador es hacer una operación como encriptar u obtener la clase de una BDA, puede hacerlo con una subclase de la clase URLClassLoader. Para usar un URLClassLoader sólo es necesario decirle al cargador donde están las clases, no hace falta hacer una subclase a menos que se tengan requisitos muy especiales. Las URLs que terminan con / se consideran directorios y cualquier otra cosa se intenta cargar como archivo JAR. A continuación se presenta un ejemplo de uso: try { URL listaURLsList[] = { new URL ("http://www.iti.upv.es/clases/"), new URL ("http://case.iti.upv.es/Monkey.zip"), new URL ("http://torpedo.upv.es/luis/norte/"), new File ("misClases.jar").toURL() }; ClassLoader cargador = new URLClassLoader (listaURLs); Class c = cargador.loadClass("MiClase"); MiClase mc = (MiClase) c.newInstance(); } catch (MalformedURLException e) { // cargar la clase de otra manera o error } 16.4. Implementación SUN del cargador de clases para la KVM. El cargador de clases embebido en la KVM para la implementación de SUN que estamos estudiando es prácticamente idéntico al que se emplea en la maquina virtual Java genérica. La única diferencia entre ambos estriba en particularidades a nivel operativo, que ya iremos viendo, y que mejoran el rendimiento de la maquina virtual limitando la operativa de la misma. El cargador de clases al igual que todos los módulos de la KVM están diseñados como un conjunto de operaciones que son invocadas por el resto de los módulos cuando son necesarias. Por otro lado dentro de este módulo tenemos una serie de operaciones relacionadas con la lectura de los ficheros de clases Java. 16.4.1. Operaciones de lectura de ficheros. Dado que la lectura de ficheros es un aspecto que guarda una fuerte dependencia con respecto al sistema de ficheros sobre el cual opera, este parte del loader es dependiente de la plataforma destino. Es por ello que todas las operaciones de lectura de ficheros han de ser implementadas para cada plataforma en el fichero loaderFile.c dentro del paquete VmExtra. El prototipo de estas operaciones se encuentra definido en loader.h y actúa a modo de interfaz con la operación de lectura específica que se implemente. Estas operaciones son las siguientes: • • • • • • • • loadByteNoEOFCheck: leer un bytes de un fichero sin comprobar si se llega al fin de fichero. loadBytesNoEOFCheck: leer un conjunto de bytes de un fichero sin comprobar si se llega al fin de fichero. loadByte: lee un byte de fichero comprobando el fin de fichero. loadShort : lee un entero corto de un fichero. loadCell: lee del fichero un entero largo que dentro de la KVM se representa mediante una celda de memoria (cell). loadBytes: lee un conjunto de bytes hasta que se llega al fin de fichero (EOF). skipBytes: desplaza el puntero actual del fichero en una serie de bytes. getBytesAvailable: devuelve el número de bytes de que dispone un fichero para su lectura. Y las operaciones de apertura/cierre de fichero serían: • • • openClassFile: para abrir un fichero devolviendo el puntero a dicho fichero. openResourceFile: para abrir un determinado recurso del que leer devolviendo el puntero a dicho recurso. closeClassFile: cierra el fichero pasado como parámetro. Las operaciones para gestión del puntero del fichero serían: • • • setFilePointer: para fijar el puntero a un fichero como objeto raíz del sistema devolviendo el descriptor del mismo. getFilePointer: devuelve el puntero a un fichero a través del descriptor del mismo. clearFilePointer: borra el puntero al fichero pasado como parámetro del conjunto de objetos raíces del sistema. Todas estas operaciones emplean en lugar del descriptor FILE de C para el acceso al sistema de ficheros de la maquina una estructura especial que tiene la siguiente forma: struct filePointerStruct { bool_t isJarFile; }; Y que nos permite saber si el fichero es un archivo JAR o no además de cualquie r otro tipo de información que necesitemos en el sistema de ficheros en particular que nos encontremos. Adicionalmente se define una lista de punteros que permite asociar un entero como descriptor de ficheros con un archivo de recursos: extern POINTERLIST filePointerRoot; 16.4.2. Operaciones de inicialización y finalización del módulo. Al igual que sucede con las operaciones de lectura de ficheros las operaciones de inicialización de este módulo también son implementadas de forma específica para cada sistema operativo o plataforma. Las dos operaciones son: • • InitializeClassLoading: inicialización del módulo. Aquí es donde por ejemplo se fija el classpath y se configura el módulo. FinalizeClassLoading: finalización del módulo que incluye tareas como cierre de archivos que estuvieran abiertos. 16.4.3. Operación de carga de un fichero de clases. La operación por la cual la KVM puede cargar un fichero de clases para después ser ejecutado por el intérprete es: Void loadClassfile(INSTANCE_CLASS CurrentClass, bool_t fatalErrorIfFail) Que recibe como parámetro por la estructura de la clase que hemos de cargar, esta estructura tiene todos sus valores nulos excepto el nombre del fichero. Recordemos brevemente los estados en los cuales se puede encontrar una clase y que ya se estudio en el capítulo acerca de las estructuras internas: • • • • • • • • • CLASS_RAW: clase compilada y no cargada aún. CLASS_LOADING: clase que esta siendo cargada. CLASS_LOADED: clase cargada. CLASS_LINKED: clase enlazada con las superclases correspondientes. CLASS_VERIFIED: clase verificada tras pasar por el verificador. CLASS_READY: clase inicializada. CLASS_ERROR: error en la clase. CLASS_INITIALIZED: clase inicializada en el hilo de ejecución actual. IS_ARRAY_CLASS: la clase es un array. Como ya se ha comentado en la introducción el cargador de clases realiza una búsqueda como veremos a continuación de la clase que ha de cargar. Dicha búsqueda la realiza en tres ubicaciones diferentes: • • • • BootStrap Loader: cargador opcional que se puede emplear como veremos en el penúltimo apartado del capítulo. Estándar extensión loader: cargador de clases por defecto de la maquina virtual y que emplea el repositorio de clases ubicada en esta. Class Path Loader: que incluye la búsqueda a través del classpath completo de la maquina virtual. NetWork Class Loader: que realiza la búsqueda en un repositorio de clases remoto. Figura 16.3: Funcionamiento modelo de delegación en J2EE. Este modelo de delegación se corresponde con el que emplea la maquina virtual de la plataforma J2EE. En la J2ME como veremos solo se realizan búsquedas a través del classpath con lo que el modelo de delegación queda muy reducido. Partiendo de la clase que deseamos cargar recorremos toda la jerarquía de clases asociada para buscar tanto la clase en cuestión como las superclases que estén en estado CLASS_RAW, es decir que no hayan sido cargadas aun. Para estas clases se invoca la operación de apoyo loadClassfileHelper que es la encargada de realizar la cara individual de una clase: while (clazz && (clazz->status == CLASS_RAW)) { loadClassfileHelper(clazz, fatalErrorIfFail); if (clazz->status == CLASS_ERROR) { if (clazz != CurrentClass) { NoClassDefFoundError(clazz); } return; } clazz = clazz->superClass; } Como se puede observar se realiza una comprobación acerca de si la clase es errónea. Si la clase es errónea el propio loader se encarga de manejar este error, pero si la clase que es errónea es una superclase de la clase a cargar eso indica un mal funcionamiento que se trata mediante la función auxiliar NoClassDefFoundError (que se usa para indicar al usuario que no existe una definición de clase válida). Para ver con más detalle como el loader realiza la carga de una clase individual mediante la operación loadClassFileHelper se recomienda la lectura del siguiente apartado. Esta separación solo responde a criterios de organización interna del código de la KVM y a criterios de modularidad de la misma dado que la carga individual solo involucra a la propia clase y no a las superclases asociadas a ellas. Seguidamente se vuelven a recorrer cada una de las superclases de la clase que se esta cargando para comprobar si alguna de estas superclases coincide con la clase en cuestión en cuyo caso se produce un error de circulación de clases: for (clazz = CurrentClass->superClass; clazz != NULL; clazz = clazz->superClass) { if (clazz == CurrentClass) { fatalError(KVM_MSG_CLASS_CIRCULARITY_ERROR); } } Hasta este punto se ha realizado la carga individual de la clase y sus superclases pero aún quedan una serie de parámetros por especificar. Para realizar dicho cálculo se emplea como referencia superclases ya linkadas. Es decir vamos recorriendo la jerarquía de superclases de la clase actual pero únicamente tomando aquellas que no están aún linkadas lo cual se realiza mediante la operación findSuperMostUnLinked. El siguiente paso sería realizar un cálculo del tamaño que debe tener la instancia de la clase que se creará así como el offset que tendrán las instancias de los elementos de la clase. Esto es debido a que cuando se crea una instancia de clase desde el módulo de estructuras de gestión interna se emplean los tamaños que ya se encuentran almacenados en la estructura de la clase (ya cargada). Entonces para cada una de estas clases no linkadas si tiene una superclase por encima se comprueban dos detalles: • Que la superclase en cuestión no tenga como modificador de acceso final en cuyo caso se genera el error correspondiente pues según la especificación de la maquina virtual Java de Sun una clase final no puede ser modificada: if (superClass->clazz.accessFlags & ACC_FINAL) { fatalError(KVM_MSG_CLASS_EXTENDS_FINAL_CLASS); } • Que la superclase en cuestión no sea una interfaz puesto que si ese es el caso esta clase debería extender a la interfaz: if (superClass->clazz.accessFlags & ACC_INTERFACE) { fatalError(KVM_MSG_CLASS_EXTENDS_INTERFACE); } Una vez comprobadas las dos circunstancias anteriores se procede a verificar el acceso a la superclase desde la clase actual y se toma la dimensión de la instancia que estábamos buscando: verifyClassAccess((CLASS)superClass, clazz); clazz->instSize = superClass->instSize; Si no existiera superclase superior a la clase no linkada de esta iteración se fija el tamaño de la instancia de la clase a cero puesto que es un objeto de tipo Object. Si la superclase en cuestión no linkada tiene una tabla de interfaces con algún elemento se recorren cada una de las interfaces que implementa la clase para proceder a la carga de las misma: if (clazz->ifaceTable) { struct loadingBacktraceStruct newBacktrace; unsigned int tableLength = clazz->ifaceTable[0]; unsigned int i; newBacktrace.prev = backtrace; newBacktrace.clazz = clazz; for (i = 1; i <= tableLength; i++) { -- TRATAMIENTO PARA CADA INTERFAZ } Para cada una de las interfaces se obtiene del pool correspondiente a la superclase no linkada que estamos examinando, la clase que representa la interfaz que se esta implementando: int cpIndex = clazz->ifaceTable[i]; struct loadingBacktraceStruct *tmp; CLASS intf = clazz->constPool->entries[cpIndex].clazz; Se comprueba que dicha clase no sea un array o la propia clase pues una clase no se puede implementar a ella misma, en estos casos se genera el error fatal correspondiente: if (IS_ARRAY_CLASS(intf)) { fatalError(KVM_MSG_CLASS_IMPLEMENTS_ARRAY_CLASS); } else if (intf == (CLASS)clazz) { fatalError(KVM_MSG_CLASS_IMPLEMENTS_ITSELF); } Se recorre entonces la estructura newBackTrace que se emplea para almacenar la traza anterior de todas las interfaces implementadas tanto por la clase que se esta tratando de cargar como sus superclases. De esta forma si en dicha traza se observa la interfaz que estamos examinando estamos ante un error cíclico que hay que reportar (para evitar de este modo bucles infinitos): for (tmp = &newBacktrace; tmp != NULL; tmp = tmp->prev) { if (tmp->clazz == (INSTANCE_CLASS)intf) { fatalError(KVM_MSG_INTERFACE_CIRCULARITY_ERROR); } } Llegado a este punto estamos ante una interfaz que hay que cargar igual que el resto de clases, es por ello que se invoca de forma recursiva a la operación de carga de clases que estamos detallando en este apartado: loadClassfileInternal((INSTANCE_CLASS)intf, &newBacktrace); TRUE, Además se ha de verificar que desde la superclase no linkada que estamos examinando se tiene visibilidad y se puede acceder a la interfaz: verifyClassAccess(intf, clazz); Una vez que se han cargado todas las interfazes que implementa la clase no linkada superclase de la clase actual se procede a modificar el tamaño inicial de estas instancias en base al número de elementos estáticos que tenga dicha interfaz: FOR_EACH_FIELD(thisField, clazz->fieldTable) unsigned short accessFlags = (unsigned short)thisField>accessFlags; if ((accessFlags & ACC_STATIC) == 0) { thisField->u.offset = clazz->instSize; clazz->instSize += (accessFlags & ACC_DOUBLE) ? 2 : 1; } END_FOR_EACH_FIELD Llegado a este punto hemos alcanzado el final de la carga de la clase que aun no estaba linkada, por lo que se mueve dicha clase a la memoria estática haciendo uso para ello de la operación moveClassFieldsToStatic y se actualiza el estado de la clase a linkada: moveClassFieldsToStatic(clazz); clazz->status = CLASS_LINKED; Una variante de esta operación de carga y linkado de clases es la carga de clases que sean arrays: Void loadArrayClass(ARRAY_CLASS clazz) Esta operación básicamente toma el array y va descomponiendo las distintas dimensiones hasta llegar al elemento base del mismo comprobando. Tener en cuenta que solo realiza esta operación en el caso en el que el flan ARRAY_FLAG_BASE_NOT_LOADED es cierto: if (clazz->flags & ARRAY_FLAG_BASE_NOT_LOADED) { CLASS cb = (CLASS)clazz; do { cb = ((ARRAY_CLASS)cb)->u.elemClass; } while (IS_ARRAY_CLASS(cb)); base = (INSTANCE_CLASS)cb; ……………………………. } Una vez tiene el elemento base que sería una clase cualquiera, se procede a cargar dicha clase haciendo uso de la operación que hemos explicado anteriormente: loadClassfile(base, TRUE); Posteriormente se vuelve a construir el array con todas sus dimensiones y actualizando el flan ARRAY_FLAG_BASE_NOT_LOADED para indicar que el elemento base ya ha sido cargado: cb = (CLASS)clazz; do { if (base->clazz.accessFlags & ACC_PUBLIC) { cb->accessFlags |= ACC_PUBLIC; } ((ARRAY_CLASS)cb)->flags &= ~ARRAY_FLAG_BASE_NOT_LOADED; cb = ((ARRAY_CLASS)cb)->u.elemClass; } while (IS_ARRAY_CLASS(cb)); 16.4.4. Operación carga individual de una clase. Como hemos visto en la operación de carga de clases, desde dicha operación se invoca a otra operación más específica y representada por el siguiente prototipo: static void loadClassfileHelper(INSTANCE_CLASS CurrentClass, bool_t fatalErrorIfFail) Esta es la función encargada de realizar la carga individual de una determinada clase. Primero se comprueba que el estado de la clase es CLASS_RAW es decir que aun no ha sido cargada: if (CurrentClass->status != CLASS_RAW) { return; } Si se encuentra activada la opción de uso de memoria ROM hay que realizar la comprobación acerca de si el paquete en el cual se quiere crear la clase es un paquete de sistema caso en el cual se ha de generar el error correspondiente y se marca la clase como errónea: #if ROMIZING { UString uPackageName = CurrentClass->clazz.packageName; if (uPackageName != NULL) { char *name = UStringInfo(uPackageName); if (IS_RESTRICTED_PACKAGE_NAME(name)) { if (fatalErrorIfFail) { fatalError(KVM_MSG_CREATING_CLASS_IN_SYSTEM_PACKAGE); } CurrentClass->status = CLASS_ERROR; return; } } } #endif Dado que se comienza el proceso de carga de la clase se actualiza el estado de la misma para indicarlo: CurrentClass->status = CLASS_LOADING; Abrimos el fichero que contiene la clase mediante la función openClassFile y como ya se ha hecho en más de una ocasión se declara como raíz temporal para que no pueda ser eliminada por el recolector de basura: DECLARE_TEMPORARY_ROOT(FILEPOINTER, openClassfile(CurrentClass)); ClassFile, Con el fichero que contiene el código de la clase ya abierto se procede a realizar la carga de sus elementos aplicando las siguientes operaciones auxiliares por las cuales se crean en currentClass las estructuras adecuadas: • loadVersionInfo: mediante esta operación se cargan los primeros bytes de la clase, comprobando el tipo del archivo y la información relativa a la versión: static void loadVersionInfo(FILEPOINTER_HANDLE ClassFileH) • loadClassInfo: se carga el identificador de acceso de la clase (ACC_XXXXX) la instancia de la clase y de la superclase como componentes de la clase que esta cargando: static void loadClassInfo(FILEPOINTER_HANDLE CurrentClass) • ClassFileH, INSTANCE_CLASS loadInterfaces: se carga la tabla de interfaces que contiene al s interfaces que implementa la clase: static void loadInterfaces(FILEPOINTER_HANDLE ClassFileH, INSTANCE_CLASS CurrentClass) • loadFields: se cargan las variables y constantes de la clase: static void loadFields(FILEPOINTER_HANDLE ClassFileH, INSTANCE_CLASS CurrentClass, POINTERLIST_HANDLE StringPoolH) • loadMethods: se cargan los métodos que componen la clase: static void loadMethods(FILEPOINTER_HANDLE ClassFileH, INSTANCE_CLASS CurrentClass, POINTERLIST_HANDLE StringPoolH) • ignoreAtttibutes: se cargan una serie de elementos de clase extras tales como el source file que son ignorados por defecto: static void ignoreAttributes(FILEPOINTER_HANDLE ClassFileH, POINTERLIST_HANDLE StringPoolH) Una vez llegado a este punto tenemos en currentClass la clase ya cargada de forma individual y para evitar errores derivados de finalización de fichero de clases incorrecto se termina de leer el fichero hasta llegar al fin del mismo: ch = loadByteNoEOFCheck(&ClassFile); if (ch != -1) { fatalError(KVM_MSG_CLASSFILE_SIZE_DOES_NOT_MATCH); } Seguidamente se cierra el flujo de fichero abierto y se fuerza a que la clase en cuestión sea una instancia de la clase genérica Class y se actualiza el estado de la clase a cargada: closeClassfile(&ClassFile); CurrentClass->clazz.ofClass = JavaLangClass; CurrentClass->status = CLASS_LOADED; Si el depurador esta funcionando se genera el evento asociado para que este pueda recogerlo y notificarlo debidamente al usuario: if (vmDebugReady) { CEModPtr cep = GetCEModifier(); cep->loc.classID = GET_CLASS_DEBUGGERID(&CurrentClass->clazz); cep->threadID = getObjectID((OBJECT)CurrentThread>javaThread); cep->eventKind = JDWP_EventKind_CLASS_LOAD; insertDebugEvent(cep); } Por supue sto, si al haber abierto el fichero al principio la operación el puntero obtenido de tal apertura y que es usado para acceder al fichero es erróneo se genera el error fatal para informa de ello al usuario además de actualizar el estado de la clase a erróneo. CurrentClass->status = CLASS_ERROR; if (fatalErrorIfFail) { sprintf(str_buffer, KVM_MSG_CANNOT_LOAD_CLASS_1PARAM, getClassName((CLASS)CurrentClass)); fatalError(str_buffer); } El modelo de funcionamiento genérico del cargador de clases de la KVM quedaría de la forma siguiente: Figura 16.4: Modelo de funcionamiento de la carga de clases KVM. 16.4.5. Funciones auxiliares. Dentro de este apartado describiremos de forma somera algunas de las funciones auxiliares que se emplean como instrumentos de apoyo por las operaciones principales del módulo. Una de estas funciones es la NoClassDefFounfError que muestra por la salida estándar información acerca de la clase que ha provocado el error y genera a su vez el error fatal correspondiente. Este error se genera cuando al cargar una clase se produce un error en la carga de algunas de las superclases. 16.5. Construir un cargador de clases nuevo en Java. Como acabamos de ver todas las maquinas virtuales disponen en su entorno de ejecución de un cargador de clases que esta embebido en la propia maquina virtual como sucede en el caso de la KVM. Así, este cargador es denominado el cargador principal. Este cargador de clases es especial pues supone que la maquina virtual tiene acceso a un repositorio de clases ya verificadas que pueden ser ejecutadas sin necesidad de invocar el verificador de clases de la maquina virtual. Un cargador de clases básicamente es invocado desde al API de java mediante el siguiente método: Class r = loadClass(String className, boolean resolveIt); Donde className es una cadena de caracteres que la maquina virtual emplea para identificar a la clase que se carga y resolveIt es un bandera que indica a la maquina virtual que todas las clases que hagan referencia a esta han de ser linkadas. Pues bien el cargador principal implementa de la forma que hemos estudiado a lo largo del capítulo este método loadClass. Este cargado entiende que la clase Java.Lang.Object es almacenada en un fichero con el prefijo java/lang/Object.class en algún lugar indicado por el classpath. Mediante esta operación también se busca a través del classpath y en los archivos zip que se encuentre. Ahora bien, ¿en que momentos es invocado este cargador principal? Básicamente en dos casos: • Cuando un bytecode nuevo es ejecutado como por ejemplo: FooClass f = new FooClass(); • Cuando el bytecode hace una referencia estática a la clase: System.out Llegado a este punto nos hacemos la siguiente pregunta, ¿Por qué es necesario un nuevo cargador de clases? Esta es una posibilidad que ofrece la maquina virtual y que posibilita que el usuario pueda emplear un repositorio de clases especial como puede ser un repositorio ubicado en un equipo remoto y accesible mediante HTTP por la red. Sin embargo hayan coste asociado a ello y es que dada la potencia operativa del cargador de clases (se puede por ejemplo reemplazar el Java.Lang.Object) aplicaciones del estilo de los applets no pueden ejecutar sus propios cargadores. El cargador de clases empieza siendo una subclase de LoaderClass siendo el único método abstracto a implementar el loadClass de la siguiente forma: • • • • • • • Verificar el nombre de la clase. Verificar si la clase ya ha sido cargada. Verificar si la clase es una clase de sistema. Recuperar la clase del repositorio. Definir la clase para la maquina virtual. Linkar la clase. Devolver la clase al cargador de clases. 16.6. Parámetros de configuración. Un parámetro de configuración que cabe reseñar es una macro que en realidad ya se ha comentado en algún punto del capítulo y es: #ifndef IS_RESTRICTED_PACKAGE_NAME #define IS_RESTRICTED_PACKAGE_NAME(name) \ ((strncmp(name, "java/", 5) == 0) || (strncmp(name, "javax/", 6) == 0)) #endif Esta macro tal y como esta configurada se emplea para evitar que se produzcan cargas dinámicas de clases que no sean del tipo java.* o javax. Otro de los parámetros que también afectan de sobremanera a este módulo es el CLASSPATH que tal y como se vio en el capítulo dedicado al inicio de la máquina virtual se carga durante dicho proceso. Se puede especificar mediante FILE_OBJECT_SIZE el tamaño de los punteros que se emplean para acceder a los ficheros del sistema de archivos. Existe un parámetro de configuración que afecta al rendimiento de todos los módulos y por consiguiente a este que nos ocupa ahora y es el depurador. La opción ENABLE_JAVA_DEBUGGER e INCLUDE_DEBUG_CODE introduce mucho código con fines de monitorización, es por ello que esta opción suele estar deshabilitada en entornos de producción y solo se emplea para pruebas de la maquina virtual cuando se ha introducido alguna modificación o se ha detectado un bug en la versión que estamos estudiando. 16.7. Conclusiones. El cargador de clases de la maquina virtual Java es un componente fundamental para la ejecución de la misma si bien no interviene de forma directa en su ejecución. Y es que como se ha visto en este capítulo el loader vuelca en la maquina virtual el contenido de un archivo de clases para que la KVM puede ejecutar el código contenido en ella. La maquina virtual de la plataforma J2ME emplea un algoritmo iterativo para la carga de clases. Y es que, a la hora de cargar una determinada clase previamente recorre cada una de las superclases e interfaces de las cuales hereda o implementa, si no están cargadas pasa a cargar estas superclases o interfaces con anterioridad. Si lo que se pretender cargar es un array de clases hay que obtener el elemento base del array, es decir la clase de cada uno de los elementos del array y cargar dicha clase base. En cuanto a la carga individual (loadClassFileHelper) de una determinada clase se ejecuta un algoritmo secuencial que permite cargar los elementos de la clase en el siguiente orden: • • • • • Los primeros bytes de la clase: para verificar la correcta lectura del archivo. Información de clase. Tabla de interfaces que se implementan. Variables y constantes. Métodos. Una vez cargados todos los elementos de la clase se actualiza el estado de la misma que pasa del estado inicial CLASS_RAW a CLASS_LOADED. Un aspecto a reseñar es que para la lectura de archivos .class que sirven de entrada al loader figuran en la implementación de referencia de la KVM una serie de métodos. Estos métodos son en realidad prototipos correspondientes a funciones que han de ser desarrolladas si bien es cierto que la implementación en estudio de la KVM comprende métodos básicos que las implementan para las plataformas Windows y Linux. Al final del capítulo se muestra al lector la forma en la que puede crear su propio cargador de clases. Así se puede sustituir el cargador de clases por defecto por uno personalizado.