La Máquina Virtual de Java (JVM) carga clases desde archivos bytecode (.class) en memoria, creando objetos de clase correspondientes. Este proceso se denomina mecanismo de carga de clases.
Etapa del Proceso
El flujo incluye: Carga -> Enlace -> Inicialización.
- Carga: Localización del archivo a través del nombre completo de la clase.
- Enlace: Compone verificación, preparación y resolución.
- Inicialización: Asigna valores a variables estáticas y ejecuta bloques de inicialización estática.
Detalles de Inicialización
- La inicialización se produce tras la carga y el enlace.
- Si existe una superclase no inicializada, se procesa primero.
- Las sentencias estáticas se ejecutan secuencialmente.
Condiciones de Activación
La inicialización se desencadena por referencias activas y pasivas.
Referencias activas incluyen:
- Creación de instancias (new, reflexión, deserialización, clon).
- Acceso o modificación de variables estáticas.
- Invocación de métodos estáticos.
- Uso de reflexión con Class.forName.
- Inicialización de subclases, lo que implica la de la superclase.
- Ejecución del método main.
Ejemplos de Referencias Pasivas
En ciertos escenarios, la clase no se inicializa:
- Acceder a un campo estático de la superclase desde una subclase no activa la subclase.
class TestCarga {
public static void main(String[] args) {
System.out.println(ChildClass.constValue);
}
}
class ParentClass {
static {
System.out.println("ParentClass inicializada");
}
public static int constValue = 42;
}
class ChildClass extends ParentClass {
static {
System.out.println("ChildClass inicializada");
}
}
Salida esperada:
ParentClass inicializada
42
- Definir un arreglo de la clase no la inicializa.
class TestCarga {
public static void main(String[] args) {
ParentClass[] arreglo = new ParentClass[8];
}
}
No se genera salida, la clase se carga pero no inicializa.
- Las constantes finales se replican en la tabla de constantes de la clase invocadora, evitando la inicialización de la clase original.
Si constValue se declara como static final, solo se imprime su valor.
Cargadores de Clases
Los cargadores de clases implementan el mecanismo de carga. Los cargadores predeterminados del JDK son:
- Bootstrap ClassLoader: Cargador raíz, implementado en C/C++, gestiona clases esenciales de Java.
- Extension ClassLoader: Carga clases de extensiones del JDK.
- Application ClassLoader: Carga clases de la aplicación del usuario.
Motivos para Cargadores Personalizados
- Aislamiento de entornos (ej. en contenedores web como Tomcat).
- Carga desde fuentes de datos no convencionales.
- Prevención de la exposición del código fuente.
Modelo de Delegación Biparental
El modelo implica delegar la carga hacia arriba y luego intentar hacia abajo. Sus ventajas:
- Evita la carga duplicada de clases.
- Protege el entorno central de Java de manipulaciones.
Áreas de Memoria en Tiempo de Ejecución
La JVM estructura la memoria en regiones compartidas y privadas.
Regiones Compartidas
Accesibles por todos los hilos:
- Montículo (Heap): Principal espacio para objetos. Se divide en generaciones joven y vieja. La generación joven se subdivide en Eden, Survivor0 y Survivor1.
- Área de Métodos: Almacena metadatos de clases, incluyendo constantes y estructuras. En JDK 1.7 se conocía como PermGen, y en 1.8 como MetaSpace.
- Tabla de Constantes: Parte integral del área de métodos.
Regiones Privadas
Exclusivas por hilo:
- Contador de Programa (PC): Registra la dirección del bytecode actual para hilos Java; undefined para métodos nativos.
- Pila de la JVM: Contiene marcos de pila con variables locales y pila de operandos.
- Pila de Métodos Nativos: Utilizada para métodos escritos en lenguajes nativos.
Categorías de Recolección de Basura
La recolección de basura (GC) se clasifica en:
- Minor GC: Recolección en la generación joven.
- Major GC: Recolección en la generación vieja.
- Full GC: Recolección completa de todas las generaciones.
- Stop-The-World: Suspensión de todos los hilos durante el proceso de GC.
Parámetros de Configuración de la JVM
Parámetros comunes:
| Parámetro | Función |
|---|---|
| -Xms | Tamaño inicial del montículo |
| -Xmx | Tamaño máximo del montículo |
| -Xmn | Dimensión de la generación joven |
| -XX:SurvivorRatio | Proporción entre Eden y Survivor en la generación joven |
| -XX:PermSize | Tamaño inicial del área de métodos (obsoleto) |
| -XX:MaxPermSize | Tamaño máximo del área de métodos (obsoleto) |
| -XX:MetaspaceSize | Umbral de GC para MetaSpace (JDK 1.8+) |
| -XX:MaxMetaspaceSize | Tamaño máximo de MetaSpace |
| -Xss | Dimensión de la pila por hilo |
| -XX:MaxDirectMemorySize | Capacidad de la memoria directa |
Principios y Pasos para la Optimización de la JVM
La optimización de la JVM no es una práctica de rutina; se debe priorizar la mejora del código. Solo se recurre a ajustes de JVM tras un análisis exhaustivo de métricas de rendimiento.
Escenarios que Requieren Optimización
- El uso del montículo se acerca constantemente al límite máximo.
- Full GC ocurre con frecuencia elevada.
- Las pausas de GC superan el umbral de tolerancia (ej. más de un segundo).
- Excepciones de tipo OutOfMemory.
- Empleo intensivo de cachés locales que consumen mucha memoria.
- Caída en el throughput o aumento de la latencia del sistema.
Metodología de Optimización
- Examinar logs de GC y volcados de memoria para identificar puntos críticos.
- Definir objetivos cuantitativos, como reducir la latencia o aumentar el throughput.
- Seleccionar parámetros de JVM basados en datos históricos.
- Ajustar aspectos de memoria, latencia y throughput de forma secuencial.
- Comparar el rendimiento antes y después de los cambios.
- Iterar hasta alcanzar una configuración estable y eficiente.
- Implementar la configuración óptima en todos los entornos y monitorizar continuamente.