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.
La Java Virtual Machine (JVM) es el programa que ejecuta tu programa. Tú compilas el código fuente .java a bytecode .class neutral para cada plataforma, y la JVM es la que carga ese bytecode, lo verifica, organiza sus objetos en memoria y lo ejecuta en hardware real. "Escribe una vez, ejecuta en cualquier lugar" significa en realidad "compila una vez y deja que la JVM de cada plataforma haga el resto". Este capítulo describe la estructura interna de la JVM — los tres subsistemas que comparte toda implementación — para que los capítulos sobre el modelo de memoria y la recolección de basura que siguen tengan un marco del que colgar los conceptos.
Tres subsistemas
Una JVM, independientemente del proveedor, está organizada en tres subsistemas que cooperan entre sí. Todo lo demás es un detalle dentro de uno de ellos.
| Subsistema | Responsabilidad |
|---|---|
| Cargador de clases | Encuentra, carga, enlaza e inicializa archivos .class en el entorno de ejecución |
| Áreas de datos en tiempo de ejecución | La memoria que gestiona la JVM: heap, pilas, área de métodos, registros PC |
| Motor de ejecución | Interpreta y compila JIT el bytecode, y ejecuta el recolector de basura |
El cargador de clases introduce los tipos, 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 frame se apila en una pila y su bytecode se ejecuta.
El subsistema cargador de clases
Las clases no se cargan todas al inicio. La JVM carga una clase de forma perezosa, la primera vez que se referencia, en tres fases: carga (leer los bytes), enlace (verificar el bytecode, preparar los campos estáticos, resolver referencias) e inicialización (ejecutar los inicializadores estáticos y los bloques static { }).
Los cargadores forman una jerarquía donde el padre tiene prioridad. Cuando se le pide una clase, un cargador delega hacia arriba a su padre antes de intentarlo él mismo — de modo que un tipo central como String siempre proviene del cargador bootstrap de confianza y nunca puede ser reemplazado por código de aplicación.
// Walk the loader chain of any class
ClassLoader loader = MyType.class.getClassLoader();
while (loader != null) {
System.out.println(loader.getName());
loader = loader.getParent();
}
// A null result means the bootstrap loader (native, no Java object).
System.out.println(String.class.getClassLoader()); // prints: nullLos tres cargadores estándar, de hijo a padre, son el cargador de aplicación (classpath), el cargador de plataforma (módulos JDK como java.sql) y el cargador bootstrap (java.base central, implementado en código nativo, representado como null). El capítulo sobre carga de clases cubre en detalle este ciclo de vida y el modelo de delegación; los módulos explican cómo se empaquetan java.base y sus compañeros.
Áreas de datos en tiempo de ejecución
Una vez que se carga una clase, su código y datos viven en regiones que la JVM divide para propósitos distintos:
- Heap — compartido entre todos los hilos; todo objeto y array reside aquí. Es lo que gestiona el recolector de basura.
- Pilas JVM — una por hilo. Cada llamada a un método apila un frame que contiene variables locales y operandos; el frame se desapila al retornar. La recursión profunda la desborda (
StackOverflowError). - Área de métodos (Metaspace en las JVM modernas) — metadatos de clases, el pool de constantes en tiempo de ejecución y campos estáticos. Memoria fuera del heap.
- Registro PC — por hilo; la dirección de la instrucción de bytecode que se está ejecutando actualmente.
// Heap allocation: 'new' carves space out of the heap
byte[] buffer = new byte[1024]; // lives on the heap, GC-managed
// Stack growth: each call adds a frame to this thread's stack
static long factorial(long n) {
return n <= 1 ? 1 : n * factorial(n - 1); // each call = one more frame
}La división clave es heap frente a no-heap: las instancias de objetos están en el heap; las estructuras de clases y la maquinaria en sí misma no. El capítulo sobre stack vs. heap contrasta estas dos regiones en detalle.
El motor de ejecución
El motor de ejecución convierte el bytecode en acción. Las JVM modernas de HotSpot son adaptativas: comienzan interpretando el bytecode (arranque rápido), perfilan qué métodos se ejecutan con más frecuencia y luego los entregan al compilador Just-In-Time (JIT), que genera código nativo de máquina optimizado. El código frío permanece interpretado; el código caliente se compila — solo pagas el costo de optimización donde se amortiza.
El motor también alberga el recolector de basura, que recupera los objetos del heap a los que ya no se puede acceder desde ninguna referencia activa, y la Java Native Interface (JNI), el puente a las bibliotecas escritas en C/C++.
// A hot loop: the JIT will compile sum() to native code after enough calls
long total = 0;
for (int i = 0; i < 100_000_000; i++) {
total += i; // interpreted at first, then JIT-compiled, then much faster
}Un ejemplo práctico: inspeccionando la JVM en ejecución
Este programa no configura nada — le pide a la JVM en ejecución que se describa a sí misma mediante 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 del cargador, informa sobre la memoria heap frente a la no-heap, y hace crecer la pila con recursión.
Lo que hay que extraer de la ejecución:
- El
RuntimeMXBeannombra el motor de ejecución — una VM de la familia HotSpot (la líneavm namemuestra algo como 'OpenJDK 64-Bit Server VM') — confirmando que el motor "Server" con capacidad JIT es el que ejecuta tu bytecode, no un simple intérprete. - La cadena del cargador imprime algo como
<unnamed> -> app -> platform -> bootstrap(null): cada iteración del bucle ascendió al padre, y la cadena terminó en un padrenull— 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()esnull— los tipos centrales dejava.baseprovienen de ese mismo cargador bootstrap en lo alto de la cadena, lo cual es exactamente la razón por la que el código de aplicación nunca puede sustituir su propioString. La delegación con prioridad al padre está haciendo su trabajo.- Las líneas de memoria muestran el heap usado en KB pero el heap máximo en MB, y una figura separada de no-heap: las instancias de objetos viven en el heap gestionado por el GC, mientras que los metadatos de clase (Metaspace) se contabilizan como no-heap — las dos regiones se rastrean de forma independiente.
depth(1)devuelve5porque cada llamada recursiva apilaba su propio frame en la pila JVM de este hilo y lo desapilaba al retornar; la pila es por hilo y está estructurada en frames, razón por la que la recursión descontrolada termina enStackOverflowErroren lugar de corromper el heap.