Volver
Featured image of post Java Virtual Machine

Java Virtual Machine

Java Virtual Machine (JVM)

¿Que es?

La Máquina Virtual de Java, en inglés Java Virtual Machine (JVM), es un componente dentro de JRE (Java Runtime Environment) necesario para la ejecución del código desarrollado en Java, es decir, es la máquina virtual la que permite ejecutar código Java en cualquier sistema operativo o arquitectura. De aquí que se conozca Java como un lenguaje multiplataforma. JVM interpreta y ejecuta instrucciones expresadas en un código máquina especial (bytecode), el cual es generado por el compilador de Java (también ocurre con los generados por los compiladores de lenguajes como Kotlin y Scala). Dicho de otra forma, es un proceso escrito en C o C++ que se encarga de interpretar el bytecode generado por el compilador y hacerlo funcionar sobre la infraestructura de ejecución. Como hay una versión de la JVM para cada entorno que sí conoce los detalles de ejecución de cada sistema, puede utilizar el código máquina equivalente para cada una de las instrucciones bytecode.

Java tiene dos componentes principales:

  • Java Runtime Environment (JRE): es el entorno de ejecucción de Java. Incluye la máquina virtual (JVM), las librerías básicas del lenguaje y otras herramientas relacionadas.
  • Java Delopment Kit (JDK); además del JRE, incluye el compilador, el debugger, el empaquetador JAR, herramientas para generar documentación, etc

Por lo que cuando queremos ejecutar un fichero java ocurre lo siguiente:

  1. Un fichero example.java se compilar (gracias al compilador del JDK) y se genera un fichero example.class que contiene el bytecode capaz de ser interpretado por la JVM.
  2. Durante la ejecución del código, Class Loader se encarga de llevar los ficheros .class a las JVM que reside en la RAM del ordenador y ByteCode Verifier se encarga de verificar que el código en bytecode procede de una compilación válida
  3. El compilador just in time compila el bytecode a código nativo de la máquina y se ejecuta directamente

En el siguiente apartado veremos más a fondo la definición de cada uno de estos subsistemas de la JVM.

Estructura

La JVM se descompone en 3 subsistemas:

Class loader subsystem

Cuando una clase Java necesita ser ejecutada, existe un componente llamado Java Class Loader Subsystem que se encarga de cargar, vincular e inicializar de forma dinámica y en tiempo de ejecución las distintas clases en la JVM. Se dice que el proceso es dinámico porque la carga de los ficheros se hace gradualmente, según se necesiten.

Carga

Existen a su vez tres tipos de Loaders y cada uno tiene una ruta predefinida desde donde cargar las clases.

  1. Bootstrap/Primordial ClassLoader: es el padre de los loaders y su función es cargar las clases principales desde jre/lib/rt.jar, fichero que contiene las clases esenciales del lenguaje
  2. Extensión ClassLoader: delega la carga de clases a su padre (bootstrap) y, en caso fallido, las carga el mismo desde los directorios de extensión de JRE (jre/lib/ext)
  3. System/Application ClassLoader: es responsable de cargar clases específicas desde la variable de entorno CLASSPATH o desde la opción por línea de comandos -cp.

Vínculo

Linking es el proceso de añadir los bytecodes cargados de una clase en el Java Runtime System para que pueda ser usado por la JVM. Existen tres paso en el proceso de Linking, aunque el último es opcional.

  1. Verify: Bytecode Verifier comprueba que el bytecode generado es correcto. En caso de no serlo, se devuelve un errror.
  2. Prepare: una vez se ha verificado, se procede a asignar memoria a las variables de las clases y se inicializan con valores por defecto dependiendo de su tipo. Las variables de clases no toman su valor inicial correcto hasta la fase de Initialization. Valores por defecto de variables primitivas:
    ➡️ int = 0
    ➡️ long = 0L
    ➡️ short = (short) 0
    ➡️ char = “\u0000”
    ➡️ byte = (byte) 0
    ➡️ boolean = false
    ➡️ reference = null
    ➡️ float = 0.0f
    ➡️ double = 0.0d
  3. Resolve: JVM localiza las clases, interfaces, campos y métodos referenciados en una tabla llamada constant pool (CP) y determina los valores concretos a partir de su referencia simbólica. Cuando se compila una clase Java, todas las referencias a variables y métodos se almacenan en el CP como referencia simbólica. Una referencia simbólica, de forma muy breve, es un string que puede usarse para devolver el objeto actual. El CP es un área de memoria con valores únicos que se almacenan para reducir la redundancia. Para el siguiente ejemplo:
    System.err.println("Test");
    System.out.println("Test");
    en el CP solo habría un objeto, String “Test”

Inicialización

Se encarga de que las variables de clase se inicialicen correctamente, con los valores que el desallorador especificó en el código.

Runtime Data Areas

JVM define varias áreas de datos que se utilizan durante la ejecución de un programa y que se podrían dividir en dos grupos. Algunas de estas áreas se crean al inicializarse la JVM y se destruyen una vez la JVM finaliza (compartidas por todos los hilos). Otras se inicializan cuando el hilo se crea y se destruyen cuando el hilo se ha completado (una por hilo).

  • Method area: es parte de Heap Area. Contiene el esqueleto de la clase (métodos, constantes, variables, atributos, constructor, etc)
  • Heap area: fragmento de memoria donde se almacenan los objetos creados (todo lo que se inicialice con el operador new). Si el objeto se borra, el Garbage Collector se encarga de liberar su espacio. Solo hay un Heap Area por JVM, por lo que es un recurso compartido (igual que Method Area)
  • Stack area: fragmento de memoria donde se almacenan las variables locales, parámetros, resultados intermedios y otros datos. Cada hilo tiene una private JVM stack, creada al mismo tiempo que el hilo.
  • PC register: contiene la dirección actual de la instrucción que se está ejecutando (una por hilo)
  • Native Method Stack: igual que Stack, pero para métodos nativos, normalmente escritor en C o C++.

Execution Engine

El bytecode que es asignado a las áreas de datos en la JVM es ejecutado por el Execution Engine, ya que este puede comunicarse con distintas áreas de memoria de la JVM. El Execution Engine tiene los siguientes componentes.

  • Interpreter: es el encargado de ir leyendo el bytecode y ejecutar el código nativo correspondiente. Esto afecta considerablemente al rendimiento de la aplicación.
  • JIT Compiler: interactúa en tiempo de ejecucción con la JVM para compilar el bytecode a código nativo y optimizarlo. Esto permite mejorar el rendimiento del programa. Esto se hace a través del HotSpot compiler.

  • Garbage Collector: libera zonas de memoria que han dejado de ser referenciadas por un objeto

Empaquetar y ejecutar aplicación Java

Ya hemos visto la parte teórica de como funciona la JVM y ahora toca realizar un ejemplo práctico.

Para compilar una app usaremos el javac dentro del directorio bin de nuestra instalación

javac MyApp.java

Esto generar archivos .class a partir de nuestro archivo fuente .java. Estos son los archivos que puede ejecutar la máquina virtual de Java.

Para ejecutar una aplicación usaremos java que lo podemos encontrar en el directorio bin del JRE o JDK. Para ello, debemos hacer referencia a una clase que contenga un método estático main, el principal punto de entrada de las aplicaciones en Java. Además, si forma parte de un paquete debemos escribir la ruta completa desde la base del árbol.

java com.example.myApp.MyApp

Java permite empaquetar las aplicaciones y librerías en archivos comprimidos. De esta forma es más sencillo poder reutilizar el código a través de distintas aplicaciones o desplegar nuevas versiones de la aplicación. Estos archivos pueden ser:

  • JAR: librerías o aplicaciones de escritorio
  • WAR: aplicaciones web
jar cf jar-file files-to-package

La opción c indica que se desea crear el archivo y la opción f especifica el nombre del archivo. Este comando genera un comprimido .jar que contiene todas las clases que indiquemos, incluyendo directorios de forma recursiva. Además, genera un archivo de manifiesto. Si el archivo de manifiesto especifica el header Main-Class, podremos ejecutar la aplicación desde el archivo JAR de la siguiente forma:

java -jar jar-file

Los archivos JAR también pueden ser agregados al classpath, de forma que las aplicaciones puedan obtener sus dependencias al explorar dentro de su contenido. Es la principal forma de distribución de librerías. Normalmente, cuando descargamos una aplicación Java, esta trae sus propios JAR además de las dependencias.

GraalVM

Hablamos de una Virtual Machine que es una extensión de la JVM tradicional que permite ejecutar cualquier lenguaje en una única VM (JavaScript, R, Ruby, Python…). Soporta modos de ejecucción tales como la compilación ahead-of-time que permite un tiempo de arranque más rápido en aplicaciones Java, resultado en ejecutables que ocupan menos memoria.

🎯 Objetivos

  • Mejorar el rendimiento de los lenguajes basados en la máquina virtual de Java haciendo que tengan un rendimiento similar a los lenguajes nativos
  • Reducir el tiempo de arranque de las aplicaciones de la JVM mediante la compilación ahead-of-time (antes de tiempo) con GraalVM Native image
  • Permitir la integración de GraalVM en Oracle Database, Node.js, Android/iOS y otros

📰 Lenguajes y runtimes

  • GraalVM JavaScript: runtime de JavaScript (ECMAScript 2019), con soporte para Node.js
  • TruffleRuby: implementación de Ruby
  • FastR: implementación del lenguaje R

🔩 Componentes

  • GraaVM Compiler: se trata de un compilador JIT para Java
  • GraalVM Native Image: permite la compilcación ahead-of-time
  • Truffle Language Implementation Framework: depende de GraalVM SDK y permite implementar otros lenguajes en GraalVM
  • Instrumentation-based Tool Support: soporte para instrumentación dinámica, que es agnóstica del lenguaje

Una opción que puede ser interesante, llegando a posibilidad de tener varios lenguajes dentro de un mismo proyecto, y que tiene el suficiente cuerpo como para que sea comentada en otro post del blog.

Creado con Hugo
Tema Stack diseñado por Jimmy