W3docs

Carga de clases en Java

Cómo la JVM encuentra y carga clases con cargadores — bootstrap, platform, system y cargadores personalizados.

Antes de que la JVM pueda ejecutar una sola línea de tu código, debe encontrar el archivo .class, leer su bytecode, verificarlo y convertirlo en un objeto Class vivo en memoria. Esa tarea pertenece a un cargador de clases (class loader). La carga de clases es lo que hace que java.lang.String esté disponible sin que hagas nada, lo que permite que un JAR en el classpath aparezca en tiempo de ejecución, y lo que impulsa sistemas de plugins, servidores de aplicaciones y herramientas de recarga en caliente. Este capítulo muestra cómo están organizados los cargadores, cómo funciona la delegación y por qué la identidad de una clase es más que solo su nombre.

La jerarquía de cargadores de clases

Los cargadores están organizados como una cadena de padres, cada uno responsable de una fuente diferente de clases. En un JDK moderno (9+) hay tres cargadores integrados:

CargadorCargaSe reporta como
BootstrapClases del núcleo del JDK (java.*, módulos base de javax.*)null
PlatformEl resto de los módulos de la plataforma JDKun PlatformClassLoader
System / ApplicationTu código desde el classpath/module pathun AppClassLoader

Cada clase recuerda el cargador que la definió. Puedes preguntarle a cualquier clase qué cargador la produjo:

ClassLoader appLoader = MyApp.class.getClassLoader();   // AppClassLoader
ClassLoader strLoader = String.class.getClassLoader();  // null = bootstrap
ClassLoader parent    = appLoader.getParent();          // PlatformClassLoader

El cargador bootstrap está escrito en código nativo, no en Java, por eso String.class.getClassLoader() devuelve null en lugar de un objeto — no hay una instancia Java de ClassLoader que devolver.

El modelo de delegación

Los cargadores de clases siguen el modelo de delegación padre-primero. Cuando se le pide cargar una clase, un cargador no intenta encontrarla de inmediato. Primero le pregunta a su padre, que le pregunta a su padre, hasta llegar al bootstrap. Solo si ningún ancestro puede suministrar la clase, el cargador original intenta definirla por sí mismo.

// Conceptual shape of ClassLoader.loadClass:
protected Class<?> loadClass(String name, boolean resolve) {
  Class<?> c = findLoadedClass(name);     // already loaded? reuse it
  if (c == null) {
    try {
      c = parent.loadClass(name);         // delegate UP first
    } catch (ClassNotFoundException e) {
      c = findClass(name);                // only now load it myself
    }
  }
  return c;
}

Esta delegación garantiza que los tipos del núcleo se carguen una sola vez, por el cargador más alto que pueda suministrarlos. Por eso no puedes sobrescribir java.lang.String colocando tu propio String.class en el classpath — el cargador bootstrap reclama el nombre primero.

Carga, enlace e inicialización

Llevar una clase a la vida ocurre en tres fases, y no son lo mismo:

  • Carga — leer el bytecode y crear el objeto Class.
  • Enlaceverificar que el bytecode está bien formado, preparar los campos estáticos con valores predeterminados y resolver referencias simbólicas.
  • Inicialización — ejecutar los inicializadores estáticos y las asignaciones de campos estáticos (el método <clinit> de la clase).

El dato práctico clave: la inicialización es perezosa y ocurre exactamente una vez. Una clase solo se inicializa en el primer uso activo — el primer new, la primera llamada a un método estático, o la primera lectura de un campo estático no constante.

class Config {
  static final Map<String, String> SETTINGS = load();  // runs once, on first touch
  static Map<String, String> load() {
    System.out.println("Config initialized");
    return Map.of("env", "prod");
  }
}
// "Config initialized" prints only when Config is first actively used.

Cargadores de clases personalizados

Puedes extender ClassLoader para cargar clases desde cualquier lugar — una base de datos, un flujo de red, bytecode generado o un JAR cifrado. Los dos métodos importantes son findClass (localizar y definir los bytes) y defineClass (entregar los bytes crudos a la JVM, que devuelve una Class).

class BytesLoader extends ClassLoader {
  private final byte[] bytecode;
  BytesLoader(byte[] bytecode) { this.bytecode = bytecode; }
  @Override
  protected Class<?> findClass(String name) {
    return defineClass(name, bytecode, 0, bytecode.length);
  }
}

URLClassLoader es la versión integrada de esta idea — apúntalo a JARs o directorios y carga clases bajo demanda:

URL jar = Path.of("plugin.jar").toUri().toURL();
try (URLClassLoader loader = new URLClassLoader(new URL[]{ jar })) {
  Class<?> plugin = loader.loadClass("com.example.Plugin");
  Object instance = plugin.getDeclaredConstructor().newInstance();
}

Identidad de clase: nombre más cargador

Aquí está la sutileza que confunde a la gente: la identidad en tiempo de ejecución de una clase es su nombre completamente calificado y el cargador que la definió. Carga bytes de Widget a través de dos cargadores distintos y obtienes dos objetos Class distintos — no iguales, no compatibles para asignación — aunque ambos provengan de bytecode idéntico. Así es exactamente como los servidores de aplicaciones aíslan dos apps desplegadas que ambas incluyen una clase llamada com.acme.Util.

Un ejemplo elaborado: cargadores, delegación, pereza e identidad

Este programa no necesita clases externas — usa los cargadores ya presentes en cualquier JVM. Recorre la cadena de cargadores, prueba que las clases del núcleo provienen del cargador bootstrap, muestra cómo la delegación devuelve el mismo objeto Class, observa cómo un inicializador estático se dispara perezosamente y solo una vez, y luego define el mismo bytecode hecho a mano a través de dos cargadores para probar la regla de identidad nombre-más-cargador.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • La cadena de cargadores impresa es la jerarquía de cargadores de clases en vivo con tu código en la parte inferior: ClassLoadingDemo fue definido por un cargador a nivel de aplicación cuyo getParent() es el siguiente cargador hacia arriba. Cada cargador solo conoce a su padre, y la cadena siempre sube hacia bootstrap.
  • String.class.getClassLoader() imprime null, la forma en que la JVM dice "cargado por el cargador bootstrap". Los tipos del núcleo del JDK siempre reportan null aquí; un objeto implicaría que vinieron de un cargador inferior, lo cual nunca ocurre.
  • app.loadClass("java.lang.StringBuilder") == StringBuilder.class es true. La delegación envió la solicitud hacia el cargador que ya posee StringBuilder, por lo que obtuviste el objeto Class idéntico, no un duplicado — prueba de que la delegación evita que los tipos del núcleo se carguen dos veces.
  • Lazy <clinit> running se imprime una sola vez, entre el marcador --- referencing Lazy now --- y el primer Lazy.VALUE = 42, y nunca más en la segunda lectura. La inicialización es perezosa (esperó hasta el primer uso) e idempotente (el bloque estático se ejecuta exactamente una vez por cargador).
  • a y b se llaman ambos Widget pero a == b es false y a.isAssignableFrom(b) es false. Dos cargadores definieron el mismo bytecode en dos tipos distintos — prueba concreta de que la identidad de clase en tiempo de ejecución es el nombre completamente calificado más el cargador que la define, el mecanismo detrás del aislamiento del classpath en servidores de aplicaciones.

Práctica

Práctica
Dos cargadores de clases personalizados independientes cargan bytecode idéntico para una clase llamada 'com.acme.Widget'. ¿Qué es verdad sobre los objetos Class resultantes a (del cargador 1) y b (del cargador 2)?
Dos cargadores de clases personalizados independientes cargan bytecode idéntico para una clase llamada 'com.acme.Widget'. ¿Qué es verdad sobre los objetos Class resultantes a (del cargador 1) y b (del cargador 2)?

Temas relacionados

La carga de clases se sitúa en la frontera entre la JVM y tu código, por lo que toca varios temas vecinos:

  • Arquitectura de la JVM — dónde encaja el subsistema de cargadores de clases entre el motor de ejecución y las áreas de datos en tiempo de ejecución.
  • Modelo de memoria de Java — cómo viven en memoria las clases cargadas y sus datos estáticos.
  • Recolección de basura — los cargadores de clases (y sus clases) pueden descargarse cuando ya no están referenciados.
  • Introducción a los módulos — en el module path, la carga se rige por la legibilidad de módulos en lugar de un classpath plano.
  • Introducción a la reflexiónClass.forName y loadClass son los puntos de entrada sobre los que se construye la reflexión.
Was this page helpful?