Download 16. Módulo cargador de clases (Loader).

Document related concepts
no text concepts found
Transcript
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.