Bienvenidos a otro capítulo de este Curso Gratis de Java para Hackers – Máquina virtual Java (JVM). Comparte este articulo y síguenos para recibir más capítulos, guías y cursos gratis.

Tabla de contenidos

¿Te gustaría enterarte de cuando lanzamos descuentos y nuevos cursos?

Máquina virtual Java (JVM)

JVM es la abreviatura de Java Virtual Machine, que es una especificación que proporciona un entorno de ejecución en el que se puede ejecutar código de bytes de Java, es decir, es algo abstracto y su implementación es independiente de la elección del algoritmo y ha sido proporcionada por Sun y otras empresas.

Es la JVM la responsable de convertir el código Byte en código específico de la máquina. También puede ejecutar aquellos programas escritos en otros lenguajes y compilados en código de bytes de Java. La JVM realiza las tareas mencionadas: carga código, verifica código, ejecuta código y proporciona un entorno de ejecución.

Java Virtual Machine (JVM) es un motor que proporciona un entorno de ejecución para controlar el código o las aplicaciones Java. Convierte el código de bytes de Java en lenguaje de máquina. JVM es parte de Java Runtime Environment (JRE). En otros lenguajes de programación, el compilador produce código de máquina para un sistema en particular. Sin embargo, el compilador de Java produce código para una máquina virtual conocida como máquina virtual Java.

La Máquina Virtual Java

Junto al lenguaje de programación, también se desarrolló un procesador del lenguaje encargado de interpretar instrucciones en un código binario especial de Java. Este procesador es la Máquina Virtual de Java o JVM (Java Virtual Machine). Es el componente encargado de:

  • Proveer la independencia en cuanto al hardware y sistema operativo sobre los cuales pueden correr las aplicaciones Java.
  •  El pequeño tamaño del código compilado.
  •  Proteger a los usuarios de posibles aplicaciones maliciosas.

Todas las instrucciones que hacen a la constitución de un programa son escritas en archivos de texto plano con formato “.java”. Estos archivos contienen el código fuente de un programa. La JVM no conoce nada sobre el lenguaje Java, solamente conoce cómo interpretar archivos en un formato binario en particular (“.class”).

Para generar estos archivos binarios, los archivos fuentes son compilados utilizando una aplicación específica para este fin  (compilador javac).

Se deben compilar todos los archivos fuentes y por  cada uno de ellos tendremos el archivo binario correspondiente con  formato “.class”.

La compilación tiene los siguientes objetivos:

  • Revisar errores en la sintaxis del código fuente.
  • Convertir el código fuente en código binario.

Básicamente Java compila su código a lo que se conoce como código intermedio, que tiene el nombre de ByteCode, y que es como un código ensamblador pero inventado por Java.

Este código no es leído directamente por el microprocesador, ya que al no estar en código de máquina, este no lo entendería, por lo que primero pasa por algo que se conoce como máquina virtual de Java. Una máquina virtual es un software que simula una computadora, y puede ejecutar programas como si fuera una computadora real.

Esta computadora virtual no necesariamente tienen que tener equivalencia con el hardware real sobre el cual se la esta ejecutando. La máquina virtual de Java traduce el ByteCode inicialmente compilado por Java a código de máquina sobre la plataforma en la cual se está ejecutando el programa. La gente de Java tuvo que crear una máquina virtual para cada plataforma existente.

En casi todos los dispositivos se puede encontrar una versión de la máquina virtual de Java, y lo mejor de todo es que en muchos casos ésta viene instalada de fabrica, como por ejemplo en Android.

Compilador Just In Time (JIT)

Y como es que realiza la traducción la máquina virtual de Java? En principio podríamos decir que la traducción se hace mediante un intérprete, pero en realidad se hace con un concepto similar que se llama compilador Just in Time, y acá es donde esta el truco para que un programa de Java sea rápido.

El compilador Just in Time compila ByteCode a código de máquina en tiempo de ejecución, por lo que se preguntarán, hay unas diferencias con el intérprete.

Primero, el compilador Just in Time hace un recuento de los métodos que se usan frecuentemente, y los compila al iniciar la máquina virtual de Java. Luego los métodos que no son utilizados frecuentemente los compilará mucho más tarde e incluso nunca. Esto es así para que el inicio de la máquina virtual de Java sea rápido y luego siga teniendo un buen rendimiento.

¿Cómo funciona la JVM?

JVM (Java Virtual Machine) actúa como un motor de ejecución para ejecutar aplicaciones Java. JVM es la que realmente llama al método principal presente en un código Java.

Las aplicaciones Java se denominan WORA (Write Once Run Anywhere). Esto significa que un programador puede desarrollar código Java en un sistema y puede esperar que se ejecute en cualquier otro sistema habilitado para Java sin ningún ajuste. Todo esto es posible gracias a JVM.

Cuando compilamos un archivo .java , el compilador de Java genera archivos .class (contiene código de bytes) con los mismos nombres de clase presentes en el archivo .java . Este archivo .class consta de varios pasos cuando lo ejecutamos. Estos pasos juntos describen toda la JVM. 

  • Primero, el código Java se compila en código de bytes. Este código de bytes se interpreta en diferentes máquinas.
  • Entre el sistema host y la fuente Java, Bytecode es un lenguaje intermediario.
  • JVM en Java es responsable de asignar espacio de memoria.

Funcionamiento de la máquina virtual Java (JVM)

Arquitectura JVM

Ahora, en este tutorial de JVM, comprendamos la arquitectura de JVM. La arquitectura JVM en Java contiene un cargador de clases, un área de memoria, un motor de ejecución, etc.

Arquitectura de máquina virtual Java (veremos esto mas adelante en mas profundidad)

1) cargador de clases – Classloader

El cargador de clases es un subsistema utilizado para cargar archivos de clases. Realiza tres funciones principales a saber. Carga, vinculación e inicialización. Classloader es un subsistema de JVM que se utiliza para cargar archivos de clases. Siempre que ejecutamos el programa Java, el cargador de clases lo carga primero. Hay tres cargadores de clases integrados en Java.

  1. Bootstrap ClassLoader : este es el primer cargador de clases que es la superclase del cargador de clases de extensión. Carga el archivo rt.jar que contiene todos los archivos de clase de Java Standard Edition, como clases de paquetes java.lang, clases de paquetes java.net, clases de paquetes java.util, clases de paquetes java.io, clases de paquetes java.sql, etc.
  2. Extension ClassLoader : este es el cargador de clases secundario de Bootstrap y el cargador de clases principal del cargador de clases del sistema. Carga los archivos jar ubicados dentro del directorio $JAVA_HOME/jre/lib/ext .
  3. System/Application ClassLoader: este es el cargador de clases secundario del cargador de clases de extensión. Carga los archivos de clase desde classpath. De forma predeterminada, classpath está configurada en el directorio actual. Puede cambiar el classpath usando el modificador «-cp» o «-classpath». También se le conoce como cargador de clases de aplicaciones.

Ejemplo de código Java:

	//Veamos un ejemplo para imprimir el nombre del cargador de clases  
	clase pública  ClassLoaderExample   
	{  
	    principal vacío estático público  (String [] argumentos)    
	    {  
	        // Imprimamos el nombre del cargador de clases de la clase actual.   
	        //El cargador de clases de Aplicación/Sistema cargará esta clase  
	        Clase c=ClassLoaderExample. clase ;  
	        System.out.println(c.getClassLoader());  
	        //Si imprimimos el nombre del cargador de clases de String, imprimirá nulo porque es un  
	        //clase incorporada que se encuentra en rt.jar, por lo que se carga mediante el cargador de clases Bootstrap  
	        System.out.println(String. class .getClassLoader());  
	    }  
	}     


	//Let's see an example to print the classloader name  
	public class ClassLoaderExample  
	{  
	    public static void main(String[] args)  
	    {  
	        // Let's print the classloader name of current class.   
	        //Application/System classloader will load this class  
	        Class c=ClassLoaderExample.class;  
	        System.out.println(c.getClassLoader());  
	        //If we print the classloader name of String, it will print null because it is an  
	        //in-built class which is found in rt.jar, so it is loaded by Bootstrap classloader  
	        System.out.println(String.class.getClassLoader());  
	    }  
	}     

Producción:

sun.misc.Launcher$AppClassLoader@4e0e2f2a
null

Estos son los cargadores de clases internos proporcionados por Java. Si desea crear su propio cargador de clases, debe ampliar la clase ClassLoader.

2) Área de clase (método) «Class(Method) Area»

El área de clase (método) almacena estructuras por clase, como el grupo de constantes de tiempo de ejecución, datos de campos y métodos, y el código de los métodos. El área de métodos JVM almacena estructuras de clases como metadatos, el grupo de tiempo de ejecución constante y el código de los métodos.

3) Heap

Es el área de datos de tiempo de ejecución en la que se asignan los objetos. Todos los objetos , sus variables de instancia relacionadas y matrices se almacenan en el montón. Esta memoria es común y compartida entre varios subprocesos.

4) Pila (Stack)

Las pilas de lenguaje Java almacenan variables locales y sus resultados parciales. Cada subproceso tiene su propia pila JVM, creada simultáneamente a medida que se crea el subproceso. Se crea un nuevo marco cada vez que se invoca un método y se elimina cuando se completa el proceso de invocación del método.

Java Stack almacena marcos. Contiene variables locales y resultados parciales, y desempeña un papel en la invocación y retorno de métodos. Cada hilo tiene una pila JVM privada, creada al mismo tiempo que el hilo. Se crea un nuevo marco cada vez que se invoca un método. Una trama se destruye cuando se completa la invocación de su método.

5) Registro del contador del programa. (Program Counter Register)

El registro de PC (contador de programa) contiene la dirección de la instrucción de la máquina virtual Java que se está ejecutando actualmente. El registro de PC almacena la dirección de la instrucción de la máquina virtual Java que se está ejecutando actualmente. En Java, cada hilo tiene su registro de PC independiente.

6) Pila de métodos nativos (Native Method Stack)

Contiene todos los métodos nativos utilizados en la aplicación. Las pilas de métodos nativos contienen instrucciones de código nativo que dependen de la biblioteca nativa. Está escrito en otro idioma en lugar de Java.

7) Motor de ejecución (Execution Engine)

Es un tipo de software utilizado para probar hardware, software o sistemas completos. El motor de ejecución de pruebas nunca contiene información sobre el producto probado.

Contiene:

  1. Un procesador virtual
  2. Intérprete: lea el flujo de código de bytes y luego ejecute las instrucciones.
  3. Compilador Just-In-Time (JIT): se utiliza para mejorar el rendimiento. JIT compila partes del código de bytes que tienen una funcionalidad similar al mismo tiempo y, por lo tanto, reduce la cantidad de tiempo necesario para la compilación. Aquí, el término «compilador» se refiere a un traductor del conjunto de instrucciones de una máquina virtual Java (JVM) al conjunto de instrucciones de una CPU específica.

8) Interfaz nativa de Java

La interfaz del método nativo es un marco de programación. Permite que el código Java que se ejecuta en una JVM sea llamado por bibliotecas y aplicaciones nativas.

Java Native Interface (JNI) es un marco que proporciona una interfaz para comunicarse con otra aplicación escrita en otro lenguaje como C, C++, ensamblador, etc. Java usa el marco JNI para enviar resultados a la consola o interactuar con las bibliotecas del sistema operativo.

9) Bibliotecas de métodos nativos

Las bibliotecas nativas son una colección de bibliotecas nativas (C, C++) que necesita el motor de ejecución.

El compilador Just In Time

¿Y por qué no compilar todo el ByteCode de una vez en lugar de utilizar el compilador Just In Time? Por un lado no se sabe que tan grande es el programa, por lo tanto, no sería agradable tener que esperar 2, 5, 10, o quien sabe cuántos minutos para solo iniciar el programa. Por otro lado el compilador Just in Time implementa algunos trucos para hacer que la ejecución del programa termine siendo más rápida, por ejemplo, vamos a ver este código

Ejemplo de código Java:

 public test(int nro) {
    if(nro == 20) {
       // muchas líneas de código que hacen cosas
    } else if (nro >= 5) {
       // muchas otras líneas de código que hacen otras cosas
    } else {
        return;
    }

El compilador puede llamar 1 o 2 veces a este método y darse cuenta sobre la marcha que si el número que se ingresa en la función es menor a 5 entonces la función termina sin hacer nada, y terminar sin hacer nada significa que tuvo que perder tiempo preguntando si el número ingresado por parámetro es igual a 20, si es mayor o igual a 5, e incluso pueden haber muchas otras condiciones más anidadas. Entonces lo que hace el compilador Just in Time es reemplazar la llamada al método test(nro) por un código como

Ejemplo de código Java:

 if(nro>4){
   public test(int nro) {
    if(nro == 20) {
       // muchas líneas de código que hacen cosas
    } else if (nro >= 5) {
       // muchas otras líneas de código que hacen otras cosas
    } else {
        return;
    }
 }

Lo que elimina la llamada al método por completo si el número que se pasa es menor a 5. Así como sucede con este método, el compilador Just in Time  va analizando el código y hace este tipo de trucos en muchas otras partes del código. Si se quisiera pre-compilar el código, o sea, compilar todo de una sola vez antes de iniciar el programa, esto no sucedería, y todos esos if anidados se ejecutarían.

Obviamente como programador podría encargarme de hacer este tipo de comprobaciones y optimizar el código, pero escribir este tipo de código y tener que reconocer estos patrones en todo el proyecto puede hacer que escribir el programa se convierta en una pesadilla. 

Proceso completo que realiza la JVM

  • En primer lugar se compila el código a ByteCode, el cual es volcado en archivos de extension .class 
  • El ByteCode es un conjunto de instrucciones que entiende la máquina virtual de java. 
  • Mediante un compilador Just in Time la máquina virtual de Java traduce el ByteCode a código de máquina en tiempo de ejecución. 
  • Y así la máquina entiende y puede ejecutar el programa.

JAR

 

Cuando se compila el código de Java se genera uno o más archivos .class que contienen el código traducido a ByteCode de mi programa. Esto hace que pueda tener muchos archivos por separado de lo que es mi programa, y que para ejecutarlos tenga que correr algún comando en la consola, o tener algun IDE de programación en Java. Para facilitar la distribución de aplicaciones los archivos de clase pueden empaquetarse juntos en un archivo con formato Jar.

Tener un Jar es como de alguna manera tener un Exe, al hacer doble clic ejecutará mi programa.

Compilador Ahead Of Time (AOT)

es de alguna forma una variante del Just in Time. Este compilador no se ejecuta junto con el Just in Time sino que se usaría en reemplazo del mismo y por defecto siempre estaremos usando el Just in Time ya que el compilador AOT está inhabilitado de forma predeterminada. El compilador Ahead of Time que en español podemos traducir como antes de tiempo, compila en programa antes de hacer la ejecución del mismo y genera un fichero ejecutable de forma nativa por la plataforma donde se quiere ejecutar el programa.

Este compilador combina ciertas variantes para optimizar el código generado, como por ejemplo el uso en primer lugar de un intérprete de un compilador Just in Time a fin de analizar las partes de código que se usan y de que manera, para luego generar un programa nativo optimizado para los escenarios observados en ejecuciones anteriores del mismo programa.

Una desventaja del compilador AOT es que genera un archivo ejecutable para la plataforma sobre la cual se está trabajando por lo que rompe con la idea de que un programa en Java es multiplataforma. Pero en casos muy específicos donde se necesite un mejor rendimiento del programa y se sepa de antemano para que plataforma se va a ejecutar puede ser una buena opción. el código generado por el compilador AOT no se ejecuta tan bien como el generado por JIT aunque la definición teórica nos indique otra cosa.

La máquina virtual JAVA

Luego de explicar conceptos como compilador e intérprete, puede surgir la duda de si Java trabaja con un compilador o bien con un intérprete. Al remitirse a las definiciones anteriores puede pensarse que trabaja con un intérprete, ya que un mismo ejecutable en JAVA puede correr bajo cualquier plataforma, pero por otra parte JAVA es un lenguaje eficiente en cuanto a rapidez y eso contradice a los lenguajes que utilizan intérpretes, ya que estos se caracterizan por no ser muy rápidos.

Entonces ¿Cómo hace JAVA para ser multiplataforma y ser eficiente al mismo tiempo? En realidad la solución a estos temas fue crear una máquina virtual, lo que se interpreta como un microprocesador virtual que trabaja con un código binario especial, llamado bytecode, el cual es generado por un compilador del lenguaje.

El código resultante compilado por el lenguaje es ejecutado por la máquina virtual de JAVA, la cual por un proceso de interpretación o bien mediante un compilador JIT (Just in Time), convierte el bytecode en código nativo de la plataforma en la que se vaya a ejecutar, lo que hace a la ejecución mucho más rápida, y que permite entonces que un mismo programa pueda ser ejecutado en diferentes plataformas como Windows, Linux, Mac, etc. Resumidamente la máquina virtual de JAVA (JVM) es el puente entre el resultado de la compilación y la plataforma en donde se vaya a ejecutar la aplicación.

Las desventajas son que la máquina virtual de JAVA debe estar instalada en las plataformas sobre las cuales se quieran ejecutar el código, aunque en la actualidad esto no presenta mucho problema ya que la mayoría de las plataformas presentan compatibilidad con ésta, y que el inicialmente se requiere de un tiempo necesario mayor para la compilación del código respecto a otros lenguajes.

Repasemos para comprender mejor…

Ok ya sabemos que JVM (Java Virtual Machine) es una máquina abstracta. Es una especificación que proporciona un entorno de ejecución en el que se puede ejecutar el código de bytes de Java y que las JVM están disponibles para muchas plataformas de hardware y software (es decir, la JVM depende de la plataforma). Entonces JVM es:

  1. Una especificación donde se especifica el funcionamiento de la Máquina Virtual Java. Pero el proveedor de implementación es independiente para elegir el algoritmo. Su implementación ha corrido a cargo de Oracle y otras empresas.
  2. Una implementación Su implementación se conoce como JRE (Java Runtime Environment).
  3. Instancia de tiempo de ejecución Cada vez que escribe un comando java en el símbolo del sistema para ejecutar la clase java, se crea una instancia de JVM.

Que hace

La JVM realiza la siguiente operación:

  • Carga el código
  • Verifica el código
  • Ejecuta código
  • Proporciona un entorno de ejecución

JVM proporciona definiciones para:

  • Área de memoria
  • Formato de archivo de clase
  • conjunto de registro
  • Montón de basura recolectada
  • Informes de errores fatales, etc.

Arquitectura y componentes de Java JVM

La arquitectura interna de JVM comprende un cargador de clases, un área de memoria, un motor de ejecución y más. Comprendamos todos los componentes de JVM en detalle. 

Cargador de clases

Classloader es un subsistema de JVM en Java que se utiliza para cargar archivos de clases. Cuando ejecuta cualquier programa Java , el cargador de clases lo carga primero. Java incluye tres cargadores de clases integrados, que son los siguientes:

Cargador de clases Bootstrap

Cada implementación de JVM debe tener un cargador de clases Bootstrap que pueda cargar clases confiables. Este es el primer cargador de clases y la superclase del cargador de clases de extensión. 

El cargador de clases Bootstrap carga archivos rt.jar que contienen archivos de clases de Java Standard Edition, como por ejemplo: 

  • clases del paquete java.util, 
  • clases del paquete java.lang, 
  • clases de paquetes java.io, 
  • clases de paquetes java.net, 
  • clases del paquete java.sql, 
  • y más. 

Además, carga las clases principales de la API de Java disponibles en el directorio JAVA_HOME/jre/lib, y la ruta se denomina ruta Bootstrap. Está implementado en lenguajes nativos, como C y C++.

Cargador de clases de extensión: 

Este es el cargador de clases principal del cargador de clases del sistema y el cargador de clases secundario del cargador de clases Bootstrap. Se utiliza para cargar los archivos jar presentes en el directorio $JAVA_HOME/jre/lib/ext. 

Esto significa que carga clases en los directorios de extensión JAVA_HOME/jre/lib/ext”(ruta de extensión) u otros directorios especificados por la propiedad del sistema java.ext.dirs. La clase sun.misc.Launcher$ExtClassLoader lo implementa. 

Cargador de clases del sistema: 

Como se mencionó anteriormente, este es el cargador de clases secundario del cargador de clases de extensión y carga archivos de clases desde la ruta de clases. El cargador de clases del sistema también se conoce como cargador de clases de aplicaciones. Classpath está configurado en el directorio actual de forma predeterminada, pero puede cambiarlo usando el modificador «-cp» o «-classpath». 

Se implementa mediante la clase sun.misc.Launcher$AppClassLoader y utiliza internamente la variable de entorno asignada a java.class.path.

Subsistema de carga de clases

Classloader es responsable de ocuparse de tres actividades clave:

  • Cargando
  • Enlace
  • Inicialización

Cargando

Como parte del trabajo de JVM, el cargador de clases lee el archivo .class, genera los datos binarios correspondientes y guarda el archivo en el área de métodos. La máquina virtual Java almacena los siguientes detalles en el área de métodos para cada archivo .class:

  • Si el archivo .class está relacionado con una interfaz, clase o enumeración.
  • Complete el nombre completo de la clase cargada y su clase principal.
  • Información relacionada con modificadores, métodos y variables.

Una vez que se carga el archivo .class, JVM genera un objeto de clase de tipo que representa el archivo en la memoria del montón. Recuerde que el objeto es de tipo clase y está predefinido en java.lang.package. Los programadores utilizan el objeto de clase para extraer información a nivel de clase, que incluye el nombre de la clase, información de métodos y variables, el nombre del padre y más. Para referencia de objetos, utilice el método getClass() de la clase Object.

Enlace

Esto incluye realizar diversas actividades, como verificación, preparación y resolución, las cuales se explican a continuación:

Verificación 

La verificación significa garantizar la exactitud del archivo .class. Aquí se comprueba si el archivo está formateado correctamente y generado por un compilador válido. Si el paso de verificación falla, recibimos la excepción de tiempo de ejecución java.lang.VerifyError. Esta función la realiza el componente ByteCodeVerifier y, una vez realizada, el archivo .class está listo para su compilación. 

Preparación

JVM asigna memoria para variables estáticas de clase e inicializa la memoria a los valores predeterminados.

Resolución

La resolución es un proceso que reemplaza las referencias simbólicas del tipo por referencias directas. La resolución se realiza buscando en el área del método para encontrar la entidad a la que se hace referencia. 

Inicialización

Esta es la etapa para asignar valores a todas las variables estáticas. Estos valores se definen en el código y en el bloque estático, si lo hay. La ejecución de la inicialización se lleva a cabo de arriba a abajo en una clase, donde se mueve en una jerarquía de la clase principal a la secundaria. 

Memoria de la máquina virtual Java (JVM)

Área del método

Es uno de los componentes más importantes de JVM. Cada JVM tiene solo un área de método, que es un recurso compartido. 

Almacena estructuras por clase e información a nivel de clase, como grupo constante de tiempo de ejecución, nombre de clase, datos de campo y método, nombre de clase principal inmediata, información de variables estáticas, código para métodos, etc. Sin embargo, en Java 8, el área Heap almacena variables estáticas. 

Área del montón

Esta es el área de datos en tiempo de ejecución que almacena información sobre todos los objetos. Además, es un recurso compartido donde se asignan objetos y hay un área de montón por JVM. 

Área de pila

El área de la pila no es un recurso compartido. JVM crea una pila de tiempo de ejecución para cada subproceso, que se almacena en esta área. Entonces, cada hilo tiene una pila JVM privada, que se crea simultáneamente con el hilo. 

Almacena fotogramas y contiene resultados parciales y variables locales. Cada bloque de la pila se conoce como registro de activación o marco de pila que almacena llamadas a métodos. Las variables locales del método se almacenan en el marco correspondiente. 

Cada vez que se invoca un método, se crea un nuevo marco, que se destruye una vez que se completa la invocación del método. El área de la pila juega un papel crucial en la invocación y retorno de métodos. Con cada hilo que termina, la JVM también destruye su pila de tiempo de ejecución. 

Registros de PC

El registro de PC, o registro de contador de programa, almacena la dirección de las instrucciones de ejecución actuales de un subproceso de la máquina virtual Java. Hay un registro de contador de programa separado para cada hilo. 

Pila de métodos nativos

Hay una pila nativa separada para cada hilo. Esta área almacena información del método nativo.

Motor de ejecución JVM

Este es otro componente de la arquitectura JVM en Java que ejecuta el código de bytes .class y lee el código de bytes línea por línea. Además, utiliza información y datos disponibles en diferentes áreas de la memoria y luego ejecuta instrucciones. Además de un procesador virtual, contiene las siguientes partes:

  • Intérprete

Lee el flujo de código de bytes, lo interpreta línea por línea y luego ejecuta las instrucciones. Sin embargo, un inconveniente aquí es que cuando se llama a un método varias veces, requiere interpretación cada vez.

  • Compilador justo a tiempo (JIT)

Esta parte se utiliza para mejorar el desempeño y la eficiencia de un intérprete. En la funcionalidad de JVM, JIT es responsable de compilar diferentes partes del código de bytes que realizan diferentes tareas al mismo tiempo y cambiarlo a un código nativo. 

Entonces, cada vez que el intérprete presencia llamadas repetitivas a métodos, JIT lo cambia al código nativo directo para esa parte específica. Por lo tanto, no es necesario reinterpretarlo, lo que reduce el tiempo de compilación y mejora la eficiencia. Aquí, compilador significa un traductor de las instrucciones de la máquina virtual Java a instrucciones específicas de la CPU. 

  • Recolector de basura

En JVM, el recolector de basura destruye los objetos no referenciados.

Interfaz nativa de Java

Java Native Interface, o JNI, es un marco que proporciona una interfaz para interactuar con otras aplicaciones escritas en diferentes lenguajes de programación, como Assembly, C, C++ y más. 

Permite la comunicación con bibliotecas de métodos nativos y ofrece bibliotecas nativas para su ejecución. Java utiliza este marco para compartir resultados en la consola e interactuar con las bibliotecas del sistema operativo. Además, permite que JVM acceda a bibliotecas C o C++ y sea llamado por bibliotecas C o C++ específicas del hardware. 

Bibliotecas de métodos nativos

Las bibliotecas de métodos nativos se refieren a una colección de bibliotecas nativas que necesita el motor de ejecución.

Preguntas frecuentes sobre JVM en Java

A continuación se muestran algunas preguntas frecuentes relacionadas con la máquina virtual Java (JVM):

¿Cuáles son los componentes principales de la JVM?

  La JVM consta de varios componentes, que incluyen:

  • Cargador de clases: carga archivos de clases Java en la memoria.
  • Motor de ejecución: interpreta y ejecuta el código de bytes de Java o realiza una compilación justo a tiempo (JIT).
  • Gestión de memoria: gestiona la asignación y desasignación de memoria, incluida la recolección de basura.
  • Área de datos de tiempo de ejecución: contiene varias regiones de memoria, incluido el área de método, la pila y otras, que se utilizan durante la ejecución del programa.
  • Compilador JIT: transforma el código de bytes en código de máquina para una ejecución eficiente en el hardware subyacente.
  • Interfaz nativa: permite la interacción entre el código Java y el código escrito en otros lenguajes de programación.

¿La plataforma JVM es independiente?

Sí, JVM proporciona independencia de plataforma para aplicaciones Java. El código de bytes de Java, que ejecuta la JVM, está diseñado para ser neutral en cuanto a plataforma. Esto permite que los programas Java se ejecuten en cualquier sistema que tenga una implementación JVM compatible, independientemente del hardware o sistema operativo subyacente.

¿Cómo funciona la gestión de memoria en la JVM?

La gestión de la memoria en la JVM implica la asignación automática de memoria y la recolección de basura. La memoria de la JVM se divide en regiones, incluidos el montón y la pila. El montón es donde se asignan y administran los objetos, mientras que la pila contiene datos específicos del método, incluidas variables locales e invocaciones de métodos. El recolector de basura de la JVM libera memoria automáticamente identificando y eliminando objetos a los que la aplicación ya no hace referencia.

¿Qué es la compilación JIT en JVM?

La compilación JIT (Just-in-Time) es una técnica utilizada por JVM para mejorar el rendimiento de las aplicaciones Java. En lugar de interpretar todo el código de bytes, el compilador JIT analiza y compila dinámicamente partes del código ejecutadas con frecuencia en código de máquina en tiempo de ejecución. Esto permite una ejecución más rápida de esas partes, lo que resulta en un mejor rendimiento general.

¿Se puede ajustar el rendimiento de la JVM?

Sí, la JVM se puede ajustar y configurar para obtener un rendimiento óptimo según los requisitos específicos de la aplicación. Esto incluye ajustar parámetros como el tamaño del montón, los algoritmos de recolección de basura, la administración de subprocesos y la optimización del código de bytes. El ajuste de JVM normalmente se realiza modificando las opciones de JVM y los argumentos de la línea de comandos.

¿Se pueden ejecutar otros idiomas además de Java en la JVM?

Sí, la JVM no se limita a ejecutar únicamente el código de bytes de Java. Otros lenguajes de programación, como Scala, Kotlin, Groovy y Clojure, también se pueden compilar en código de bytes de JVM y ejecutar en la JVM. Esta flexibilidad permite a los desarrolladores aprovechar el rico ecosistema y los beneficios de rendimiento de la JVM para varios lenguajes de programación.

¿Cuál de las siguientes opciones describe mejor la JVM?

Un intérprete para el código de bytes de Java. 

¿Cuál es el propósito del compilador JIT en la JVM?

Para optimizar la ejecución de código ejecutado con frecuencia. 

¿Cuál de los siguientes es responsable de la gestión automática de la memoria en la JVM?

Recolector de basura

La JVM proporciona independencia de plataforma para aplicaciones Java. ¿Qué quiere decir esto?

Las aplicaciones Java pueden ejecutarse en cualquier sistema operativo sin modificaciones.

¿Cuáles son los componentes principales del área de datos de tiempo de ejecución de la JVM?

Área de pila, montón y método

Proceso de compilación y ejecución de código de software

Para escribir y ejecutar un programa de software, necesita lo siguiente

1) Editor : para escribir su programa, se puede usar un bloc de notas para esto

2) Compilador : para convertir su programa en lenguaje avanzado en código de máquina nativo

3) Vinculador : para combinar diferentes referencias de archivos de programa en su programa principal.

4) Cargador : para cargar los archivos desde su dispositivo de almacenamiento secundario, como disco duro, unidad flash o CD, en la RAM para su ejecución. La carga se realiza automáticamente cuando ejecuta su código.

5) Ejecución : ejecución real del código manejado por su sistema operativo y procesador.

Con estos antecedentes, consulte el siguiente video y conozca el funcionamiento interno y la arquitectura de JVM (Java Virtual Machine).

Proceso de compilación y ejecución de código C.

Comprender el proceso de compilación de Java en Java. Primero echemos un vistazo rápido al proceso de compilación y vinculación en C.

Supongamos que, en general, ha llamado a dos funciones f1 y f2. La función principal se almacena en el archivo a1.c.

Ahora, La función f1 se almacena en un archivo a2.c

La función f2 se almacena en un archivo a3.c

Todos estos archivos, es decir, a1.c, a2.c y a3.c, se envían al compilador. Cuya salida son los archivos objeto correspondientes que son el código de máquina.

El siguiente paso es integrar todos estos archivos objeto en un único archivo .exe con la ayuda del vinculador. El vinculador agrupará todos estos archivos y producirá el archivo .exe.

Durante la ejecución del programa, un programa de carga cargará un.exe en la RAM para la ejecución.

Compilación y ejecución de código Java en Java VM

Ahora, en este tutorial de JVM, veamos el proceso para JAVA. En tu main, tienes dos métodos f1 y f2.

  • El método principal se almacena en el archivo a1.java.
  • f1 se almacena en un archivo como a2.java
  • f2 se almacena en un archivo como a3.java

El compilador compilará los tres archivos y producirá 3 archivos .class correspondientes que consisten en código BYTE. A diferencia de C, no se realiza ningún enlace .

La Java VM o Java Virtual Machine reside en la RAM. Durante la ejecución, utilizando el cargador de clases, los archivos de clases se llevan a la RAM. El código BYTE se verifica para detectar violaciones de seguridad.

A continuación, el motor de ejecución convertirá el código de bytes en código de máquina nativo. Esta es la compilación justo a tiempo. Es una de las razones principales por las que Java es comparativamente lento.

NOTA: El compilador JIT o Just-in-time es parte de la Máquina Virtual Java (JVM). Interpreta parte del Byte Code que tiene una funcionalidad similar al mismo tiempo.

¿Por qué Java es un lenguaje interpretado y compilado?

Los lenguajes de programación se clasifican en

  • Idioma de nivel superior Ej. C++ , Java
  • Idiomas de nivel medio Ej. C
  • Lenguaje de bajo nivel Ex ensamblador
  • finalmente el nivel más bajo como Lenguaje de Máquina.

Un compilador es un programa que convierte un programa de un nivel de lenguaje a otro. Ejemplo de conversión de un programa C++ a código máquina.

El compilador de Java convierte código Java de alto nivel en código de bytes (que también es un tipo de código de máquina).

Un intérprete es un programa que convierte un programa de un nivel a otro lenguaje de programación del mismo nivel. Ejemplo de conversión de un programa Java a C++

En Java, el generador de código Just In Time convierte el código de bytes en código de máquina nativo que se encuentran en los mismos niveles de programación.

Por lo tanto, Java es un lenguaje tanto compilado como interpretado.

¿Por qué Java es lento?

Las dos razones principales detrás de la lentitud de Java son

  1. Enlace dinámico: a diferencia de C, el enlace se realiza en tiempo de ejecución, cada vez que el programa se ejecuta en Java.
  2. Intérprete en tiempo de ejecución: la conversión de código de bytes en código de máquina nativo se realiza en tiempo de ejecución en Java, lo que ralentiza aún más la velocidad.

Sin embargo, la última versión de Java ha solucionado en gran medida los cuellos de botella en el rendimiento.

Resumen :

  • La forma completa de JVM es Java Virtual Machine. JVM en Java es el motor que impulsa el código Java. Convierte el código de bytes de Java en lenguaje de máquina.
  • La arquitectura JVM en Java contiene un cargador de clases, un área de memoria, un motor de ejecución, etc.
  • En JVM, el código Java se compila en código de bytes. Este código de bytes se interpreta en diferentes máquinas.
  • JIT significa compilador justo a tiempo. JIT es la parte de la Máquina Virtual Java (JVM). Se utiliza para acelerar el tiempo de ejecución.
  • En comparación con otras máquinas compiladoras, JVM en Java puede tener una ejecución lenta.

Profundicemos en cómo funciona JVM

Subsistema de cargador de clases

Es principalmente responsable de tres actividades. 

  • Cargando
  • Enlace
  • Inicialización

Cargando: El cargador de clases lee el archivo “. class” , genere los datos binarios correspondientes y guárdelos en el área de métodos. Para cada archivo » .class» , JVM almacena la siguiente información en el área de método. 

  • El nombre completo de la clase cargada y su clase principal inmediata.
  • Si el archivo » .class» está relacionado con Clase, Interfaz o Enum.
  • Información sobre modificadores, variables y métodos, etc.

Después de cargar el archivo » .class» , JVM crea un objeto de tipo Clase para representar este archivo en la memoria del montón. Tenga en cuenta que este objeto es de tipo Clase predefinida en el paquete java.lang . El programador puede utilizar estos objetos de clase para obtener información a nivel de clase, como el nombre de la clase, el nombre del padre, los métodos y la información de las variables, etc. Para obtener esta referencia de objeto, podemos usar el método getClass() de la clase Object .

Ejemplo de código Java:

// A Java program to demonstrate working
// of a Class type object created by JVM
// to represent .class file in memory.
import java.lang.reflect.Field;
import java.lang.reflect.Method;
 
// Java code to demonstrate use
// of Class object created by JVM
public class Test {
    public static void main(String[] args)
    {
        Student s1 = new Student();
 
        // Getting hold of Class
        // object created by JVM.
        Class c1 = s1.getClass();
 
        // Printing type of object using c1.
        System.out.println(c1.getName());
 
        // getting all methods in an array
        Method m[] = c1.getDeclaredMethods();
        for (Method method : m)
            System.out.println(method.getName());
 
        // getting all fields in an array
        Field f[] = c1.getDeclaredFields();
        for (Field field : f)
            System.out.println(field.getName());
    }
}
 
// A sample class whose information
// is fetched above using its Class object.
class Student {
    private String name;
    private int roll_No;
 
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getRoll_no() { return roll_No; }
    public void setRoll_no(int roll_no)
    {
        this.roll_No = roll_no;
    }
}

Producción

Alumno
obtenerNombre
escoger un nombre
getRoll_no
setRoll_no
nombre
rollo_No

Nota: Por cada archivo “ .class” cargado, solo se crea  un objeto de la clase.

Estudiante s2 = nuevo Estudiante();

// c2 apuntará al mismo objeto donde

// c1 apunta

Class c2 = s2.getClass();

System.out.println(c1==c2); // verdadero

Vinculación: Realiza verificación, preparación y (opcionalmente) resolución. 

  • Verificación : Garantiza la exactitud del archivo .class , es decir, comprueba si este archivo está formateado y generado correctamente por un compilador válido o no. Si la verificación falla, obtenemos una excepción de tiempo de ejecución java.lang.VerifyError . Esta actividad la realiza el componente ByteCodeVerifier. Una vez completada esta actividad, el archivo de clase estará listo para su compilación.
  • Preparación : JVM asigna memoria para las variables estáticas de clase e inicializa la memoria a los valores predeterminados. 
  • Resolución : Es el proceso de sustituir referencias simbólicas del tipo por referencias directas. Se realiza buscando en el área del método para localizar la entidad a la que se hace referencia.

Inicialización: en esta fase, a todas las variables estáticas se les asignan sus valores definidos en el código y el bloque estático (si corresponde). Esto se ejecuta de arriba a abajo en una clase y de padre a hijo en la jerarquía de clases. 

En general, existen tres cargadores de clases: 

  • Cargador de clases Bootstrap : cada implementación de JVM debe tener un cargador de clases bootstrap, capaz de cargar clases confiables. Carga las clases principales de la API de Java presentes en el directorio » JAVA_HOME/jre/lib» . Esta ruta se conoce popularmente como ruta de arranque. Está implementado en lenguajes nativos como C, C++.
  • Cargador de clases de extensión : es un hijo del cargador de clases de arranque. Carga las clases presentes en los directorios de extensiones “ JAVA_HOME/jre/lib/ext” (ruta de extensión) o cualquier otro directorio especificado por la propiedad del sistema java.ext.dirs. Se implementa en Java mediante la clase sun.misc.Launcher$ExtClassLoader .
  • Cargador de clases de sistema/aplicación : es un hijo del cargador de clases de extensión. Es responsable de cargar clases desde el classpath de la aplicación. Utiliza internamente una variable de entorno que se asigna a java.class.path. También se implementa en Java mediante la clase sun.misc.Launcher$AppClassLoader .

Ejemplo de código Java:

// Java code to demonstrate Class Loader subsystem

public class Test {

    public static void main(String[] args)

    {

        // String class is loaded by bootstrap loader, and

        // bootstrap loader is not Java object, hence null

        System.out.println(String.class.getClassLoader());

        // Test class is loaded by Application loader

        System.out.println(Test.class.getClassLoader());

    }

}

Producción

nulo
jdk.internal.loader.ClassLoaders$AppClassLoader@8bcc55f

Nota: JVM sigue el principio de jerarquía de delegación para cargar clases. El cargador de clases del sistema delega la solicitud de carga al cargador de clases de extensión y el cargador de clases de extensión delega la solicitud al cargador de clases de arranque. Si se encuentra una clase en la ruta de arranque, la clase se carga; de lo contrario, la solicitud nuevamente se transfiere al cargador de clases de extensión y luego al cargador de clases del sistema. Por último, si el cargador de clases del sistema no puede cargar la clase, obtenemos la excepción de tiempo de ejecución java.lang.ClassNotFoundException . 

Memoria JVM 

  1. Área de métodos: en el área de métodos, se almacena toda la información a nivel de clase, como el nombre de la clase, el nombre de la clase principal inmediata, la información de métodos y variables, etc., incluidas las variables estáticas. Solo hay un área de método por JVM y es un recurso compartido. 
  2. Área del montón: la información de todos los objetos se almacena en el área del montón. También hay un área de montón por JVM. También es un recurso compartido.
  3. Área de pila: para cada subproceso, JVM crea una pila de tiempo de ejecución que se almacena aquí. Cada bloque de esta pila se llama registro de activación/marco de pila que almacena llamadas a métodos. Todas las variables locales de ese método se almacenan en su marco correspondiente. Una vez que finaliza un subproceso, la JVM destruirá su pila de tiempo de ejecución. No es un recurso compartido.
  4. Registros de PC: almacena la dirección de la instrucción de ejecución actual de un hilo. Obviamente, cada hilo tiene registros de PC separados.
  5. Pilas de métodos nativos: para cada subproceso, se crea una pila nativa separada. Almacena información del método nativo. 

Motor de ejecución 

El motor de ejecución ejecuta el “ .class” (código de bytes). Lee el código de bytes línea por línea, utiliza datos e información presentes en varias áreas de la memoria y ejecuta instrucciones. Se puede clasificar en tres partes:

  • Intérprete : interpreta el código de bytes línea por línea y luego lo ejecuta. La desventaja aquí es que cuando se llama a un método varias veces, se requiere interpretación cada vez.
  • Compilador Just-In-Time (JIT) : se utiliza para aumentar la eficiencia de un intérprete. Compila todo el código de bytes y lo cambia a código nativo, de modo que cada vez que el intérprete ve llamadas repetidas a métodos, JIT proporciona código nativo directo para esa parte, por lo que no es necesaria una reinterpretación, lo que mejora la eficiencia.
  • Recolector de basura : destruye objetos no referenciados. Para obtener más información sobre Garbage Collector, consulte Garbage Collector .

Interfaz nativa de Java (JNI): 

Es una interfaz que interactúa con las bibliotecas de métodos nativos y proporciona las bibliotecas nativas (C, C++) necesarias para la ejecución. Permite que JVM llame a bibliotecas C/C++ y sea llamada por bibliotecas C/C++ que pueden ser específicas del hardware.

Bibliotecas de métodos nativos: 

Es una colección de bibliotecas nativas (C, C++) que requiere el motor de ejecución.

Un intérprete, para cada bucle, recupera el valor de ‘suma’ de la memoria, le agrega ‘I’ y lo almacena nuevamente en la memoria. El acceso a la memoria es una operación costosa y normalmente requiere varios ciclos de CPU. Dado que este código se ejecuta varias veces, es un HotSpot. El JIT compilará este código y realizará la siguiente optimización.

Una copia local de ‘suma’ se almacenaría en un registro, específico de un hilo en particular. Todas las operaciones se realizarían con el valor del registro y, cuando se complete el ciclo, el valor se volvería a escribir en la memoria.

¿Qué pasa si otros hilos también acceden a la variable? Dado que algún otro hilo está realizando actualizaciones en una copia local de la variable, verían un valor obsoleto. En tales casos, se necesita sincronización de subprocesos. Una primitiva de sincronización muy básica sería declarar ‘suma’ como volátil. Ahora, antes de acceder a una variable, un hilo vaciaría sus registros locales y recuperaría el valor de la memoria. Después de acceder a él, el valor se escribe inmediatamente en la memoria.

A continuación se muestran algunas optimizaciones generales realizadas por los compiladores JIT:

  • Método en línea
  • Eliminación de código muerto
  • Heurística para optimizar sitios de llamadas
  • Plegado constante

Niveles de compilación

JVM admite cinco niveles de compilación:

  • Intérprete
  • C1 con optimización total (sin perfiles)
  • C1 con contadores de invocación y back-edge (perfilado ligero)
  • C1 con perfilado completo
  • C2 (utiliza datos de perfil de los pasos anteriores)

Use -Xint si desea deshabilitar todos los compiladores JIT y usar solo el intérprete.

JIT de cliente versus servidor

Utilice -client y -server para activar los modos respectivos.

El compilador del cliente (C1) comienza a compilar el código antes que el compilador del servidor (C2). Entonces, cuando C2 haya comenzado la compilación, C1 ya habrá compilado secciones de código.

Pero mientras espera, C2 perfila el código para saber más sobre él que C1. Por lo tanto, el tiempo de espera si se compensa con las optimizaciones se puede utilizar para generar un binario mucho más rápido. Desde la perspectiva de un usuario, el equilibrio es entre el tiempo de inicio del programa y el tiempo que tarda en ejecutarse. Si el tiempo de inicio es prioritario, entonces se debe utilizar C1. Si se espera que la aplicación se ejecute durante mucho tiempo (típico de las aplicaciones implementadas en servidores), es mejor usar C2 ya que genera código mucho más rápido que compensa en gran medida cualquier tiempo de inicio adicional.

Para programas como IDE (NetBeans, Eclipse) y otros programas GUI, el tiempo de inicio es fundamental. NetBeans puede tardar un minuto o más en iniciarse. Se compilan cientos de clases cuando se inician programas como NetBeans. En tales casos, el compilador C1 es la mejor opción.

Tenga en cuenta que hay dos versiones de C1 − 32b y 64b . C2 viene sólo en 64b .

Compilación escalonada

En versiones anteriores de Java, el usuario podría haber seleccionado una de las siguientes opciones:

  • Intérprete (-Xint)
  • C1 (-cliente)
  • C2 (-servidor)

Vino en Java 7. Utiliza el compilador C1 para iniciarse y, a medida que el código se calienta, cambia al C2. Se puede activar con las siguientes opciones de JVM: -XX:+TieredCompilation. El valor predeterminado se establece en falso en Java 7 y en verdadero en Java 8 .

De los cinco niveles de compilación, la compilación por niveles utiliza 1 -> 4 -> 5 .

32b frente a 64b

En una máquina 32b, solo se puede instalar la versión 32b de JVM. En una máquina 64b, el usuario puede elegir entre la versión 32b y 64b. Pero hay ciertos matices en esto que pueden afectar el rendimiento de nuestras aplicaciones Java.

Si la aplicación Java usa menos de 4G de memoria, deberíamos usar la JVM de 32b incluso en máquinas de 64b. Esto se debe a que las referencias de memoria en este caso serían solo 32b y manipularlas sería menos costoso que manipular direcciones 64b. En este caso, la JVM 64b tendría un peor rendimiento incluso si utilizamos OOPS (punteros de objetos ordinarios). Usando OOPS, la JVM puede usar direcciones 32b en la JVM 64b. Sin embargo, manipularlas sería más lento que las referencias 32b reales, ya que las referencias nativas subyacentes seguirían siendo 64b.

Si nuestra aplicación va a consumir más de 4G de memoria, tendremos que usar la versión 64b ya que las referencias de 32b no pueden direccionar más de 4G de memoria. Podemos tener ambas versiones instaladas en la misma máquina y cambiar entre ellas usando la variable PATH.

Optimizaciones JIT

Aquí aprenderemos sobre las optimizaciones JIT.

Método en línea

En esta técnica de optimización, el compilador decide reemplazar las llamadas a funciones con el cuerpo de la función. A continuación se muestra un ejemplo de lo mismo:

int sum3;

static int add(int a, int b) {

   return a + b;

}

public static void main(String…args) {

   sum3 = add(5,7) + add(4,2);

}

//after method inlining

public static void main(String…args) {

   sum3 = 5+ 7 + 4 + 2;

}

Usando esta técnica, el compilador ahorra a la máquina la sobrecarga de realizar llamadas a funciones (requiere insertar y extraer parámetros en la pila). Por tanto, el código generado se ejecuta más rápido.

La inserción de métodos solo se puede realizar para funciones no virtuales (funciones que no se anulan). Considere lo que sucedería si el método ‘agregar’ se anulara en una subclase y el tipo de objeto que contiene el método no se conoce hasta el tiempo de ejecución. En este caso, el compilador no sabría qué método incorporar. Pero si el método estuviera marcado como «final», entonces el compilador sabría fácilmente que puede estar en línea porque ninguna subclase no puede anularlo. Tenga en cuenta que no se garantiza en absoluto que un método final esté siempre en línea.

Eliminación de códigos inalcanzables y muertos

El código inalcanzable es código al que no se puede acceder mediante ningún flujo de ejecución posible. Consideraremos el siguiente ejemplo:

void foo() {

   if (a) return;

   else return;

   foobar(a,b); //unreachable code, compile time error

}

El código muerto también es código inalcanzable, pero el compilador arroja un error en este caso. En cambio, solo recibimos una advertencia. Cada bloque de código, como constructores, funciones, try, catch, if, while, etc., tiene sus propias reglas para código inalcanzable definidas en JLS (Especificación del lenguaje Java).

Plegado constante

Para comprender el concepto de plegado constante, consulte el siguiente ejemplo.

final int num = 5;

int b = num * 6; //compile-time constant, num never changes

//compiler would assign b a value of 30.

Recolección de basura

El ciclo de vida de un objeto Java lo gestiona la JVM. Una vez que el programador crea un objeto, no debemos preocuparnos por el resto de su ciclo de vida. La JVM encontrará automáticamente aquellos objetos que ya no están en uso y recuperará su memoria del montón.

La recolección de basura es una operación importante que realiza JVM y ajustarla a nuestras necesidades puede aumentar enormemente el rendimiento de nuestra aplicación. Existe una variedad de algoritmos de recolección de basura proporcionados por las JVM modernas. Necesitamos ser conscientes de las necesidades de nuestra aplicación para decidir qué algoritmo utilizar.

No se puede desasignar un objeto mediante programación en Java, como se puede hacer en lenguajes que no son GC como C y C++. Por lo tanto, no se pueden tener referencias pendientes en Java. Sin embargo, es posible que tenga referencias nulas (referencias que se refieren a un área de memoria donde la JVM nunca almacenará objetos). Siempre que se utiliza una referencia nula, la JVM genera una NullPointerException.

Tenga en cuenta que, si bien es raro encontrar pérdidas de memoria en programas Java gracias al GC, ocurren. Crearemos una pérdida de memoria al final de este tema.

Los siguientes GC se utilizan en las JVM modernas

  • Coleccionista en serie
  • Colector de rendimiento
  • recopilador de CMS
  • Colector G1

Cada uno de los algoritmos anteriores realiza la misma tarea: encontrar objetos que ya no están en uso y recuperar la memoria que ocupan en el montón. Uno de los enfoques ingenuos para esto sería contar el número de referencias que tiene cada objeto y liberarlo tan pronto como el número de referencias llegue a 0 (esto también se conoce como recuento de referencias). ¿Por qué es esto ingenuo? Considere una lista circular enlazada. Cada uno de sus nodos tendrá una referencia a él, pero no se hace referencia al objeto completo desde ningún lugar y, idealmente, debería liberarse.

La JVM no sólo libera la memoria, sino que también fusiona pequeños módulos de memoria en otros más grandes. Esto se hace para evitar la fragmentación de la memoria.

En pocas palabras, un algoritmo de GC típico realiza las siguientes actividades:

  • Encontrar objetos no utilizados
  • Liberando la memoria que ocupan en el montón
  • Fusionando los fragmentos

El GC tiene que detener los subprocesos de la aplicación mientras se está ejecutando. Esto se debe a que mueve los objetos cuando se ejecuta y, por lo tanto, esos objetos no se pueden utilizar. Estas paradas se denominan «pausas para detener el mundo» y nuestro objetivo es minimizar la frecuencia y duración de estas pausas al ajustar nuestro GC.

Memoria fusionada

A continuación se muestra una demostración sencilla de la fusión de memoria.

La parte sombreada son objetos que deben liberarse. Incluso después de recuperar todo el espacio, solo podemos asignar un objeto de tamaño máximo = 75 Kb. Esto es incluso después de que tengamos 200 Kb de espacio libre como se muestra a continuación.

GC generacionales

La mayoría de las JVM dividen el montón en tres generaciones: la generación joven (YG), la generación anterior (OG) y la generación permanente (también llamada generación permanente) . ¿Cuáles son las razones detrás de tal pensamiento?

Los estudios empíricos han demostrado que la mayoría de los objetos que se crean tienen una vida útil muy corta:


Como puede ver, a medida que se asignan más y más objetos con el tiempo, la cantidad de bytes que sobreviven disminuye (en general). Los objetos Java tienen una alta tasa de mortalidad.

Veamos un ejemplo sencillo. La clase String en Java es inmutable. Esto significa que cada vez que necesite cambiar el contenido de un objeto String, deberá crear un objeto completamente nuevo. Supongamos que realiza cambios en la cadena 1000 veces en un bucle como se muestra en el siguiente código:

Ejemplo

String str = «G11 GC»;

for(int i = 0 ; i < 1000; i++) {

   str = str + String.valueOf(i);

}

En cada bucle, creamos un nuevo objeto de cadena y la cadena creada durante la iteración anterior se vuelve inútil (es decir, no está referenciada por ninguna referencia). La vida útil de ese objeto fue solo una iteración: el GC los recopilará en poco tiempo. Estos objetos de corta duración se guardan en el área de generación joven del montón. El proceso de recolección de objetos de la generación joven se llama recolección menor de basura y siempre provoca una pausa de «detener el mundo».

A medida que la generación joven se llena, el GC realiza una pequeña recolección de basura. Los objetos muertos se descartan y los objetos vivos se trasladan a la generación anterior. Los subprocesos de la aplicación se detienen durante este proceso.

Ventajas

Aquí podemos ver las ventajas que ofrece un diseño de generación de este tipo. La generación joven es sólo una pequeña parte del grupo y se llena rápidamente. Pero procesarlo lleva mucho menos tiempo que procesar todo el montón. Así pues, las pausas para «detener el mundo» en este caso son mucho más cortas, aunque más frecuentes. Siempre deberíamos aspirar a pausas más cortas en lugar de pausas más largas, aunque puedan ser más frecuentes. Discutiremos esto en detalle en secciones posteriores de este tutorial.

La generación joven está dividida en dos espacios: el Edén y el Espacio de los Supervivientes . Los objetos que han sobrevivido durante la recolección del Edén se trasladan al espacio de supervivientes, y los que sobreviven al espacio de supervivientes se trasladan a la generación anterior. La generación joven se compacta mientras se recoge.

A medida que los objetos se trasladan a la generación anterior, eventualmente se llena y hay que recogerlos y compactarlos. Diferentes algoritmos adoptan diferentes enfoques al respecto. Algunos de ellos detienen los subprocesos de la aplicación (lo que conduce a una larga pausa de ‘detener el mundo’ ya que la generación anterior es bastante grande en comparación con la generación joven), mientras que algunos lo hacen simultáneamente mientras los subprocesos de la aplicación siguen ejecutándose. Este proceso se llama GC completo. Dos de estos recopiladores son CMS y G1 .

Analicemos ahora estos algoritmos en detalle.

GC en serie

es el GC predeterminado en máquinas de clase cliente (máquinas con un solo procesador o JVM 32b, Windows). Normalmente, los GC tienen muchos subprocesos, pero el GC en serie no lo es. Tiene un solo subproceso para procesar el montón y detendrá los subprocesos de la aplicación cada vez que esté realizando un GC menor o un GC mayor. Podemos ordenarle a la JVM que use este GC especificando el indicador: -XX:+UseSerialGC .

Si queremos que utilice algún algoritmo diferente, especifique el nombre del algoritmo. Tenga en cuenta que la generación anterior se compacta completamente durante una GC importante.

Rendimiento GC

Este GC es el predeterminado en JVM de 64b y máquinas con múltiples CPU. A diferencia del GC en serie, utiliza múltiples subprocesos para procesar a las generaciones jóvenes y mayores. Debido a esto, el GC también se denomina recopilador paralelo . Podemos ordenarle a nuestra JVM que use este recopilador usando la bandera: -XX:+UseParallelOldGC o -XX:+UseParallelGC (para JDK 8 en adelante). Los subprocesos de la aplicación se detienen mientras realiza una recolección de basura mayor o menor. Al igual que el coleccionista en serie, compacta completamente a la generación joven durante una gran general.

El GC de rendimiento recoge YG y OG. Cuando el Edén se ha llenado, el recolector expulsa objetos vivos al OG o a uno de los espacios de supervivientes (SS0 y SS1 en el siguiente diagrama). Los objetos muertos se descartan para liberar el espacio que ocupaban.

Antes de la general de YG

Después de la general de YG

Durante una GC llena, el recolector de rendimiento vacía todo el YG, SS0 y SS1. Después de la operación, el OG contiene solo objetos vivos. Debemos tener en cuenta que los dos recopiladores anteriores detienen los subprocesos de la aplicación mientras procesan el montón. Esto significa largas pausas para «detener el mundo» durante una gran CG. Los siguientes dos algoritmos tienen como objetivo eliminarlos, a costa de más recursos de hardware:

Colector CMS

Significa «barrido de marcas concurrente». Su función es utilizar algunos subprocesos en segundo plano para escanear periódicamente la generación anterior y eliminar los objetos muertos. Pero durante una GC menor, los subprocesos de la aplicación se detienen. Sin embargo, las pausas son bastante pequeñas. Esto convierte al CMS en un recopilador de pausas bajas.

Este recopilador necesita tiempo de CPU adicional para escanear el montón mientras ejecuta los subprocesos de la aplicación. Además, los subprocesos en segundo plano simplemente recopilan el montón y no realizan ninguna compactación. Pueden provocar que el montón se fragmente. A medida que esto continúa, después de un cierto período de tiempo, el CMS detendrá todos los subprocesos de la aplicación y compactará el montón usando un solo subproceso. Utilice los siguientes argumentos de JVM para indicarle a la JVM que utilice el recopilador CMS:

«XX:+UseConcMarkSweepGC -XX:+UseParNewGC» como argumentos de JVM para indicarle que utilice el recopilador CMS.

Antes de la general

Después de la general

Tenga en cuenta que la recopilación se realiza al mismo tiempo.

G1 CG

Este algoritmo funciona dividiendo el montón en varias regiones. Al igual que el recopilador CMS, detiene los subprocesos de la aplicación mientras realiza una GC menor y utiliza subprocesos en segundo plano para procesar la generación anterior mientras mantiene los subprocesos de la aplicación en funcionamiento. Dado que dividió la generación anterior en regiones, las sigue compactando mientras mueve objetos de una región a otra. Por tanto, la fragmentación es mínima. Puede usar la bandera: XX:+UseG1GC para indicarle a su JVM que use este algoritmo. Al igual que CMS, también necesita más tiempo de CPU para procesar el montón y ejecutar los subprocesos de la aplicación al mismo tiempo.

Este algoritmo ha sido diseñado para procesar montones más grandes (> 4G), que se dividen en varias regiones diferentes. Algunas de esas regiones comprenden la generación joven y el resto comprenden la generación mayor. El YG se borra de forma tradicional: se detienen todos los subprocesos de la aplicación y todos los objetos que aún están vivos pasan a la generación anterior o al espacio superviviente.

Tenga en cuenta que todos los algoritmos de GC dividieron el montón en YG y OG, y utilizaron un STWP para limpiar el YG. Este proceso suele ser muy rápido.

Sintonizando el GC

En el último tema, aprendimos sobre varios Gcs generacionales. En este tema, discutiremos cómo ajustar el GC.

Tamaño de la pila

El tamaño del montón es un factor importante en el rendimiento de nuestras aplicaciones Java. Si es demasiado pequeño, se llenará con frecuencia y, como resultado, el GC deberá recolectarlo con frecuencia. Por otro lado, si simplemente aumentamos el tamaño del montón, aunque sea necesario recopilarlo con menos frecuencia, la duración de las pausas aumentaría.

Además, aumentar el tamaño del montón tiene una grave penalización para el sistema operativo subyacente. Al utilizar la paginación, el sistema operativo hace que nuestros programas de aplicación vean mucha más memoria de la que realmente está disponible. El sistema operativo gestiona esto utilizando algo de espacio de intercambio en el disco, copiando en él partes inactivas de los programas. Cuando esas partes son necesarias, el sistema operativo las copia del disco a la memoria.

Supongamos que una máquina tiene 8G de memoria y la JVM ve 16G de memoria virtual, la JVM no sabría que, de hecho, solo hay 8G disponibles en el sistema. Simplemente solicitará 16G al sistema operativo y, una vez que obtenga esa memoria, continuará usándola. El sistema operativo tendrá que intercambiar una gran cantidad de datos dentro y fuera, y esto supone una enorme penalización en el rendimiento del sistema.

Las pausas

Y luego vienen las pausas que se producirían durante la GC completa de dicha memoria virtual. Dado que el GC actuará en todo el montón para la recopilación y compactación, tendrá que esperar mucho hasta que la memoria virtual se extraiga del disco. En el caso de un recopilador concurrente, los subprocesos en segundo plano tendrán que esperar mucho para que los datos se copien desde el espacio de intercambio a la memoria.

Entonces aquí surge la pregunta de cómo debemos decidir el tamaño de montón óptimo. La primera regla es nunca solicitar al sistema operativo más memoria de la que realmente está presente. Esto evitaría totalmente el problema de los intercambios frecuentes. Si la máquina tiene varias JVM instaladas y en ejecución, entonces la solicitud de memoria total de todas ellas combinadas es menor que la RAM real presente en el sistema.

Puede controlar el tamaño de la solicitud de memoria por parte de la JVM usando dos indicadores:

  • -XmsN − Controla la memoria inicial solicitada.
  • -XmxN − Controla la memoria máxima que se puede solicitar.

Los valores predeterminados de ambos indicadores dependen del sistema operativo subyacente. Por ejemplo, para JVM de 64b que se ejecutan en MacOS, -XmsN = 64M y -XmxN = mínimo de 1G o 1/4 de la memoria física total.

Tenga en cuenta que la JVM puede ajustar entre los dos valores automáticamente. Por ejemplo, si nota que se está produciendo demasiada GC, seguirá aumentando el tamaño de la memoria siempre que esté por debajo de -XmxN y se cumplan los objetivos de rendimiento deseados.

Si sabe exactamente cuánta memoria necesita su aplicación, puede configurar -XmsN = -XmxN. En este caso, la JVM no necesita determinar un valor «óptimo» del montón y, por lo tanto, el proceso de GC se vuelve un poco más eficiente.

Tamaños de generación

Puede decidir qué parte del montón desea asignar al YG y cuánto desea asignar al OG. Ambos valores afectan el rendimiento de nuestras aplicaciones de la siguiente manera.

Si el tamaño del YG es muy grande, entonces se recolectaría con menos frecuencia. Esto daría como resultado una menor cantidad de objetos promovidos al OG. Por otro lado, si aumenta demasiado el tamaño de OG, recolectarlo y compactarlo tomaría demasiado tiempo y esto conduciría a largas pausas de STW. Por tanto, el usuario tiene que encontrar un equilibrio entre estos dos valores.

A continuación se muestran las banderas que puede utilizar para establecer estos valores:

  • -XX:NewRatio=N: Relación entre YG y OG (valor predeterminado = 2)
  • -XX:NewSize=N: tamaño inicial de YG
  • -XX:MaxNewSize=N: tamaño máximo de YG
  • -XmnN: establece NewSize y MaxNewSize en el mismo valor usando esta bandera

El tamaño inicial de YG está determinado por el valor de NewRatio mediante la fórmula dada:

(total heap size)/(newRatio +1)

Dado que el valor inicial de newRatio es 2, la fórmula anterior da que el valor inicial de YG sea 1/3 del tamaño total del montón. Siempre puede anular este valor especificando explícitamente el tamaño de YG usando el indicador NewSize. Este indicador no tiene ningún valor predeterminado y, si no se establece explícitamente, el tamaño del YG se seguirá calculando utilizando la fórmula anterior.

Permagen y Metaespacio

El permagen y el metaespacio son áreas de almacenamiento dinámico donde la JVM guarda los metadatos de las clases. El espacio se llama ‘permagen’ en Java 7, y en Java 8, se llama ‘metaespacio’. Esta información es utilizada por el compilador y el tiempo de ejecución.

Puede controlar el tamaño del permagen usando las siguientes banderas: -XX: PermSize=N y -XX:MaxPermSize=N . El tamaño de Metaspace se puede controlar usando: -XX:Metaspace-Size=N y -XX:MaxMetaspaceSize=N .

Existen algunas diferencias en cómo se gestionan el permagen y el metaespacio cuando los valores de las banderas no están establecidos. De forma predeterminada, ambos tienen un tamaño inicial predeterminado. Pero mientras el metaespacio puede ocupar tanto del montón como sea necesario, el permagen no puede ocupar más que los valores iniciales predeterminados. Por ejemplo, la JVM 64b tiene 82 M de espacio de almacenamiento dinámico como tamaño máximo permagen.

Tenga en cuenta que…

Tenga en cuenta que, dado que el metaespacio puede ocupar cantidades ilimitadas de memoria a menos que se especifique lo contrario, puede haber un error de falta de memoria. Se produce una GC completa cada vez que se cambia el tamaño de estas regiones. Por lo tanto, durante el inicio, si hay muchas clases que se están cargando, el metaespacio puede seguir cambiando de tamaño, lo que resulta en un GC completo cada vez. Por lo tanto, las aplicaciones grandes tardan mucho en iniciarse en caso de que el tamaño del metaespacio inicial sea demasiado bajo. Es una buena idea aumentar el tamaño inicial ya que reduce el tiempo de inicio.

Aunque el permagen y el metaespacio contienen los metadatos de la clase, no son permanentes y el GC recupera el espacio, como en el caso de los objetos. Esto suele ocurrir en el caso de aplicaciones de servidor. Cada vez que realiza una nueva implementación en el servidor, los metadatos antiguos deben limpiarse ya que los nuevos cargadores de clases ahora necesitarán espacio. Este espacio es liberado por el GC.

Pérdida de memoria en Java

Aquí discutiremos sobre el concepto de pérdida de memoria en Java.

El siguiente código crea una pérdida de memoria en Java:

void queryDB() {

   try{

      Connection conn = ConnectionFactory.getConnection();

      PreparedStatement ps = conn.preparedStatement(«query»); // executes a

      SQL

      ResultSet rs = ps.executeQuery();

      while(rs.hasNext()) {

         //process the record

      }

   } catch(SQLException sqlEx) {

      //print stack trace

   }

}

En el código anterior, cuando el método sale, no hemos cerrado el objeto de conexión. Por lo tanto, la conexión física permanece abierta antes de que se active el GC y considera que el objeto de conexión es inalcanzable. Ahora llamará al método final en el objeto de conexión; sin embargo, es posible que no se implemente. Por lo tanto, el objeto no será recolectado como basura en este ciclo.

Lo mismo sucederá en el siguiente hasta que el servidor remoto vea que la conexión ha estado abierta durante mucho tiempo y la cierre por la fuerza. Por tanto, un objeto sin referencia permanece en la memoria durante mucho tiempo, lo que genera una fuga.

No te detengas, sigue avanzando

Aquí tienes un propósito para este 2024 que debes considerar seriamente: si has querido mejorar tus habilidades en hacking, Ciberseguridad y programación ahora es definitivamente el momento de dar el siguiente paso. ¡Desarrolla tus habilidades aprovechando nuestros cursos a un precio increíble y avanza en tu carrera!

Universidad Hacking. Todo en Ciberseguridad. Curso Completo

Aprende Hacking Ético y Ciberseguridad sin necesitar conocimientos Previos. Practica Hacking Ético y Ciberseguridad aquí

Calificación: 4,6 de 5 (2.877 calificaciones) 15.284 estudiantes Creado por Alvaro Chirou • 1.800.000+ Enrollments Worldwide

Lo que aprenderás

  • Aprende Seguridad informática
  • Te enseñare Hacking Ético
  • Veremos Ciberseguridad
  • La base principal del Hacking, Redes
  • Programación (Python) Necesitaras saber Python para, Hacking con Python
  • Te enseñare Análisis de Malware, además haremos laboratorios, practicas y ejecutaremos Malware para que veas su comportamiento.
  • Te enseñare a reforzar tu Privacidad y Anonimato
  • Aprenderás una de las herramientas mas populares por excelencia en el mundo del Hacking, Metasploit
  • Es importante que aprendas Seguridad informática Mobile ya que usamos nuestro celular como una PC
  • Veremos también el top 10 de Owasp Web
  • Veremos también el top 10 de Owasp mobile
  • Veremos también el top 10 de Owasp API
  • Ante la demanda del mercado, te enseñare Seguridad informática para empresas
  • Veras también la suit de herramientas de seguridad informática en un sistema operativo, Kali Linux
  • Herramientas de hacking para el celular en Termux
  • Seguridad informática en WordPress
  • Análisis de trafico en Wireshark

El Hacking Ético y Ciberseguridad es Transversal a todo lo que sea Tecnología.

¿Esto que significa?

Que hoy más que nunca, se necesitan personas capacitadas en este rubro para trabajar.

Por esa razón cree esta formación profesional para compartirte mis conocimientos y experiencia en la materia y puedas iniciar en este mundo del Hacking Ético y Ciberseguridad.

Te voy a estar acompañando en el proceso de aprendizaje, donde si estas empezando desde 0, sin conocimientos previos, no es un impedimento ya que iniciaremos como si no supieras nada de la materia.

Y si sos una persona con conocimientos, podrás iniciar directamente en el nivel más avanzado o en el que tu elijas.

Como en todos mis cursos en udemy, tendrás muchísima practica para que materialices lo que vas aprendiendo.

Empieza a aprender ya mismo!

Aprende con nuestros más de 100 cursos que tenemos disponibles para vos

¿Te gustaría enterarte de cuando lanzamos descuentos y nuevos cursos?

Sobre los autores

Álvaro Chirou

Yo soy Álvaro Chirou, tengo más de 20 Años de experiencia trabajando en Tecnología, eh dado disertaciones en eventos internacionales como OWASP, tengo más de 1.800.000 estudiantes en Udemy y 100 formaciones profesionales impartidas en la misma. Puedes serguirme en mis redes:

Laprovittera Carlos

Soy Laprovittera Carlos. Con más de 20 años de experiencia en IT brindo Educación y Consultoría en Seguridad de la Información para profesionales, bancos y empresas. Puedes saber más de mi y de mis servicios en mi sitio web: laprovittera.com y seguirme en mis redes:

¿Quieres iniciarte en hacking y ciberseguridad pero no sabes por dónde empezar? Inicia leyendo nuestra guia gratuita: https://achirou.com/como-iniciarse-en-ciberseguridad-y-hacking-en-2024/ que te lleva de 0 a 100. Desde los fundamentos más básicos, pasando por cursos, recursos y certificaciones hasta cómo obtener tu primer empleo.

SIGUE APRENDIENDO GRATIS CON NUESTRAS GUIAS

Cómo Iniciarse en Hacking y Ciberseguridad en 2024

Curso Gratis de Programación

Curso Gratis Linux – Capitulo 1 – Introducción a Linux

Curso Gratis de Redes – Capitulo 1 – Tipos de redes y servicios

Como iniciarse en TRY HACK ME – Complete Beginner #1

OSINT #1 Más de 200 Search Tools