¿Qué es JVM?

¿Qué es JVM?

Angela Gomes. La JVM (Java Virtual Machine) es una máquina virtual que permite ejecutar programas Java. Interpreta el código de bytes generado por el compilador de Java y ejecuta las instrucciones en un entorno independiente de la plataforma. Esto significa que los programas Java pueden ejecutarse en diferentes sistemas operativos sin necesidad de volver a compilarlos.

La JVM funciona como una capa de abstracción entre el código Java y el sistema operativo subyacente. Es responsable de varias tareas, como cargar clases, administrar la memoria, realizar garbage collection (recolección de basura), ejecutar instrucciones Java y manejar excepciones.

Implementa la especificación de la máquina virtual Java, que define la estructura, el comportamiento y los requisitos de la JVM. Esta especificación permite que diferentes proveedores creen múltiples implementaciones de JVM.

Realiza la verificación del código de bytes en el momento de la carga de la clase para garantizar que el código cumpla con las reglas de seguridad e integridad del lenguaje Java. Esto incluye verificar el acceso a campos y métodos, verificar tipos, detectar excepciones no controladas y validar la estructura del código.

Admite la creación de agentes Java que se pueden adjuntar a un programa en ejecución para instrumentar y monitorear el comportamiento de la aplicación. Los agentes Java se pueden utilizar para realizar tareas como creación de perfiles, supervisión del rendimiento, modificación dinámica de código y más.

Admite funciones avanzadas de diagnóstico y solución de problemas, como la capacidad de generar heap dumps para análisis, perfiles de asignación de memoria, análisis de subprocesos y monitoreo de utilización de recursos.

Tiene una arquitectura de clasificación dinámica (Dynamic Class Loading) que permite cargar clases en tiempo de ejecución según condiciones específicas. Esto hace posible crear aplicaciones flexibles y dinámicas, donde las clases se pueden cargar según demanda, lo que reduce la sobrecarga de memoria.

Admite la creación y ejecución de subprogramas Java, que son pequeñas aplicaciones que se ejecutan en navegadores web. Los subprogramas de Java han sido populares en el pasado por proporcionar funcionalidad interactiva en páginas web.

Asimismo, acepta varias opciones de configuración y ajuste del rendimiento a través de argumentos de línea de comandos, variables de entorno y archivos de configuración. Esto permite a los administradores del sistema optimizar la JVM para satisfacer las necesidades específicas del entorno de ejecución.

Tiene una rica API llamada Java Native Interface (JNI), que permite la comunicación entre código Java y código nativo escrito en lenguajes como C y C++. Esto hace posible integrar bibliotecas nativas existentes y acceder a funciones del sistema operativo de bajo nivel.

Principales Componentes de JVM

  1. Class Loader (Cargador de Clases): Se encarga de cargar las clases necesarias en tiempo de ejecución.
  2. Memory Area (Área de Memoria): Es donde se asigna la memoria para diferentes propósitos, como el heap (donde se almacenan los objetos) y el stack (donde se ejecutan los métodos).
  3. Execution Engine (Mecanismo de Ejecución): Es responsable de ejecutar el código de bytes de Java. Existen diferentes tipos de mecanismos de aplicación, como la interpretación, just-in-time compilation (JIT) y la AOT (Ahead-of-Time) compilation.
  4. Garbage Collector (Recolector de Basura): Se encarga de gestionar la memoria, liberando objetos a los que el programa ya no hace referencia.

Carga de Clases

La carga de clases se produce en tres fases distintas:

  • Carga: En esta parte, el Class Loaderbusca, carga y lee los bytes de definición de clase. Los bytes de clase generalmente se almacenan en un archivo .class en el sistema de archivos o se pueden obtener de otras fuentes, como una base de datos o una red.
  • Vinculación:

Verificación: En este paso, la JVM comprueba si los bytes de clase son correctos y coherentes. Esto implica verificar la estructura de la clase, como por ejemplo si los campos y métodos a los que se hace referencia existen y tienen el acceso adecuado.

Preparación: En este paso, la JVM asigna espacio para los campos estáticos de la clase y los inicializa con valores predeterminados.

Resolución: En este paso, las referencias simbólicas de la clase se reemplazan por referencias directas a otros tipos (clases, interfaces o métodos). Esto implica resolver nombres de clases, interfaces y miembros.

  • Inicialización: En esta fase, el código estático de la clase se ejecuta para inicializar los campos estáticos y realizar otras inicializaciones necesarias. Este código se ejecuta solo una vez, cuando la clase se carga por primera vez o cuando se hace referencia a ella por primera vez.
  • Carga Dinámica de Clases: Además de la carga estándar, puede cargar clases dinámicamente en tiempo de ejecución utilizando el ‘Class.forName()’ o las API de Reflection. Esto permite que un programa cargue clases según condiciones específicas o incluso agregue nuevas clases en tiempo de ejecución.
  • Classpath: Classpath es un conjunto de ubicaciones (directorios o archivos JAR) especificados para que la JVM busque clases durante la carga. Determina dónde el Class Loader debes buscar las clases solicitadas. El Classpath se puede configurar a través de variables de entorno o especificarse en la línea de comando al iniciar un programa Java.

Class Loader: Funcionamiento

El Class Loader se activa cuando se hace referencia a una clase por primera vez durante la ejecución del programa. Busca la clase en diferentes ubicaciones, como el sistema de archivos local, directorios de bibliotecas o incluso en la red. Una vez encontrada, el cargador de clases carga la definición de clase en la memoria y la prepara para su uso.

La JVM tiene una jerarquía de Class Loaders que trabajan juntos para cargar las clases. La jerarquía se basa en padres e hijos, donde cada Class Loader tiene un padre excepto el Class Loader raíz. Cuando se hace referencia a una clase, el Class Loader actual intenta cargarlo y, si no puede, pasa la solicitud a su padre en la jerarquía. Esta cadena continúa hasta que la clase se encuentra y se carga correctamente o se produce una excepción, lo que indica que no se puede encontrar la clase.

La JVM posee un Class Loader patrón que se utiliza como punto de partida para cargar clases. Es responsable de cargar bibliotecas del sistema, clases principales de Java y otras clases de propósito general. El Class Loader predeterminado lo implementa la clase java.lang.ClassLoader y se puede ampliar para crear cargadores de clases personalizados.

Es posible crear Class Loaders personalizados en la JVM. Esto permite a los desarrolladores cargar clases de forma personalizada, como cargar clases desde una ubicación específica, descargarlas a través de la red o incluso generarlas dinámicamente en tiempo de ejecución. Los Class Loaders personalizados pueden resultar útiles en escenarios como complementos, entornos sandbox y aplicaciones de servidor.

El Class Loader utiliza un modelo de delegación para cargar clases. Esto significa que el Class Loader primero intenta delegar la responsabilidad de la carga al Class Loader padre antes de intentar cargar la clase usted mismo. Este enfoque ayuda a evitar la duplicación de la carga de clases y promueve la reutilización del código.

El Class Loader juega un papel importante en la carga y gestión de clases en tiempo de ejecución en la JVM. Permite que el programa Java acceda dinámicamente a las clases requeridas, brindando flexibilidad y modularidad a las aplicaciones.

Hay tres tipos principales de ClassLoader en Java:

  • Bootstrap Class Loader: El Bootstrap Class Loader, también conocido como Class Loader primordial, es el primer Class Loader para ser cargado por la JVM. Es responsable de cargar las clases principales de Java, como las que se encuentran en las bibliotecas del sistema (por ejemplo, rt.jar). El Bootstrap Class Loader se implementa en código nativo y no es posible sustituirlo ni ampliarlo.
  • Extension Class Loader: El Extension Class Loader es un Class Loader hijo del Bootstrap Class Loader. Se encarga de cargar las clases ubicadas en el directorio de la extensión classpath (jre/lib/ext) o definido a través de la variable de entorno java.ext.dirs. Ese Class Loader permite agregar funcionalidad adicional a Java a través de extensiones.
  • Application Class Loader: El Application Class Loader, conocido también como System Class Loader, es un Class Loader hijo de Extension Class Loader. Es responsable de cargar las clases de la aplicación en ejecución. El Application Class Loader se establece mediante el parámetro de línea de comando -classpath o la variable de entorno CLASSPATH. También se encarga de cargar clases personalizadas desarrolladas por el usuario.

Runtime Data Areas (Áreas de Datos en Tiempo de Ejecución)

Estas son áreas de memoria utilizadas por la JVM (Java Virtual Machine) mientras ejecuta un programa Java. Se encargan de almacenar diferentes tipos de datos e información necesarios para la correcta ejecución del programa. Estas son las áreas principales de los datos en tiempo de ejecución:

  • Method Area (Área de Métodos)

La Method Area Es un área de memoria compartida entre todos los subprocesos que se ejecutan en la JVM. Almacena información sobre las clases y métodos cargados, como la estructura de clases, campos, métodos y constantes. La Method Area también contiene las constantes de clase, como strings literales y valores numéricos constantes.

  • Heap

El Heap es el área de memoria donde se asignan los objetos Java durante la ejecución del programa. Es compartido entre todas las threads y es gestionado por el recolector de basura (garbage collector) de la JVM. El Heap se divide en dos regiones principales: Young Generation (Generación Joven) y Old Generation (Generación Antigua), que son responsables de gestionar objetos de corta duración y objetos de larga duración, respectivamente.

  • Stack

Java Stack es un área de memoria asociada a cada thread en ejecución. Almacena información sobre los métodos en ejecución, como parámetros y variables locales. Cada método invocado crea un nuevo marco en la pila de Java, que contiene información específica del método, como los valores de las variables locales y la referencia al método que se está ejecutando actualmente.

  • PC Register (Registrador de Contador de Programa)

El PC Register es un registro dedicado a cada thread. Contiene la dirección de memoria de la siguiente instrucción a ejecutar por el thread. El PC Register se actualiza con cada instrucción ejecutada y se utiliza para controlar el flujo de ejecución del programa.

  • Native Method Stack

El Native Method Stack es un área de memoria utilizada para ejecutar métodos nativos, que se implementan en lenguajes distintos de Java, como C o C++. Cada thread running tiene su propia pila de métodos nativos.

  • Direct Memory

La Direct Memory es un área de memoria que se utiliza para almacenar datos fuera del Heap, generalmente para operaciones que requieren acceso directo a la memoria, como operaciones de E/S (entrada/salida) y operaciones de red. La Direct Memory no es administrada por el recolector de basura y requiere la liberación explícita por parte de los programas Java.

Estas áreas de datos de tiempo de ejecución son esenciales para el funcionamiento adecuado de la JVM y se utilizan para almacenar información crítica durante la ejecución del programa Java, como estructuras de clases, objetos asignados, métodos de ejecución y registros de control de flujo.

Execution Engine (Mecanismo de Ejecución)

Es una parte crucial de la JVM (Java Virtual Machine), responsable de ejecutar el código de bytes de Java. Interpreta y ejecuta instrucciones en código de bytes, lo que permite que el programa Java se ejecute en cualquier plataforma compatible con JVM.

  • Interpretación

El Execution Engine utiliza un motor de interpretación para ejecutar el código de bytes de Java. Lee las instrucciones de código de bytes una por una y las convierte en acciones equivalentes o llamadas a métodos en la plataforma subyacente. El proceso de interpretación es relativamente lento, ya que cada instrucción debe interpretarse en tiempo real.

  • Just-in-Time Compilation (Compilación Just-in-Time - JIT)

Para mejorar el rendimiento, muchas JVM modernas utilizan Just-in-Time Compilation (compilación en tiempo de ejecución). El JIT Compiler (compilador JIT) convierte partes del código de bytes de Java en código de máquina nativo durante la ejecución del programa. Esto permite que las instrucciones sean ejecutadas directamente por la CPU, proporcionando un aumento significativo en el rendimiento.

  • HotSpot JVM: esta implementación de JVM proporcionado por Oracle, es conocido por su enfoque de optimización dinámica. Combina la interpretación inicial del código de bytes con la compilación JIT adaptativa. HotSpot JVM monitorea el rendimiento del código en tiempo real e identifica fragmentos de código ejecutados con frecuencia (llamados "hotspots"). Luego, estos puntos de acceso se compilan en código nativo optimizado para mejorar el rendimiento.
  • Profiling: El Execution Engine realiza el profiling (perfilado) del programa en ejecución para recopilar información sobre el comportamiento del código. Esto incluye la identificación de fragmentos de código ejecutados con frecuencia, el tiempo dedicado a cada método, la asignación de objetos, entre otros datos. Esta información es utilizada por el JIT Compiler para realizar optimizaciones específicas, como inserción de métodos, eliminación de código muerto y loops.
  • Manejo de Excepciones: El Execution Engine se ocupa de la gestión de excepciones durante la ejecución del programa Java. Cuando ocurre una excepción, el Execution Engine detiene la ejecución normal del programa y busca un bloque controlador adecuado para manejar la excepción. Si no se encuentra ningún bloque de manejo, el programa finaliza con un mensaje de error.

El Execution Engine es responsable de ejecutar el código de bytes de Java en una plataforma específica, asegurando que el programa Java se ejecute correctamente y optimizando el rendimiento siempre que sea posible. Su combinación de interpretación y compilación JIT hace que la ejecución de Java sea eficiente y portátil en diferentes sistemas operativos y arquitecturas de hardware.

Garbage Collector (Recoletor de Basura)

Es una parte esencial de la JVM (Java Virtual Machine) encargada de gestionar automáticamente la memoria y recuperar los objetos no utilizados, liberando el espacio que ocupan los mismos. El Garbage Collector automatiza el proceso de liberación de memoria, eliminando la necesidad de que el programador administre explícitamente la asignación y desasignación de memoria.

Manejo de Memoria: El Garbage Collector gestiona la memoria del programa Java, asignando espacio para objetos durante la ejecución y liberando automáticamente la memoria ocupada por objetos a los que ya no se hace referencia. Esto evita pérdidas de memoria y hace que el desarrollo de Java sea más conveniente, ya que el programador no tiene que preocuparse por desasignar memoria manualmente.

El Garbage Collector Identifica objetos a los que el programa Java ya no hace referencia. Un objeto se considera sin referencia si no se puede acceder a él desde ningún punto de acceso al programa. Esto se hace mediante técnicas como el recuento de referencias, la recolección de raíces y el análisis de alcance.

Algoritmos de Recolección de Basura: Existen diferentes algoritmos de recolección de basura implementados por los Garbage Collectors para determinar qué objetos serán liberados. Los algoritmos más comunes son el Mark and Sweep, Generational Collection y Stop-the-World Generational Collection. Cada algoritmo tiene sus propias estrategias para identificar y liberar objetos no utilizados.

  • Generational Collection: La mayoría de los Garbage Collectors moderno utiliza el concepto de coleccionar generaciones. En este modelo la memoria se divide en diferentes generaciones, como por ejemplo la Generación Joven (Young Generation) y la vieja generación (Old Generation). Los objetos recién creados se asignan a la generación joven, mientras que los objetos que sobreviven a múltiples recolecciones de basura se promueven a la generación vieja o antigua. La colección generacional permite optimizaciones específicas para cada tipo de objeto, lo que resulta en un mejor rendimiento.
  • Stop-the-World: Mientras se recolecta la basura, la mayoría de los Garbage Collectors pausa temporalmente la ejecución del programa Java, lo que se conoce como detener mundos (stop-the-world). Durante esta pausa, todos los hilos se suspenden mientras el Garbage Collector realiza sus operaciones de recolección de basura. Aunque esto puede causar breves interrupciones en la ejecución del programa, esta pausa está optimizada para minimizar su impacto en el rendimiento general.

La JVM ofrece opciones de configuración para ajustar el comportamiento del Garbage Collector, permitiendo optimizaciones personalizadas para satisfacer necesidades de aplicaciones específicas. Las opciones de configuración incluyen elegir el algoritmo de recolección de basura, el tamaño del montón, las políticas de promoción y limpieza, entre otras.

El Garbage Collector es una parte fundamental del entorno de ejecución de Java, que garantiza una gestión eficiente de la memoria y evita pérdidas de memoria. Alivia la carga del programador de asignar y desasignar memoria manualmente, permitiéndole centrarse en la lógica empresarial de la aplicación.

Ventajas de la JVM

  • Portabilidad

La JVM está diseñada para proporcionar portabilidad del código Java. Esto significa que un programa Java escrito una vez puede ejecutarse en cualquier sistema operativo o arquitectura de hardware que admita la JVM. Esto permite a los desarrolladores escribir aplicaciones Java una vez y ejecutarlas en diferentes plataformas sin tener que reescribir el código fuente.

  • Manejo de Memoria

La JVM gestiona automáticamente la asignación y desasignación de memoria a través del Garbage Collector. Esto simplifica el desarrollo al eliminar la necesidad de que el programador se ocupe explícitamente de la asignación y desasignación de memoria. El Garbage Collector Rastrea y libera automáticamente objetos no utilizados, evitando pérdidas de memoria y haciendo que el desarrollo de Java sea más eficiente y seguro.

  • Seguridad

La JVM está diseñada con sólidas funciones de seguridad. Implementa un modelo de seguridad basado en sandbox que restringe el acceso no autorizado a los recursos del sistema. Esto garantiza que las aplicaciones Java que se ejecutan en la JVM no puedan causar daños al sistema host, protegiendo contra amenazas como acceso no autorizado, corrupción de datos o ejecución de código malicioso.

  • Manejo de excepciones

La JVM tiene un sistema de gestión de excepciones robusto e integrado. Proporciona mecanismos para detectar, lanzar, capturar y manejar excepciones en tiempo de ejecución. Esto permite a los desarrolladores identificar y manejar errores y excepciones de forma estructurada, lo que facilita la depuración y el desarrollo de aplicaciones más sólidas y confiables.

  • Desempeño

Aunque JVM es una máquina virtual intermedia, proporciona muy buen rendimiento. La JVM utiliza técnicas avanzadas, como la compilación Just-in-Time (JIT), para convertir el código de bytes de Java en código nativo optimizado en tiempo de ejecución. Esto da como resultado un rendimiento comparable al de los lenguajes de programación nativos, lo que permite que las aplicaciones Java se ejecuten de manera eficiente y rápida.

  • Facilidad de Mantenimiento

La JVM y el lenguaje Java son conocidos por promover la escritura de código legible y mantenible. El lenguaje Java tiene una sintaxis clara y estandarizada, lo que hace que el código sea más comprensible y facilita la colaboración entre desarrolladores. Además, la JVM proporciona capacidades avanzadas de depuración, creación de perfiles y monitoreo, lo que facilita la identificación y solución de problemas en tiempo de ejecución.

  • Multithreading

La JVM admite programación de forma nativa multithread, permitiendo a los desarrolladores escribir aplicaciones Java altamente concurrentes y eficientes. La JVM gestiona la ejecución simultánea de subprocesos y proporciona mecanismos de sincronización, como bloqueos y semáforos, para garantizar la coherencia de los datos compartidos entre subprocesos. Esto permite la creación de aplicaciones escalables y de alto rendimiento que aprovechan las capacidades de procesamiento paralelo.

  • Integración con otros lenguajes

La JVM no es exclusiva del lenguaje Java. Admite otros lenguajes de programación como Kotlin, Scala, Groovy y muchos otros al compilar estos lenguajes en código de bytes de Java. Esto permite a los desarrolladores aprovechar la JVM, como el Garbage Collector, el rendimiento y el ecosistema, mientras utilizan otros lenguajes de programación.

  • Soporte a gran escala

JVM es capaz de manejar aplicaciones a gran escala y de alto rendimiento. Admite sistemas distribuidos, agrupación en clústeres y equilibrio de carga, lo que permite que las aplicaciones Java escale horizontalmente para satisfacer las crecientes demandas. Además, la JVM cuenta con herramientas avanzadas de gestión y supervisión del rendimiento, que ayudan a optimizar y ajustar el rendimiento de las aplicaciones en entornos de producción.

  • Ecosistema y Bibliotecas

La JVM tiene un ecosistema maduro y una amplia colección de bibliotecas y marcos disponibles. Esto facilita el desarrollo de aplicaciones Java, ya que los desarrolladores pueden aprovechar las bibliotecas existentes para tareas comunes como manipulación de archivos, acceso a bases de datos, comunicación de red, interfaces gráficas, entre otras. El ecosistema Java es rico en herramientas, IDE (entornos de desarrollo integrados) y recursos de soporte comunitario, lo que proporciona un entorno de desarrollo sólido y colaborativo.

Implementaciones de la JVM

Hay varias implementaciones de JVM disponibles que admiten el desarrollo y la ejecución de aplicaciones Java en diferentes plataformas.

  • Oracle HotSpot JVM

HotSpot JVM es la implementación estándar de Oracle y se usa ampliamente. Es conocido por su rendimiento y funciones avanzadas como el compilador. Just-in-Time (JIT) y el Garbage Collector de última generación.

  • OpenJDK

OpenJDK es una implementación de código abierto de la plataforma Java y JVM. Lo mantiene la comunidad Java y sirve como base para varias otras implementaciones, incluida HotSpot JVM de Oracle. OpenJDK se utiliza ampliamente y admite múltiples plataformas.

  • IBM J9 JVM

La JVM J9 de IBM es otra implementación de JVM popular. Es conocido por su bajo consumo de memoria y escalabilidad en entornos corporativos. La JVM J9 se utiliza en varias soluciones de IBM, como WebSphere Application Server.


Estas son sólo algunas de las implementaciones de JVM más populares. Cada uno de ellos tiene sus propias características y capacidades distintas, pero todos son compatibles con el estándar Java y le permiten ejecutar aplicaciones Java en diferentes plataformas.

En resumen, la JVM es un componente esencial para ejecutar aplicaciones Java, ya que ofrece portabilidad, rendimiento, seguridad y un conjunto completo de características para desarrollar y ejecutar aplicaciones sólidas y escalables.
Proporciona un entorno de ejecución potente y flexible para aplicaciones Java, proporcionando funciones avanzadas de gestión de memoria, optimización del rendimiento, seguridad y extensibilidad.

La JVM es un componente central de la plataforma Java y continúa evolucionando para satisfacer las demandas de las aplicaciones modernas. Es una tecnología compleja y versátil que admite una amplia gama de características y funcionalidades para ejecutar aplicaciones Java.

💡
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Listopro.

Listopro Community da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.