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 están compilados en 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 Java en diferentes implementaciones, de modo que los autores de programas que utilizan Java Development Kit (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 entorno de ejecución de OpenJDK. Eclipse OpenJ9 es otra JVM de código abierto para OpenJDK.
La máquina virtual Java es una computadora abstracta (virtual) definida por una especificación. Es parte del entorno de ejecución de Java. No se especifica el algoritmo de recolección de basura utilizado 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. [2]
A partir de Java Platform, Standard Edition (J2SE) 5.0, se han desarrollado cambios en la especificación de la JVM bajo el Proceso de la Comunidad Java como JSR 924. [3] A partir de 2006 [actualizar], se están realizando cambios en la especificación para admitir los cambios propuestos en el formato de archivo de clase (JSR 202) [4] como una versión de mantenimiento de JSR 924. La especificación para la JVM se publicó como el libro azul , [5] cuyo prefacio establece:
Nuestra intención es que esta especificación documente de manera suficiente la máquina virtual Java para que sea posible implementarla en una sala limpia y compatible. 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; la otra, heredada de BEA Systems , es JRockit . Oracle posee la marca registrada Java y puede permitir su uso para certificar que las suites de implementación sean totalmente compatibles con la especificación de Oracle.
Una de las unidades organizativas del código de bytes de JVM es una clase . Una implementación de cargador de clases debe poder reconocer y cargar cualquier cosa que se ajuste al formato de archivo de clase de Java . Cualquier implementación es libre de reconocer otros formatos binarios 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 orden estricto:
En general, hay tres tipos de cargadores de clases: cargador de clases de arranque, cargador de clases de extensión y cargador de clases de sistema/aplicación.
Toda implementación de máquina virtual Java debe tener un cargador de clases de arranque que sea capaz de cargar clases confiables, 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 Java no especifica cómo un cargador de clases debe ubicar las clases.
La JVM opera sobre tipos específicos de datos como se especifica en las especificaciones de la máquina virtual Java. Los tipos de datos se pueden dividir [6] en tipos primitivos ( enteros , de punto flotante, largos, etc.) y tipos de referencia. Las JVM anteriores eran solo máquinas de 32 bitslong
. Los tipos y double
, que son de 64 bits , se admiten de forma nativa, pero consumen dos unidades de almacenamiento en las variables locales o la pila de operandos de un marco, ya que cada unidad es de 32 bits. Los tipos boolean
, byte
, short
, y char
son todos de signo extendido (excepto char
que es de cero extendido ) y se operan como enteros de 32 bits, lo mismo que int
los tipos. Los tipos más pequeños solo tienen unas pocas instrucciones específicas de tipo para cargar, almacenar y convertir tipos. se opera como valores boolean
de 8 bits , con 0 representando y 1 representando . (Aunque se ha tratado como un tipo desde que la Especificación de la Máquina Virtual Java, Segunda Edición aclaró este problema, en el código compilado y ejecutado hay poca diferencia entre a y a excepto por la alteración de nombres en las firmas de métodos y el tipo de matrices booleanas. s en las firmas de métodos se alteran como mientras que s se alteran como . Las matrices booleanas llevan el tipo pero usan 8 bits por elemento, y la JVM no tiene capacidad incorporada para empaquetar booleanos en una matriz de bits , por lo que, excepto por el tipo, funcionan y se comportan igual que las matrices. En todos los demás usos, el tipo es efectivamente desconocido para la JVM ya que todas las instrucciones para operar en booleanos también se usan para operar en s). Sin embargo, las versiones más nuevas de JVM (OpenJDK HotSpot JVM) admiten 64 bits, por lo que 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 espacio de direcciones más grande. Esto permite un tamaño de montón de Java mucho mayor y un mayor número máximo de subprocesos Java, lo cual es necesario para ciertos tipos de aplicaciones grandes; sin embargo, hay una pérdida de rendimiento al usar una JVM de 64 bits en comparación con una JVM de 32 bits.byte
false
true
boolean
boolean
byte
boolean
Z
byte
B
boolean[]
byte
boolean
byte
La JVM tiene un montón de recolección de basura para almacenar objetos y matrices. El código, las constantes y otros datos de clase se almacenan en el "área de métodos". El área de métodos es lógicamente parte del montón, pero las implementaciones pueden tratar el área de métodos por separado del montón y, por ejemplo, podrían no recolectarla como basura. Cada subproceso de la JVM también tiene su propia pila de llamadas (llamada "pila de la máquina virtual 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 utiliza para que los operandos ejecuten cálculos y para recibir el valor de retorno de un método llamado, mientras que las variables locales cumplen la misma función que los registros y también se utilizan para pasar argumentos de métodos. Por lo tanto, la JVM es tanto una máquina de pila como una máquina de registros . En la práctica, HotSpot elimina todas las pilas además de la pila de llamadas/subprocesos nativos incluso cuando se ejecuta en modo interpretado, ya que su intérprete de plantillas funciona técnicamente como un compilador.
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 la JVM y el entorno 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 principal de Java que debe asignarse a cada sistema operativo host.
Estas instrucciones operan sobre un conjunto de valores comunes.tipos de datos abstraídos en lugar de los tipos de datos nativos de cualquier arquitectura de conjunto de instrucciones específica .
Un lenguaje JVM es cualquier lenguaje con funcionalidad que se puede expresar en términos de un archivo de clase válido que puede ser alojado por la Máquina Virtual Java. Un archivo de clase contiene instrucciones de la Máquina Virtual Java ( código de bytes de Java ) y una tabla de símbolos, así como otra información auxiliar. El formato del archivo de clase es el formato binario independiente del hardware y del sistema operativo que se utiliza para representar las clases e interfaces compiladas. [7]
Existen varios lenguajes JVM, tanto lenguajes antiguos portados a JVM como lenguajes completamente nuevos. JRuby y Jython son quizás los ports más conocidos de lenguajes existentes, es decir Ruby y Python respectivamente. De los nuevos lenguajes que se han creado desde cero para compilar a bytecode de Java, Clojure , Groovy , Scala y Kotlin pueden ser los más populares. Una característica notable con los lenguajes JVM es que son compatibles entre sí , de modo que, por ejemplo, las bibliotecas de Scala se pueden usar con programas Java y viceversa. [8]
La JVM de Java 7 implementa JSR 292: Supporting Dynamically Typed Languages [9] en la plataforma Java, una nueva característica que admite lenguajes de tipado dinámico en la JVM. Esta característica se desarrolló dentro del proyecto Da Vinci Machine , cuya misión es ampliar la JVM para que admita lenguajes distintos de Java. [10] [11]
Una filosofía básica de Java es que es inherentemente 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 que pertenecen a código confiable del 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 programación que a menudo conducen a corrupción de datos o comportamiento impredecible, como acceder al final de una matriz o usar 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 basura y el verificador.
La JVM verifica todo el código de bytes antes de ejecutarlo. Esta verificación consta principalmente de tres tipos de comprobaciones:
Las dos primeras comprobaciones se realizan principalmente durante el paso de verificación que se produce cuando se carga una clase y se la habilita para su uso. La tercera se realiza principalmente de forma dinámica, cuando otra clase accede por primera vez a los elementos de datos o métodos de una clase.
El verificador permite solo algunas secuencias de bytecode en programas válidos, por ejemplo, una instrucción de salto (ramificación) solo puede apuntar a una instrucción dentro del mismo método . Además, el verificador garantiza que cualquier instrucción dada opera en una ubicación de pila fija, [12] lo que permite al compilador JIT transformar 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, a un compilador JIT no le importa si obtiene registros imaginarios nombrados o posiciones de pila imaginarias que deben asignarse a los registros de la arquitectura de destino. De hecho, la verificación de código hace que la JVM sea diferente de una arquitectura de pila clásica, de la cual la emulación eficiente con un compilador JIT es más complicada y generalmente 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 plantillas, que traduce el código de bytes directamente a un lenguaje de máquina nativo basado en registros en lugar de emular una pila como un intérprete típico. [13] En muchos aspectos, el intérprete HotSpot 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 utiliza realmente en la implementación, sino simplemente una especificación para la representación intermedia que puede implementarse bien en una arquitectura basada en registros. Otro ejemplo 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 . [14]
La especificación original del verificador de bytecode utilizaba un lenguaje natural que era incompleto o incorrecto en algunos aspectos. Se han hecho varios intentos de especificar la JVM como un sistema formal. De esta manera, se puede analizar más a fondo la seguridad de las implementaciones actuales de la JVM y evitar posibles vulnerabilidades de seguridad. También será posible optimizar la JVM saltando comprobaciones de seguridad innecesarias, si se demuestra que la aplicación que se está ejecutando es segura. [15]
Una arquitectura de máquina virtual permite un control muy detallado sobre las acciones que el código dentro de la máquina puede realizar. Supone que el código es "semánticamente" correcto, es decir, que pasó con éxito el proceso de verificación de bytecode (formal), materializado por una herramienta, posiblemente fuera de la máquina virtual. Esto está diseñado para permitir la ejecución segura de código no confiable desde fuentes remotas, un modelo utilizado por los applets de Java y otras descargas de código seguro. Una vez verificado el bytecode, el código descargado se ejecuta en un " sandbox " restringido, que está diseñado para proteger al usuario de un mal comportamiento o código malicioso. Como complemento al proceso de verificación de bytecode, los editores pueden comprar un certificado con el que firmar digitalmente los applets como seguros, lo que les da permiso para pedirle al usuario que salga del sandbox y acceda al sistema de archivos local, al portapapeles , ejecute piezas externas de software o a la red.
La industria de Javacard ha realizado pruebas formales de verificadores de bytecode (Desarrollo formal de un verificador integrado para código de bytes de Java Card [16] )
Para cada arquitectura de hardware se necesita un intérprete de bytecode de Java diferente . Cuando una computadora tiene un intérprete de bytecode de Java, puede ejecutar cualquier programa de bytecode de Java, y el mismo programa puede ejecutarse en cualquier computadora que tenga dicho intérprete.
Cuando un intérprete ejecuta un 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 con los compiladores Just-In-Time (JIT) para ejecutar el bytecode de Java. Un compilador JIT puede traducir el bytecode de Java al lenguaje de máquina nativo mientras ejecuta el programa. Las partes traducidas del programa se pueden ejecutar mucho más rápido de lo que se podrían interpretar. Esta técnica se aplica a aquellas partes de un programa que se ejecutan con frecuencia. De esta manera, 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 bytecode de Java. Un programa escrito en Java se puede compilar directamente en el lenguaje de máquina de un ordenador real y los programas escritos en otros lenguajes distintos de Java se pueden compilar en bytecode de Java.
El bytecode de Java está pensado para ser independiente de la plataforma y seguro. [17] Algunas implementaciones de JVM no incluyen un intérprete, sino que consisten únicamente en un compilador justo a tiempo. [18]
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 [actualizar], la mayoría de los navegadores web y sistemas operativos que incluyen navegadores web no se entregan con un complemento de Java ni permiten la carga lateral de ningún complemento que no sea Flash . El complemento de navegador de Java quedó obsoleto en JDK 9. [19]
El complemento NPAPI para navegadores Java fue diseñado para permitir que la JVM ejecute los denominados applets de Java integrados en páginas HTML. En el caso de los navegadores que tienen instalado el complemento, se permite que el applet dibuje en una región rectangular de la página que se le ha asignado. Como el complemento incluye una JVM, los applets de Java no están restringidos al lenguaje de programación Java; cualquier lenguaje que tenga como destino 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.
[actualizar]Según W3Techs, en junio de 2015 , el uso de subprogramas Java y Silverlight había caído al 0,1 % cada uno para todos los sitios web, mientras que el de Flash había caído al 10,8 %. [20]
Desde mayo de 2016, JavaPoly permite a los usuarios importar bibliotecas Java sin modificar e invocarlas directamente desde JavaScript. JavaPoly permite que los sitios web utilicen bibliotecas Java sin modificar, incluso si el usuario no tiene Java instalado en su computadora. [21]
Con las mejoras continuas en la velocidad de ejecución de JavaScript, combinadas con el aumento del uso de dispositivos móviles cuyos navegadores web no implementan compatibilidad con complementos, se están realizando esfuerzos para llegar a esos usuarios mediante la transpilación a JavaScript. Es posible transpilar el código fuente o el bytecode de JVM a JavaScript.
La compilación del bytecode de la JVM, que es universal en todos los lenguajes de la JVM, permite desarrollar el bytecode a partir del compilador existente del lenguaje. Los principales transpiladores de bytecode de la JVM a JavaScript son TeaVM, [22] el compilador incluido en Dragome Web SDK, [23] Bck2Brwsr, [24] y j2js-compiler. [25]
Los principales transpiladores de lenguajes JVM a JavaScript incluyen el transpilador de Java a JavaScript contenido en Google Web Toolkit , Clojurescript (Clojure), GrooScript (Apache Groovy), Scala.js (Scala) y otros. [26]