Ciclo completo de carga
En la JVM una clase se carga solo cuando el programa realmente la necesita, por ejemplo al crear una instancia, invocar un método static o acceder a un campo. El proceso se divide en varias fases:
- Carga: se localiza el archivo
.classen disco, se lee como secuencia de bytes y se crea un objetojava.lang.Classque actúa como puerta de acceso a la información del tipo en el área de métodos. - Verificación: se comprueba que el bytecode sea válido, que la estructura del archivo sea correcta y que no incumpla restricciones de seguridad.
- Preparación: se asigna memoria para las variables estáticas y se inicializan con sus valores por defecto.
- Resolución: las referencias simbólicas se transforman en referencias directas, es decir, se convierten nombres como
java.lang.Stringen punteros a la estructura interna. - Inicialización: se asignan los valores finales a las variables estáticas y se ejecutan los bloques
static.
La fase de resolución puede completarse en este momento (static linking) o aplazarse hasta que se ejecute el código (dynamic linking).
Cargadores incluidos en la plataforma
Java proporciona una jerarquía de cargadores que se diferencian por el origen de las clases que gestionan:
- Bootstrap (o de arranque): implementado en C/C++ dentro de la JVM, carga las clases del núcleo ubicadas en
rt.jar,charsets.jary similares. - Extensión / Plataforma: en JDK 8 era
ExtClassLoader; a partir de Java 9 se conoce comoPlatformClassLoader, y carga los módulos o extensiones propias del JDK. - Aplicación:
AppClassLoader, encargado delclasspathde la aplicación, incluyendo las clases del usuario. - Personalizado: cualquier cargador definido por el programador heredando de
java.lang.ClassLoader.
Consulta de cargadores
public class MuestraCargadores {
public static void main(String[] args) {
ClassLoader aplicacion = MiClase.class.getClassLoader();
ClassLoader plataforma = aplicacion.getParent();
ClassLoader bootstrap = plataforma.getParent();
System.out.println("Aplicación: " + aplicacion);
System.out.println("Plataforma: " + plataforma);
System.out.println("Bootstrap: " + bootstrap); // null
}
}
El cargador de arranque no es accesible desde Java, por eso su referencia es null. AppClassLoader y el cargador de plataforma son clases internas del Launcher del JDK.
Relación padre-hijo
La clase abstracta ClassLoader mantiene un campo parent que apunta al cargador padre dentro de la cadena de delegación:
public abstract class ClassLoader {
private final ClassLoader parent;
// ...
}
La herencia de clases es distinta de la relación padre-hijo: URLClassLoader extiende SecureClassLoader, que a su vez extiende ClassLoader, mientras que AppClassLoader y el cargador de plataforma suelen extender URLClassLoader.
Construcción interna de la cadena
Durante el arranque, Launcher crea primero el cargador de plataforma y luego el de aplicación, estableceindo el primero como padre del segundo. La secuencia esencial es la siguiente:
ExtClassLoader extens = Launcher.ExtClassLoader.getExtClassLoader();
this.loader = Launcher.AppClassLoader.getAppClassLoader(extens);
Thread.currentThread().setContextClassLoader(this.loader);
El constructor de URLClassLoader recibe el cargador padre y el conjunto de URLs que puede consultar:
public URLClassLoader(URL[] urls, ClassLoader parent) {
super(parent);
this.ucp = new URLClassPath(urls);
}
Como el cargador de arranque no existe en el lado Java, el cargador de plataforma recibe null como padre.
Rutas que gestiona cada cargador
ClassLoader aplicacion = ClassLoader.getSystemClassLoader();
ClassLoader plataforma = aplicacion.getParent();
ClassLoader bootstrap = plataforma.getParent();
System.out.println(aplicacion);
System.out.println(plataforma);
System.out.println(bootstrap);
// Rutas del bootstrap (JDK 8)
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (URL u : urls) {
System.out.println(u);
}
// Directorios de extensiones / plataforma
System.out.println(System.getProperty("java.ext.dirs"));
// Classpath de la aplicación
System.out.println(System.getProperty("java.class.path"));
El orden de los valores impresos dependerá de la versión del JDK, pero la idea permanece: cada nivel tiene un dominio bien definido.
Delegación: consultar arriba, resolver abajo
Cuando se solicita una clase, la JVM no intenta cargarla inmediatamente desde el cargador actual. En su lugar, se sube la petición por la jerarquía y, si nadie la resuelve, se baja intentando cargar en cada nivel. El flujo se puede resumir así:
AppClassLoaderpregunta si ya ha cargado la clase confindLoadedClass; si no, delega a su padre.- El cargador de plataforma hace lo mismo y delega al de arranque.
- El cargador de arranque intenta cargar la clase desde sus librerías. Si lo consigue, devuelve la clase.
- Si falla, el cargador de plataforma intenta cargar la clase.
- Si tampoco lo consigue, finalmente
AppClassLoaderbusca en elclasspathde la aplicación.
Es decir, la regla es: primero el padre, luego el hijo.
Implementación típica de loadClass
protected Class<?> loadClass(String className, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(className)) {
// 1. ¿Ya fue cargada por este cargador?
Class<?> loaded = findLoadedClass(className);
if (loaded == null) {
// 2. Delegar hacia arriba
if (parent != null) {
loaded = parent.loadClass(className, false);
} else {
loaded = findBootstrapClassOrNull(className);
}
// 3. Si nadie la cargó, intentar en este nivel
if (loaded == null) {
loaded = findClass(className);
}
}
if (resolve) {
resolveClass(loaded);
}
return loaded;
}
}
El método findLoadedClass evita cargar dos veces la misma clase en el mismo cargador, mientras que findBootstrapClassOrNull consulta al cargador de arrenque, cuya implementación es nativa.
¿Por qué utilizar la delegación?
- Seguridad: impide que una clase definida por el usuario sustituya a otra del núcleo. Si intentamos crear nuestro propio
java.lang.String, el cargador de arranque ya habrá cargado la clase oficial y la versión del usuario será ignorada. - Unicidad: como cada clase se identifica por su nombre completamente calificado y su cargador definidor, evitar cargas duplicadas garantiza que exista una única definición dentro del mismo cargador.
Delegación completa
El principio de delegación completa indica que, si un cargador carga una clase, también es responsable de cargar las clases de las que depende, salvo que se indique explícitamente otro cargador. Esto simplifica la resolución de dependencias dentro del mismo contexto.
Cargador personalizado
Para crear un cargador propio se extiende ClassLoader y se sobrescribe findClass, no loadClass, de modo que se preserve la lógica de delegación. El padre por omisión será AppClassLoader.
public class CargadorPropietario extends ClassLoader {
private final Path directorio;
public CargadorPropietario(Path directorio) {
this.directorio = directorio;
}
@Override
protected Class<?> findClass(String nombre) throws ClassNotFoundException {
try {
String ruta = nombre.replace('.', File.separatorChar) + ".class";
byte[] bytes = Files.readAllBytes(directorio.resolve(ruta));
return defineClass(nombre, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("No se pudo cargar " + nombre, e);
}
}
}
Romper la delegación
Existen contenedores como Tomcat que no pueden seguir el modelo estricto porque deben:
- Aislar las clases de cada aplicación web, de modo que dos apps puedan usar versiones distintas de una misma librería.
- Compartir librerías comunes entre aplicaciones para no duplicar memoria.
- Separar las clases propias del contenedor de las de las aplicaciones.
- Permitir recargar JSP sin reiniciar el servidor, lo que exige descartar y volver a cargar clases generadas.
Para lograrlo, Tomcat usa una jerarquía propia de cargadores, donde cada aplicación dispone de su propio cargador y puede preferir clases propias antes de delegar, invirtiendo parcialmente la regla.