¿De qué sirve convertir el código fuente a bytecode Java?


37

Si uno necesita diferentes JVM para diferentes arquitecturas, no puedo entender cuál es la lógica detrás de la introducción de este concepto. En otros idiomas, necesitamos diferentes compiladores para diferentes máquinas, pero en Java requerimos diferentes JVM, ¿cuál es la lógica detrás de la introducción del concepto de JVM o este paso adicional?



12
@gnat: En realidad, eso no es un duplicado. Este es "código fuente vs código de bytes", es decir, solo la primera transformación. En términos de lenguaje, esto es Javascript versus Java; su enlace sería C ++ versus Java.
MSalters

2
¿Prefiere escribir un intérprete de código de bytes simple para esos 50 modelos de dispositivos a los que está agregando codificación digital para la actualización o 50 compiladores para 50 hardware diferente? Java fue desarrollado originalmente para electrodomésticos y maquinaria. Ese era su punto fuerte. Tenga esto en cuenta al leer estas respuestas, ya que java no tiene una verdadera ventaja hoy en día (debido a la ineficiencia del proceso de interpretación). Es solo un modelo que seguimos usando.
The Great Duck

1
Usted parece no entender lo que una máquina virtual es . Es una maquina. Podría implementarse en hardware con compiladores de código nativo (y en el caso de la JVM lo ha sido). La parte 'virtual' es lo importante aquí: esencialmente estás emulando esa arquitectura sobre otra. Digamos que escribí un emulador 8088 para ejecutar en x86. No va a portar el antiguo código 8088 a x86, solo lo ejecutará en la plataforma emulada. La JVM es una máquina a la que apuntas como cualquier otra, la diferencia es que se ejecuta sobre las otras plataformas.
Jared Smith

77
@TheGreatDuck ¿Proceso de interpretación? La mayoría de las JVM hoy en día compilan justo a tiempo el código de la máquina. Sin mencionar que "interpretación" es un término bastante amplio hoy en día. La propia CPU simplemente "interpreta" el código x86 en su propio microcódigo interno, y se utiliza para mejorar la eficiencia. Las últimas CPU de Intel también son muy adecuadas para los intérpretes en general (aunque, por supuesto, encontrará puntos de referencia para probar lo que quiera probar).
Luaan

Respuestas:


79

La lógica es que el código de bytes JVM es mucho más simple que el código fuente de Java.

Se puede pensar que los compiladores, en un nivel muy abstracto, tienen tres partes básicas: análisis, análisis semántico y generación de código.

El análisis consiste en leer el código y convertirlo en una representación de árbol dentro de la memoria del compilador. El análisis semántico es la parte en la que analiza este árbol, descubre lo que significa y simplifica todas las construcciones de alto nivel a las de nivel inferior. Y la generación de código toma el árbol simplificado y lo escribe en una salida plana.

Con un archivo de código de bytes, la fase de análisis se simplifica enormemente, ya que está escrita en el mismo formato de flujo de bytes plano que utiliza el JIT, en lugar de un lenguaje fuente recursivo (estructurado en árbol). Además, gran parte del trabajo pesado del análisis semántico ya lo ha realizado el compilador de Java (u otro lenguaje). Por lo tanto, todo lo que tiene que hacer es leer el código en secuencia, realizar un análisis mínimo y un análisis semántico mínimo, y luego realizar la generación del código.

Esto hace que la tarea que el JIT debe realizar sea mucho más simple y, por lo tanto, mucho más rápida de ejecutar, al tiempo que conserva los metadatos de alto nivel y la información semántica que hace posible escribir teóricamente código multiplataforma de fuente única.


77
Algunos de los otros intentos iniciales de distribución de applets, como SafeTCL, realmente distribuyeron el código fuente. El uso de Java de un bytecode simple y estrictamente especificado hace que la verificación del programa sea mucho más manejable, y ese fue el problema difícil que se estaba resolviendo. Los códigos de bytes como el código p ya se conocían como parte de la solución al problema de portabilidad (y ANDF probablemente estaba en desarrollo en ese momento).
Toby Speight

99
Precisamente. Los tiempos de inicio de Java ya son un problema debido al bytecode -> paso del código de máquina. Ejecute javac en su proyecto (no trivial), y luego imagine hacer todo ese código Java -> máquina en cada inicio.
Paul Draper

24
Tiene otro gran beneficio: si algún día todos queremos cambiar a un nuevo idioma hipotético, llamémoslo "Scala", solo necesitamos escribir un compilador Scala -> bytecode, en lugar de docenas de Scala -> código de máquina compiladores Como beneficio adicional, obtenemos todas las optimizaciones específicas de la plataforma de JVM de forma gratuita.
BlueRaja - Danny Pflughoeft

8
Algunas cosas aún no son posibles en el código de bytes JVM, como la optimización de llamadas de cola. Recuerdo que esto compromete enormemente un lenguaje funcional que compila a JVM.
JDługosz

8
@ JDługosz a la derecha: JVM desafortunadamente impone algunas restricciones / modismos de diseño que, si bien pueden ser perfectamente naturales si vienes de un lenguaje imperativo, pueden convertirse en una obstrucción artificial si quieres escribir un compilador para un lenguaje que funciona fundamentalmente diferente. Por lo tanto, considero que LLVM es un mejor objetivo, en lo que respecta a la reutilización del trabajo del lenguaje en el futuro, también tiene limitaciones, pero coinciden más o menos con las limitaciones que tienen los procesadores actuales (y probablemente algún tiempo en el futuro) de todos modos.
Leftaroundabout

27

Las representaciones intermedias de varios tipos son cada vez más comunes en el diseño del compilador / tiempo de ejecución, por algunas razones.

En el caso de Java, la razón número uno inicialmente era probablemente la portabilidad : Java se comercializó inicialmente como "Escribir una vez, ejecutar en cualquier lugar". Si bien puede lograr esto distribuyendo el código fuente y utilizando diferentes compiladores para apuntar a diferentes plataformas, esto tiene algunas desventajas:

  • los compiladores son herramientas complejas que deben comprender todas las sintaxis de conveniencia del lenguaje; bytecode puede ser un lenguaje más simple, ya que está más cerca del código ejecutable por máquina que la fuente legible por humanos; esto significa:
    • la compilación puede ser lenta en comparación con la ejecución de bytecode
    • los compiladores que apuntan a diferentes plataformas pueden terminar produciendo un comportamiento diferente o no mantenerse al día con los cambios de idioma
    • producir un compilador para una nueva plataforma es mucho más difícil que producir una VM (o compilador bytecode-to-native) para esa plataforma
  • distribuir el código fuente no siempre es deseable; bytecode ofrece cierta protección contra la ingeniería inversa (aunque todavía es bastante fácil descompilar a menos que se ofusque deliberadamente)

Otras ventajas de una representación intermedia incluyen:

  • optimización , donde los patrones pueden detectarse en el código de bytes y compilarse a equivalentes más rápidos, o incluso optimizarse para casos especiales a medida que se ejecuta el programa (usando un compilador "JIT" o "Just In Time")
  • interoperabilidad entre múltiples idiomas en la misma VM; Esto se ha vuelto popular con la JVM (por ejemplo, Scala), y es el objetivo explícito del marco .net

1
Java también estaba orientado a sistemas integrados. En tales sistemas, el hardware tenía varias limitaciones de memoria y CPU.
Laiv

¿Se pueden desarrollar los compiladores de manera que primero compilen el código fuente de Java en código de bytes y luego compilen el código de bytes en el código de máquina? ¿Eliminaría la mayoría de las desventajas que mencionaste?
Sher10ck

@ Sher10ck Sí, es perfectamente posible que AFAIK escriba un compilador que convierta estáticamente el código de bytes JVM en instrucciones de máquina para una arquitectura particular. Pero solo tendría sentido si mejorara el rendimiento lo suficiente como para superar el esfuerzo adicional para el distribuidor o el tiempo extra para el primer uso para el usuario. Un sistema integrado de baja potencia podría beneficiarse; Una PC moderna que descargue y ejecute muchos programas diferentes probablemente sería mejor con un JIT bien ajustado. Creo que Android va a algún lado en esta dirección, pero no conozco los detalles.
IMSoP

8

Parece que te estás preguntando por qué no solo distribuimos el código fuente. Permítanme cambiar esa pregunta: ¿por qué no solo distribuimos código de máquina?

Claramente, la respuesta aquí es que Java, por diseño, no asume que sabe cuál es la máquina donde se ejecutará su código; podría ser una computadora de escritorio, una supercomputadora, un teléfono o cualquier cosa que esté en el medio y más allá. Java deja espacio para que el compilador JVM local haga lo suyo. Además de aumentar la portabilidad de su código, tiene el beneficio de permitir que el compilador haga cosas como aprovechar las optimizaciones específicas de la máquina, si existen, o aún producir al menos código de trabajo si no lo hacen. Cosas como las instrucciones SSE o la aceleración de hardware se pueden usar solo en las máquinas que las admiten.

Visto desde esta perspectiva, el razonamiento para usar el código de bytes sobre el código fuente sin procesar es más claro. Acercarse lo más posible al lenguaje de máquina sin procesar nos permite darnos cuenta o darnos cuenta parcialmente de algunos de los beneficios del código de máquina, como:

  • Tiempos de inicio más rápidos, ya que parte de la compilación y el análisis ya está hecho.
  • Seguridad, ya que el formato de código de bytes tiene un mecanismo incorporado para firmar los archivos de distribución (la fuente podría hacerlo por convención, pero el mecanismo para lograr esto no está integrado de la manera en que lo está con el código de bytes).

Tenga en cuenta que no menciono una ejecución más rápida. Tanto el código fuente como el código de bytes son o pueden (en teoría) compilarse completamente en el mismo código de máquina para la ejecución real.

Además, el código de bytes permite algunas mejoras sobre el código de máquina. Por supuesto, existe la independencia de la plataforma y las optimizaciones específicas del hardware que mencioné anteriormente, pero también hay cosas como dar servicio al compilador JVM para producir nuevas rutas de ejecución a partir del código antiguo. Esto puede ser para parchear problemas de seguridad, o si se descubren nuevas optimizaciones, o para aprovechar las nuevas instrucciones de hardware. En la práctica, es raro ver grandes cambios de esta manera, porque puede exponer errores, pero es posible, y es algo que sucede de manera pequeña todo el tiempo.


8

Parece que hay al menos dos posibles preguntas diferentes aquí. Uno se trata realmente de compiladores en general, con Java básicamente solo un ejemplo del género. El otro es más específico para Java, los códigos de bytes específicos que utiliza.

Compiladores en general

Consideremos primero la pregunta general: ¿por qué un compilador usaría una representación intermedia en el proceso de compilación del código fuente para ejecutarse en algún procesador en particular?

Reducción de Complejidad

Una respuesta a eso es bastante simple: convierte un problema O (N * M) en un problema O (N + M).

Si se nos dan N idiomas de origen y M objetivos, y cada compilador es completamente independiente, entonces necesitamos N * M compiladores para traducir todos esos idiomas de origen a todos esos objetivos (donde un "objetivo" es algo así como una combinación de un procesador y sistema operativo).

Sin embargo, si todos esos compiladores están de acuerdo en una representación intermedia común, entonces podemos tener N front-end del compilador que traducen los lenguajes de origen a la representación intermedia, y M back-end del compilador que traduce la representación intermedia a algo adecuado para un objetivo específico.

Segmentación de problemas

Mejor aún, separa el problema en dos dominios más o menos exclusivos. Las personas que conocen / se preocupan por el diseño del lenguaje, el análisis y cosas por el estilo pueden concentrarse en los componentes del compilador, mientras que las personas que conocen los conjuntos de instrucciones, el diseño del procesador y cosas por el estilo pueden concentrarse en el back-end.

Entonces, por ejemplo, dado algo como LLVM, tenemos muchos front-end para varios idiomas diferentes. También tenemos back-end para muchos procesadores diferentes. Un chico de idiomas puede escribir un nuevo front-end para su idioma y rápidamente admite muchos objetivos. Un chico de procesador puede escribir un nuevo back-end para su objetivo sin tener que lidiar con el diseño del lenguaje, el análisis, etc.

Separar los compiladores en un front-end y back-end, con una representación intermedia para comunicarse entre los dos no es original con Java. Ha sido una práctica bastante común durante mucho tiempo (desde mucho antes de que apareciera Java, de todos modos).

Modelos de distribución

En la medida en que Java agregó algo nuevo a este respecto, estaba en el modelo de distribución. En particular, a pesar de que los compiladores se han separado internamente en piezas de front-end y back-end durante mucho tiempo, generalmente se distribuyeron como un solo producto. Por ejemplo, si compró un compilador de Microsoft C, internamente tenía un "C1" y un "C2", que eran el front-end y el back-end respectivamente, pero lo que compró fue solo "Microsoft C" que incluía ambos piezas (con un "controlador compilador" que coordinaba las operaciones entre los dos). A pesar de que el compilador se construyó en dos partes, para un desarrollador normal que usa el compilador, fue solo una cosa que se tradujo del código fuente al código objeto, sin nada visible en el medio.

Java, en cambio, distribuyó el front-end en el Kit de desarrollo de Java y el back-end en la Máquina virtual de Java. Cada usuario de Java tenía un back-end compilador para apuntar al sistema que estaba usando. Los desarrolladores de Java distribuyeron código en el formato intermedio, por lo que cuando un usuario lo cargó, la JVM hizo lo que fue necesario para ejecutarlo en su máquina en particular.

Precedentes

Tenga en cuenta que este modelo de distribución tampoco era completamente nuevo. Solo por ejemplo, el sistema P de UCSD funcionó de manera similar: los componentes del compilador produjeron código P, y cada copia del sistema P incluía una máquina virtual que hacía lo necesario para ejecutar el código P en ese objetivo en particular 1 .

Código de bytes Java

El código de bytes de Java es bastante similar al código P. Se trata básicamente de instrucciones para una justa máquina simple. Esa máquina está destinada a ser una abstracción de las máquinas existentes, por lo que es bastante fácil traducir rápidamente a casi cualquier objetivo específico. La facilidad de traducción fue importante desde el principio porque la intención original era interpretar los códigos de bytes, como lo había hecho P-System (y sí, así es exactamente como funcionaban las primeras implementaciones).

Fortalezas

El código de bytes de Java es fácil de producir para un compilador front-end. Si (por ejemplo) tiene un árbol bastante típico que representa una expresión, generalmente es bastante fácil atravesar el árbol y generar código bastante directamente a partir de lo que encuentra en cada nodo.

Los códigos de bytes de Java son bastante compactos, en la mayoría de los casos, mucho más compactos que el código fuente o el código de máquina para la mayoría de los procesadores típicos (y, especialmente para la mayoría de los procesadores RISC, como el SPARC que Sun vendió cuando diseñaron Java). Esto fue particularmente importante en ese momento, porque una de las principales intenciones de Java era admitir applets (código incrustado en páginas web que se descargarían antes de la ejecución) en un momento en que la mayoría de las personas accedían a nosotros a través de módems a través de líneas telefónicas a aproximadamente 28.8 kilobits por segundo (aunque, por supuesto, todavía había bastantes personas que usaban módems más antiguos y más lentos).

Debilidades

La principal debilidad de los códigos de bytes de Java es que no son particularmente expresivos. Aunque pueden expresar los conceptos presentes en Java bastante bien, no funcionan tan bien para expresar conceptos que no son parte de Java. Del mismo modo, si bien es fácil ejecutar códigos de bytes en la mayoría de las máquinas, es mucho más difícil hacerlo de una manera que aproveche al máximo cualquier máquina en particular.

Por ejemplo, es bastante rutinario que si realmente desea optimizar los códigos de bytes de Java, básicamente realice una ingeniería inversa para traducirlos hacia atrás desde una representación similar a un código de máquina, y volverlos a convertir en instrucciones SSA (o algo similar) 2 . Luego manipulas las instrucciones de la SSA para hacer tu optimización, luego traduces desde allí a algo que se dirija a la arquitectura que realmente te importa. Sin embargo, incluso con este proceso bastante complejo, algunos conceptos que son ajenos a Java son lo suficientemente difíciles de expresar que es difícil traducir de algunos lenguajes de origen a código de máquina que se ejecuta (incluso cerca) de manera óptima en la mayoría de las máquinas típicas.

Resumen

Si está preguntando por qué usar representaciones intermedias en general, dos factores principales son:

  1. Reduzca un problema de O (N * M) a un problema de O (N + M), y
  2. Divide el problema en piezas más manejables.

Si está preguntando acerca de los detalles de los códigos de bytes de Java y por qué eligieron esta representación en particular en lugar de otra, entonces diría que la respuesta se debe en gran medida a su intención original y las limitaciones de la web en ese momento , lo que lleva a las siguientes prioridades:

  1. Representación compacta.
  2. Rápido y fácil de decodificar y ejecutar.
  3. Rápido y fácil de implementar en las máquinas más comunes.

Poder representar muchos idiomas o ejecutar de manera óptima en una amplia variedad de objetivos eran prioridades mucho más bajas (si se consideraban prioridades).


  1. Entonces, ¿por qué se olvida principalmente el sistema P? Principalmente una situación de precios. El sistema P se vendió bastante decente en Apple II, Commodore SuperPets, etc. Cuando salió la PC de IBM, el sistema P era un sistema operativo compatible, pero MS-DOS cuesta menos (desde el punto de vista de la mayoría de las personas, esencialmente se lanzó de forma gratuita) y rápidamente tenía más programas disponibles, ya que es para lo que escribieron Microsoft e IBM (entre otros).
  2. Por ejemplo, así es como funciona el hollín .

Muy cerca de los applets web: la intención original era distribuir código a los dispositivos (decodificadores ...), de la misma manera que RPC distribuye llamadas a funciones, y CORBA distribuye objetos.
ninjalj

2
Esta es una gran respuesta, y una buena idea de cómo las diferentes representaciones intermedias hacen diferentes compensaciones. :)
IMSoP

@ninjalj: Eso fue realmente Oak. Para cuando se transformó en Java, creo que las ideas del decodificador (y similares) se habían archivado (aunque soy el primero en admitir que hay un argumento justo para afirmar que Oak y Java son lo mismo).
Jerry Coffin

@TobySpeight: Sí, la expresión probablemente encaja mejor allí. Gracias.
Jerry Coffin

0

Además de las ventajas que otras personas han señalado, el código de bytes es mucho más pequeño, por lo que es más fácil de distribuir y actualizar, y ocupa menos espacio en el entorno de destino. Esto es especialmente importante en entornos con mucho espacio limitado.

También facilita la protección del código fuente protegido por derechos de autor.


2
El código de bytes de Java (y .NET) es tan fácil de convertir en una fuente razonablemente legible que hay productos para manipular nombres y, a veces, otra información para hacer esto más difícil, algo que a menudo también se hace a JavaScript para hacerlo más pequeño, ya que ahora estamos quizás configurando un bytecode para navegadores web.
LnxPrgr3

0

El sentido es que compilar el código de bytes en código máquina es más rápido que interpretar el código original en código máquina justo a tiempo. Pero necesitamos interpretaciones para hacer que nuestra aplicación sea multiplataforma, porque queremos usar nuestro código original en cada plataforma sin cambios y sin ninguna preparación (compilaciones). Entonces, primero Java compila nuestra fuente en código de bytes, luego podemos ejecutar este código de bytes en cualquier lugar y será interpretado por Java Virtual Machine para codificar el código más rápidamente. La respuesta: ahorra tiempo.


0

Originalmente, la JVM era un intérprete puro . Y obtendrá el mejor intérprete si el idioma que está interpretando es lo más simple posible. Ese era el objetivo del código de bytes: proporcionar una entrada eficientemente interpretable al entorno de tiempo de ejecución. Esta única decisión colocó a Java más cerca de un lenguaje compilado que de un lenguaje interpretado, a juzgar por su rendimiento.

Solo más tarde, cuando se hizo evidente que el rendimiento de las JVM de interpretación todavía apestaba, las personas invirtieron el esfuerzo para crear compiladores just-in-time que funcionen bien. Esto cerró un poco la brecha a lenguajes más rápidos como C y C ++. (Sin embargo, persisten algunos problemas de velocidad inherentes a Java, por lo que probablemente nunca obtendrá un entorno Java que funcione tan bien como el código C escrito).

Por supuesto, con las técnicas de compilación justo a tiempo, podríamos volver a distribuir el código fuente y compilarlo justo a tiempo en el código de máquina. Sin embargo, esto disminuiría considerablemente el rendimiento de inicio hasta que se compilen todas las partes relevantes del código. El código de bytes sigue siendo de gran ayuda porque es mucho más sencillo de analizar que el código Java equivalente.


¿Le gustaría al downvoter explicar por qué ?
cmaster

-5

El código fuente del texto es una estructura que pretende ser fácil de leer y modificar por un humano.

El código de bytes es una estructura que pretende ser fácil de leer y ejecutar por una máquina.

Dado que todo lo que JVM hace con el código es leerlo y ejecutarlo, el código de bytes es mejor para el consumo de JVM.

Noto que todavía no ha habido ningún ejemplo. Pseudo ejemplos tontos:

//Source code
i += 1 + 5 * 2 + x;

// Byte code
i += 11, i += x
____

//Source code
i = sin(1);

// Byte code
i = 0.8414709848
_____

//Source code
i = sin(x)^2+cos(x)^2;

// Byte code (actually that one isn't true)
i = 1

Por supuesto, el código de bytes no se trata solo de optimizaciones. Una gran parte de esto se trata de poder ejecutar código sin tener que preocuparse por reglas complicadas, como verificar si la clase contiene un miembro llamado "foo" en algún lugar más abajo en el archivo cuando un método se refiere a "foo".


2
Esos "ejemplos" de códigos de bytes son legibles por humanos. Ese no es el código de bytes en absoluto. Esto es engañoso y tampoco aborda la pregunta formulada.
Comodín el

@Wildcard Puede que te hayas perdido de que este sea un foro, leído por humanos. Es por eso que pongo contenido en forma legible para humanos. Dado que el foro trata sobre ingeniería de software, pedir a los lectores que comprendan el concepto de abstracción simple no es pedir mucho.
Peter

La forma legible por humanos es el código fuente, no el código de bytes. Está ilustrando el código fuente con expresiones precalculadas, NO con código de bytes. Y no me perdí que este es un foro legible para humanos: usted fue quien criticó a otros que respondieron por no incluir ningún ejemplo de código de bytes, no a mí. Así que usted dice, "Me he dado cuenta que no ha habido ningún ejemplo todavía", y luego proceder a dar a los no -examples que no ilustran el código de bytes en absoluto. Y esto todavía no aborda la pregunta en absoluto. Vuelve a leer la pregunta.
Comodín el
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.