Máquina virtual Java (JVM)

Ajustar Compartir Imprimir Citar

Una máquina virtual Java (JVM) es una máquina virtual que permite que una computadora ejecute programas Java, así como programas escritos en otros lenguajes que también se compilan en el código de bytes de Java. La JVM se detalla mediante una especificación que describe formalmente lo que se requiere en una implementación de JVM. Tener una especificación garantiza la interoperabilidad de los programas de Java en diferentes implementaciones, de modo que los autores de programas que utilizan el Kit de desarrollo de Java (JDK) no necesitan preocuparse por las idiosincrasias de la plataforma de hardware subyacente.

La implementación de referencia de JVM está desarrollada por el proyecto OpenJDK como código fuente abierto e incluye un compilador JIT llamado HotSpot. Las versiones de Java con soporte comercial disponibles de Oracle se basan en el tiempo de ejecución de OpenJDK. Eclipse OpenJ9 es otra JVM de código abierto para OpenJDK.

Especificación JVM

La máquina virtual Java es una computadora abstracta (virtual) definida por una especificación. Es una parte del entorno de tiempo de ejecución de Java. No se especifica el algoritmo de recolección de elementos no utilizados ni ninguna optimización interna de las instrucciones de la máquina virtual Java (su traducción a código de máquina). La razón principal de esta omisión es no restringir innecesariamente a los implementadores. Cualquier aplicación Java puede ejecutarse solo dentro de alguna implementación concreta de la especificación abstracta de la máquina virtual Java.

A partir de Java Platform, Standard Edition (J2SE) 5.0, los cambios en la especificación de JVM se han desarrollado bajo el Java Community Process como JSR 924. A partir de 2006, los cambios en la especificación para admitir los cambios propuestos en el formato de archivo de clase (JSR 202) se está realizando como una versión de mantenimiento de JSR 924. La especificación para JVM se publicó como el libro azul. El prefacio dice:

Tenemos la intención de que esta especificación documente suficientemente la máquina virtual de Java para hacer posibles implementaciones de sala limpia compatibles. Oracle proporciona pruebas que verifican el correcto funcionamiento de las implementaciones de la Máquina Virtual Java.

Una de las JVM de Oracle se llama HotSpot; el otro, heredado de BEA Systems, es JRockit. Oracle posee la marca comercial Java y puede permitir su uso para certificar conjuntos de implementación como totalmente compatibles con la especificación de Oracle.

Cargador de clases

Una de las unidades organizativas del código de bytes de JVM es una clase. Una implementación de cargador de clases debe ser capaz de reconocer y cargar todo lo que se ajuste al formato de archivo de clase de Java. Cualquier implementación es libre de reconocer otras formas binarias además de los archivos de clase, pero debe reconocer los archivos de clase.

El cargador de clases realiza tres actividades básicas en este estricto orden:

  1. Cargando: encuentra e importa los datos binarios para un tipo
  2. Vinculación: realiza verificación, preparación y (opcionalmente) resolución
    • Verificación: asegura la exactitud del tipo importado
    • Preparación: asigna memoria para variables de clase e inicializa la memoria a valores predeterminados
    • Resolución: transforma las referencias simbólicas del tipo en referencias directas.
  3. Inicialización: invoca el código Java que inicializa las variables de clase a sus valores iniciales adecuados.

En general, hay tres tipos de cargadores de clases: cargadores de clases de arranque, cargadores de clases de extensión y cargadores de clases de Sistema/Aplicación.

Cada implementación de máquina virtual Java debe tener un cargador de clases de arranque que sea capaz de cargar clases de confianza, así como un cargador de clases de extensión o un cargador de clases de aplicación. La especificación de la máquina virtual de Java no especifica cómo debe ubicar las clases un cargador de clases.

Arquitectura de máquinas virtuales

La JVM opera en tipos específicos de datos como se especifica en las especificaciones de la máquina virtual de Java. Los tipos de datos se pueden dividir en tipos primitivos (enteros, punto flotante, largos, etc.) y tipos de referencia. La JVM anterior era solo una máquina de 32 bits. longy doublelos tipos, que son de 64 bits, se admiten de forma nativa, pero consumen dos unidades de almacenamiento en las variables locales de un marco o en la pila de operandos, ya que cada unidad es de 32 bits. booleanLos tipos, byte, shorty chartienen extensión de signo (excepto charel que tiene extensión de cero) y funcionan como enteros de 32 bits, al igual que los inttipos. Los tipos más pequeños solo tienen algunas instrucciones específicas del tipo para cargar, almacenar y convertir tipos. se opera como valores booleande 8 bits, con 0 representandobytefalsey 1 representando true. (Aunque booleanse ha tratado como un tipo desde que The Java Virtual Machine Specification, Second Edition aclaró este problema, en el código compilado y ejecutado hay poca diferencia entre a booleany a, byteexcepto por la manipulación de nombres en las firmas de métodos y el tipo de matrices booleanas. booleans en las firmas de los métodos se alteran como Zmientras que bytelos s se alteran como B. Las matrices booleanas llevan el tipo boolean[]pero usan 8 bits por elemento, y la JVM no tiene capacidad integrada para empaquetar valores booleanos en una matriz de bits, por lo que excepto por el tipo que realizan y se comportan como mismo que las bytematrices En todos los demás usos, elbooleantipo es efectivamente desconocido para la JVM, ya que todas las instrucciones para operar en booleanos también se utilizan para operar en bytes). Sin embargo, las versiones más recientes de JVM (OpenJDK HotSpot JVM) admiten 64 bits, puede tener JVM de 32 bits / 64 bits en un sistema operativo de 64 bits. La principal ventaja de ejecutar Java en un entorno de 64 bits es el mayor espacio de direcciones. Esto permite un tamaño de montón de Java mucho mayor y un mayor número máximo de subprocesos de Java, que es necesario para ciertos tipos de aplicaciones grandes; sin embargo, hay un impacto en el rendimiento al usar JVM de 64 bits en comparación con JVM de 32 bits.

La JVM tiene un montón de basura recolectada para almacenar objetos y matrices. El código, las constantes y otros datos de clase se almacenan en el "área de método". El área del método es lógicamente parte del montón, pero las implementaciones pueden tratar el área del método por separado del montón y, por ejemplo, es posible que no la recolecten basura. Cada subproceso de JVM también tiene su propia pila de llamadas (llamada "pila de máquina virtual de Java" para mayor claridad), que almacena marcos. Se crea un nuevo marco cada vez que se llama a un método, y el marco se destruye cuando ese método sale.

Cada marco proporciona una "pila de operandos" y una matriz de "variables locales". La pila de operandos se usa para operandos en cálculos y para recibir el valor de retorno de un método llamado, mientras que las variables locales tienen el mismo propósito que los registros y también se usan para pasar argumentos de métodos. Por lo tanto, la JVM es tanto una máquina de pila como una máquina de registro. (En la práctica, HotSpot elimina por completo todas las pilas además de la pila nativa de subprocesos/llamadas incluso cuando se ejecuta en modo interpretado, ya que su intérprete de plantillas es técnicamente un compilador disfrazado)

Instrucciones de código de bytes

La JVM tiene instrucciones para los siguientes grupos de tareas:

El objetivo es la compatibilidad binaria. Cada sistema operativo host en particular necesita su propia implementación de JVM y tiempo de ejecución. Estas JVM interpretan el código de bytes semánticamente de la misma manera, pero la implementación real puede ser diferente. Más complejo que simplemente emular el código de bytes es implementar de manera compatible y eficiente la API central de Java que debe asignarse a cada sistema operativo host.

Estas instrucciones operan sobre un conjunto detipos de datos abstractos en lugar de los tipos de datos nativos de cualquier arquitectura de conjunto de instrucciones específico.

Lenguajes JVM

Un lenguaje JVM es cualquier lenguaje con funcionalidad que se puede expresar en términos de un archivo de clase válido que se puede alojar en la máquina virtual de Java. Un archivo de clase contiene instrucciones de la máquina virtual de Java (código de bytes de Java) y una tabla de símbolos, así como otra información auxiliar. El formato de archivo de clase es el formato binario independiente del hardware y del sistema operativo que se utiliza para representar clases e interfaces compiladas.

Hay varios lenguajes de JVM, tanto lenguajes antiguos portados a JVM como lenguajes completamente nuevos. JRuby y Jython son quizás los puertos más conocidos de los lenguajes existentes, es decir, Ruby y Python respectivamente. De los nuevos lenguajes que se han creado desde cero para compilar a bytecode Java, Clojure, Apache Groovy, Scala y Kotlin pueden ser los más populares. Una característica destacable de los lenguajes JVM es que son compatibles entre sí, de modo que, por ejemplo, las bibliotecas Scala se pueden utilizar con programas Java y viceversa.

Java 7 JVM implementa JSR 292: compatibilidad con lenguajes tipificados dinámicamente en la plataforma Java, una nueva característica que admite lenguajes tipificados dinámicamente en la JVM. Esta función se desarrolla dentro del proyecto Da Vinci Machine, cuya misión es extender la JVM para que admita otros lenguajes además de Java.

Verificador de código de bytes

Una filosofía básica de Java es que es intrínsecamente seguro desde el punto de vista de que ningún programa de usuario puede bloquear la máquina anfitriona o interferir de manera inapropiada con otras operaciones en la máquina anfitriona, y que es posible proteger ciertos métodos y estructuras de datos pertenecientes a sistemas confiables. código de acceso o corrupción por código no confiable que se ejecuta dentro de la misma JVM. Además, no se permite que ocurran errores comunes de programadores que a menudo conducen a la corrupción de datos o un comportamiento impredecible, como el acceso desde el final de una matriz o el uso de un puntero no inicializado. Varias características de Java se combinan para proporcionar esta seguridad, incluido el modelo de clase, el montón de recolección de elementos no utilizados y el verificador.

La JVM verifica todo el código de bytes antes de ejecutarse. Esta verificación consiste principalmente en tres tipos de controles:

Las dos primeras de estas comprobaciones tienen lugar principalmente durante el paso de verificación que se produce cuando se carga una clase y se hace apta para su uso. El tercero se realiza principalmente de forma dinámica, cuando otra clase accede por primera vez a elementos de datos o métodos de una clase.

El verificador permite solo algunas secuencias de código de bytes en programas válidos, por ejemplo, una instrucción de salto (bifurcación) solo puede apuntar a una instrucción dentro del mismo método. Además, el verificador asegura que cualquier instrucción dada opere en una ubicación de pila fija,permitiendo que el compilador JIT transforme los accesos a la pila en accesos a registros fijos. Debido a esto, que la JVM sea una arquitectura de pila no implica una penalización de velocidad para la emulación en arquitecturas basadas en registros cuando se usa un compilador JIT. Frente a la arquitectura JVM verificada por código, no hace ninguna diferencia para un compilador JIT si recibe nombres de registros imaginarios o posiciones de pila imaginarias que deben asignarse a los registros de la arquitectura de destino. De hecho, la verificación del código hace que la JVM sea diferente de una arquitectura de pila clásica, cuya emulación eficiente con un compilador JIT es más complicada y, por lo general, la lleva a cabo un intérprete más lento. Además, el intérprete utilizado por la JVM predeterminada es un tipo especial conocido como intérprete de plantilla, que traduce el código de bytes directamente a nativo,(En muchos aspectos, HotSpot Interpreter puede considerarse un compilador JIT en lugar de un verdadero intérprete), lo que significa que la arquitectura de pila a la que apunta el código de bytes no se usa realmente en la implementación, sino simplemente una especificación para la representación intermedia que bien puede implementarse en una arquitectura basada en registros (otra instancia de una arquitectura de pila que es simplemente una especificación e implementada en una máquina virtual basada en registros es Common Language Runtime).

La especificación original para el verificador de código de bytes usaba un lenguaje natural que estaba incompleto o era incorrecto en algunos aspectos. Se han realizado varios intentos para especificar la JVM como un sistema formal. Al hacer esto, la seguridad de las implementaciones actuales de JVM se puede analizar más a fondo y se pueden prevenir posibles vulnerabilidades de seguridad. También será posible optimizar la JVM omitiendo comprobaciones de seguridad innecesarias, si se demuestra que la aplicación que se está ejecutando es segura.

Ejecución segura de código remoto

Una arquitectura de máquina virtual permite un control muy detallado sobre las acciones que puede realizar el código dentro de la máquina. Asume que el código es "semánticamente" correcto, es decir, pasó con éxito el proceso de verificación de código de bytes (formal), materializado por una herramienta, posiblemente externa a la máquina virtual. Esto está diseñado para permitir la ejecución segura de código que no es de confianza desde fuentes remotas, un modelo utilizado por los applets de Java y otras descargas de código seguras. Una vez verificado el código de bytes, el código descargado se ejecuta en un "sandbox" restringido, que está diseñado para proteger al usuario contra el mal comportamiento o el código malicioso. Como complemento al proceso de verificación de código de bytes, los editores pueden comprar un certificado con el que firmar applets digitalmente como seguros,

La industria de Javacard ha realizado una prueba formal de los verificadores de código de bytes (Desarrollo formal de un verificador integrado para el código de bytes de la tarjeta Java)

Intérprete de bytecode y compilador justo a tiempo

Para cada arquitectura de hardware se necesita un intérprete de bytecode de Java diferente. Cuando una computadora tiene un intérprete de código de bytes de Java, puede ejecutar cualquier programa de código de bytes de Java, y el mismo programa se puede ejecutar en cualquier computadora que tenga dicho intérprete.

Cuando un intérprete ejecuta el bytecode de Java, la ejecución siempre será más lenta que la ejecución del mismo programa compilado en lenguaje de máquina nativo. Este problema se mitiga mediante compiladores justo a tiempo (JIT) para ejecutar el código de bytes de Java. Un compilador JIT puede traducir el código de bytes de Java al lenguaje de máquina nativo mientras ejecuta el programa. Las partes traducidas del programa pueden entonces ejecutarse mucho más rápido de lo que podrían interpretarse. Esta técnica se aplica a aquellas partes de un programa que se ejecutan con frecuencia. De esta forma, un compilador JIT puede acelerar significativamente el tiempo de ejecución general.

No existe una conexión necesaria entre el lenguaje de programación Java y el código de bytes de Java. Un programa escrito en Java se puede compilar directamente en el lenguaje de máquina de una computadora real y los programas escritos en otros lenguajes que no sean Java se pueden compilar en el código de bytes de Java.

El código de bytes de Java está destinado a ser independiente de la plataforma y seguro. Algunas implementaciones de JVM no incluyen un intérprete, sino que consisten solo en un compilador justo a tiempo.

JVM en el navegador web

Al comienzo de la vida útil de la plataforma Java, la JVM se comercializó como una tecnología web para crear aplicaciones web enriquecidas. A partir de 2018, la mayoría de los navegadores web y sistemas operativos que incluyen navegadores web no incluyen un complemento de Java, ni permiten la carga lateral de ningún complemento que no sea Flash. El complemento del navegador Java quedó obsoleto en JDK 9.

El complemento del navegador NPAPI Java fue diseñado para permitir que la JVM ejecute los llamados applets de Java incrustados en páginas HTML. Para los navegadores con el complemento instalado, el subprograma puede dibujar en una región rectangular en la página que se le asignó. Debido a que el complemento incluye una JVM, los subprogramas Java no están restringidos al lenguaje de programación Java; cualquier idioma destinado a la JVM puede ejecutarse en el complemento. Un conjunto restringido de API permite que los applets accedan al micrófono del usuario o a la aceleración 3D, aunque los applets no pueden modificar la página fuera de su región rectangular. Adobe Flash Player, la principal tecnología competidora, funciona de la misma manera en este sentido.

En junio de 2015, según W3Techs, el uso de subprogramas Java y Silverlight había caído al 0,1 % cada uno para todos los sitios web, mientras que Flash había caído al 10,8 %.

JVM e intérpretes de JavaScript

A partir de mayo de 2016, JavaPoly permite a los usuarios importar bibliotecas de Java no modificadas e invocarlas directamente desde JavaScript. JavaPoly permite que los sitios web utilicen bibliotecas de Java sin modificar, incluso si el usuario no tiene Java instalado en su computadora.

Compilación a JavaScript

Con las continuas mejoras en la velocidad de ejecución de JavaScript, combinadas con el mayor uso de dispositivos móviles cuyos navegadores web no implementan soporte para complementos, se están realizando esfuerzos para dirigirse a esos usuarios a través de la compilación en JavaScript. Es posible compilar el código fuente o el código de bytes JVM en JavaScript.

La compilación del código de bytes de JVM, que es universal en todos los lenguajes de JVM, permite construir sobre el código de bytes existente del compilador del lenguaje. El código de bytes principal de JVM para los compiladores de JavaScript son TeaVM, el compilador contenido en Dragome Web SDK, Bck2Brwsr y j2js-compiler.

Los principales compiladores de lenguajes JVM a JavaScript incluyen el compilador de Java a JavaScript contenido en Google Web Toolkit, Clojurescript (Clojure), GrooScript (Apache Groovy), Scala.js (Scala) y otros.