W3docs

Arquitectura de la JVM de Java

Cómo está estructurada la JVM — cargador de clases, áreas de datos en tiempo de ejecución, motor de ejecución e interfaz nativa.

Arquitectura de la JVM de Java

La máquina virtual de Java es el programa que ejecuta su programa. Usted compila el código fuente .java a bytecode .class independiente de la plataforma, y la JVM es lo que carga ese bytecode, lo verifica, dispone sus objetos en memoria y lo ejecuta sobre hardware real. «Write once, run anywhere» significa en realidad «compila una vez, y deja que la JVM de cada plataforma haga el resto». Este capítulo cartografía la estructura interna de la JVM — los tres subsistemas que comparte toda implementación — para que los capítulos siguientes sobre memoria y recolección de basura tengan un marco al que aferrarse.

Tres subsistemas

Una JVM, sea cual sea el proveedor, se organiza en tres subsistemas que cooperan. Todo lo demás es un detalle dentro de uno de ellos.

SubsistemaResponsabilidad
Cargador de clasesEncuentra, carga, enlaza e inicializa los archivos .class en el tiempo de ejecución
Áreas de datos en tiempo de ejecuciónLa memoria que gestiona la JVM: montículo, pilas, área de métodos, registros PC
Motor de ejecuciónInterpreta y compila en JIT el bytecode, y ejecuta el recolector de basura

El cargador de clases trae los tipos hacia dentro, las áreas de datos en tiempo de ejecución mantienen el estado, y el motor de ejecución ejecuta el código. Una llamada a un método toca los tres: su clase se carga, su marco se apila en una pila, y su bytecode se ejecuta.

El subsistema del cargador de clases

No todas las clases se cargan al arrancar. La JVM carga una clase de forma perezosa, la primera vez que se la referencia, en tres fases: carga (leer los bytes), enlazado (verificar el bytecode, preparar los campos estáticos, resolver las referencias) e inicialización (ejecutar los inicializadores estáticos y los bloques static { }).

Los cargadores forman una jerarquía «padre primero». Cuando se le pide una clase, un cargador delega hacia arriba a su padre antes de intentarlo él mismo — de modo que un tipo fundamental como String siempre proviene del cargador bootstrap de confianza y nunca puede ser eclipsado por el código de la aplicación.

// Recorrer la cadena de cargadores de cualquier clase
ClassLoader loader = MyType.class.getClassLoader();
while (loader != null) {
  System.out.println(loader.getName());
  loader = loader.getParent();
}
// Un resultado null significa el cargador bootstrap (nativo, sin objeto Java).
System.out.println(String.class.getClassLoader()); // imprime: null

Los tres cargadores estándar, de hijo a padre, son el cargador de aplicación (classpath), el cargador de plataforma (módulos del JDK como java.sql) y el cargador bootstrap (el núcleo java.base, implementado en código nativo, representado como null).

Áreas de datos en tiempo de ejecución

Una vez cargada una clase, su código y sus datos viven en regiones que la JVM particiona para fines distintos:

  • Montículo (heap) — compartido entre todos los hilos; cada objeto y arreglo vive aquí. Es lo que gestiona el recolector de basura.
  • Pilas de la JVM — una por hilo. Cada llamada a un método apila un marco que contiene las variables locales y los operandos; el marco se desapila al retornar. La recursión profunda lo desborda (StackOverflowError).
  • Área de métodos (Metaspace en las JVM modernas) — metadatos de clases, el pool de constantes en tiempo de ejecución y los campos estáticos. Memoria fuera del montículo.
  • Registro PC — por hilo; la dirección de la instrucción de bytecode que se ejecuta en ese momento.
// Asignación en el montículo: 'new' recorta espacio del montículo
byte[] buffer = new byte[1024]; // vive en el montículo, gestionado por el GC

// Crecimiento de la pila: cada llamada añade un marco a la pila de este hilo
static long factorial(long n) {
  return n <= 1 ? 1 : n * factorial(n - 1); // cada llamada = un marco más
}

Montículo frente a fuera-del-montículo es la separación clave: las instancias de objetos están en el montículo; las estructuras de clases y la propia maquinaria, no.

El motor de ejecución

El motor de ejecución convierte el bytecode en acción. Las JVM HotSpot modernas son adaptativas: empiezan interpretando el bytecode (arranque rápido), perfilan qué métodos se ejecutan en caliente y luego los entregan al compilador Just-In-Time (JIT), que emite código máquina nativo optimizado. El código frío permanece interpretado; el código caliente se compila — usted paga el coste de optimización solo donde se rentabiliza.

El motor también aloja el recolector de basura, que recupera objetos del montículo ya no alcanzables desde ninguna referencia viva, y la Java Native Interface (JNI), el puente hacia bibliotecas escritas en C/C++.

// Un bucle caliente: el JIT compilará sum() a código nativo tras suficientes llamadas
long total = 0;
for (int i = 0; i < 100_000_000; i++) {
  total += i; // al principio interpretado, luego compilado por el JIT, luego mucho más rápido
}

Un ejemplo trabajado: inspeccionar la JVM en vivo

Este programa no configura nada — le pide a la JVM en ejecución que se describa a sí misma a través de los beans de java.lang.management y la API del cargador de clases. Nombra la VM y el motor de ejecución, recorre la jerarquía de cargadores, informa la memoria del montículo frente a la de fuera del montículo, y hace crecer la pila con recursión.

java— editable, runs on the server

Qué llevarse de la ejecución:

  • El RuntimeMXBean nombra el motor de ejecución — una VM de la familia HotSpot (la línea vm name muestra algo como «OpenJDK 64-Bit Server VM») — confirmando que es el motor «Server» con capacidad JIT el que ejecuta su bytecode, y no un mero intérprete.
  • La cadena de cargadores imprime algo como <unnamed> -> app -> platform -> bootstrap(null): cada paso del bucle ascendió al padre, y la cadena terminó en un padre null — el cargador bootstrap, que es código nativo sin objeto Java. La jerarquía es real y observable, no una metáfora.
  • String.class.getClassLoader() es null — los tipos fundamentales de java.base provienen de ese mismo cargador bootstrap en la cima de la cadena, que es exactamente la razón por la que el código de la aplicación nunca puede sustituir su propia String. La delegación padre-primero hace su trabajo.
  • Las líneas de memoria muestran el montículo usado en KB pero un montículo máximo en MB, y una cifra separada de fuera-del-montículo: las instancias de objetos viven en el montículo gestionado por el GC, mientras que los metadatos de clases (Metaspace) se cuentan como fuera del montículo — las dos regiones se rastrean de forma independiente.
  • depth(1) devuelve 5 porque cada llamada recursiva apiló su propio marco en la pila de la JVM de este hilo y lo desapiló al retornar; la pila es por hilo y está estructurada en marcos, por lo que una recursión descontrolada termina en StackOverflowError en lugar de corromper el montículo.

Práctica

Práctica

Un programa Java referencia la clase 'java.lang.String'. En la jerarquía estándar de cargadores de clases «padre primero», ¿qué cargador la proporciona en última instancia, y cómo aparece ese cargador desde el código Java?