web-dev-qa-db-fra.com

À quoi sert la conversion du code source en Java bytecode?

Si l'on a besoin de JVM différentes pour différentes architectures, je ne peux pas comprendre quelle est la logique derrière l'introduction de ce concept. Dans d'autres langages, nous avons besoin de différents compilateurs pour différentes machines, mais en Java nous avons besoin de JVM différentes, alors quelle est la logique derrière l'introduction du concept d'une JVM ou de cette étape supplémentaire ??

37
Pranjal Kumar

La logique est que le bytecode JVM est beaucoup plus simple que Java code source.

Les compilateurs peuvent être considérés, à un niveau très abstrait, comme ayant trois parties fondamentales: l'analyse, l'analyse sémantique et la génération de code.

L'analyse consiste à lire le code et à le transformer en une représentation arborescente dans la mémoire du compilateur. L'analyse sémantique est la partie où elle analyse cet arbre, comprend ce que cela signifie et simplifie toutes les constructions de haut niveau jusqu'aux constructions de niveau inférieur. Et la génération de code prend l'arborescence simplifiée et l'écrit dans une sortie plate.

Avec un fichier de bytecode, la phase d'analyse est grandement simplifiée, car elle est écrite dans le même format de flux d'octets plats que le JIT utilise, plutôt que dans un langage source récursif (structuré en arbre). De plus, une grande partie de la lourde tâche de l'analyse sémantique a déjà été effectuée par le compilateur Java (ou autre langage). Tout ce qu'il a à faire est donc de lire le code en continu, de faire un minimum l'analyse et l'analyse sémantique minimale, puis effectuer la génération de code.

Cela rend la tâche que le JIT doit effectuer beaucoup plus simple, et donc beaucoup plus rapide à exécuter, tout en préservant les métadonnées de haut niveau et les informations sémantiques qui permettent théoriquement d'écrire du code multiplateforme à source unique.

79
Mason Wheeler

Les représentations intermédiaires de divers types sont de plus en plus courantes dans la conception de compilateur/runtime, pour plusieurs raisons.

Dans le cas de Java, la raison numéro un au départ était probablement portabilité: Java a été initialement commercialisé comme "Write Once, Run Anywhere". Bien que vous puissiez y parvenir en distribuant le code source et l'utilisation de différents compilateurs pour cibler différentes plates-formes, cela a quelques inconvénients:

  • les compilateurs sont des outils complexes qui doivent comprendre toutes les syntaxes de commodité du langage; le bytecode peut être un langage plus simple, car il est plus proche du code exécutable par une machine que d'une source lisible par l'homme; ça signifie:
    • la compilation peut être lente par rapport à l'exécution de bytecode
    • les compilateurs ciblant différentes plates-formes peuvent finir par produire un comportement différent ou ne pas suivre les changements de langue
    • produire un compilateur pour une nouvelle plate-forme est beaucoup plus difficile que de produire un VM (ou compilateur bytecode-to-native) pour cette plate-forme
  • la distribution de code source n'est pas toujours souhaitable; bytecode offre une certaine protection contre la rétro-ingénierie (bien qu'il soit encore assez facile à décompiler à moins qu'il ne soit délibérément obscurci)

Les autres avantages d'une représentation intermédiaire incluent:

  • optimisation, où les modèles peuvent être repérés dans le bytecode et compilés en équivalents plus rapides, ou même optimisés pour des cas spéciaux pendant l'exécution du programme (en utilisant un compilateur "JIT" ou "Just In Time")
  • interopérabilité entre plusieurs langues dans la même machine virtuelle; cela est devenu populaire avec la JVM (par exemple Scala), et c'est l'objectif explicite du framework .net
27
IMSoP

On dirait que vous vous demandez pourquoi nous ne distribuons pas seulement du code source. Permettez-moi de retourner cette question: pourquoi ne distribuons-nous pas simplement du code machine?

De toute évidence, la réponse ici est que Java, par conception, ne suppose pas qu'il sait quelle est la machine où votre code s'exécutera; il peut s'agir d'un ordinateur de bureau, d'un super-ordinateur, d'un téléphone ou de tout ce qui se situe entre et au-delà. Java laisse la place au compilateur JVM local pour faire son travail. En plus d'augmenter la portabilité de votre code, cela a l'avantage de permettre au compilateur de faire des choses comme profiter de la machine- des optimisations spécifiques, si elles existent, ou produisent toujours au moins du code de travail dans le cas contraire. Des choses comme SSE les instructions ou l'accélération matérielle ne peuvent être utilisées que sur les machines qui les soutiennent.

Vu sous cet angle, le raisonnement pour utiliser le code octet sur le code source brut est plus clair. Se rapprocher le plus possible du langage machine brut nous permet de réaliser ou de réaliser partiellement certains des avantages du code machine, tels que:

  • Temps de démarrage plus rapides, car une partie de la compilation et de l'analyse est déjà effectuée.
  • Sécurité, car le format octet-code a un mécanisme intégré pour signer les fichiers de distribution (la source pourrait le faire par convention, mais le mécanisme pour y parvenir n'est pas intégré de la même manière qu'avec le code octet).

Notez que je ne mentionne pas une exécution plus rapide. Le code source et le code d'octet sont ou peuvent (en théorie) être entièrement compilés dans le même code machine pour une exécution réelle.

De plus, le code octet permet certaines améliorations par rapport au code machine. Bien sûr, il y a l'indépendance de la plate-forme et les optimisations spécifiques au matériel que j'ai mentionnées plus tôt, mais il y a aussi des choses comme l'entretien du compilateur JVM pour produire de nouveaux chemins d'exécution à partir de l'ancien code. Cela peut être pour corriger les problèmes de sécurité, ou si de nouvelles optimisations sont découvertes, ou pour tirer parti des nouvelles instructions matérielles. Dans la pratique, il est rare de voir de grands changements de cette façon, car cela peut exposer des bogues, mais c'est possible, et c'est quelque chose qui se produit de petites manières tout le temps.

8
Joel Coehoorn

Il semble y avoir au moins deux questions différentes possibles ici. L'un concerne vraiment les compilateurs en général, avec Java essentiellement un exemple du genre. L'autre est plus spécifique à Java les codes d'octets spécifiques qu'il utilise.

Compilateurs en général

Examinons d'abord la question générale: pourquoi un compilateur utiliserait-il une représentation intermédiaire dans le processus de compilation du code source pour s'exécuter sur un processeur particulier?

Réduction de la complexité

Une réponse à cela est assez simple: il convertit un problème O (N * M) en un problème O (N + M).

Si on nous donne N langues sources et M cibles, et que chaque compilateur est complètement indépendant, alors nous avons besoin de N * M compilateurs pour traduire toutes ces langues sources vers toutes ces cibles (où une "cible" est quelque chose comme une combinaison d'un processeur et OS).

Si, cependant, tous ces compilateurs s'accordent sur une représentation intermédiaire commune, alors nous pouvons avoir N frontaux de compilateur qui traduisent les langues source en représentation intermédiaire, et M backends de compilateur qui traduisent la représentation intermédiaire en quelque chose qui convient à une cible spécifique.

Segmentation des problèmes

Mieux encore, il sépare le problème en deux domaines plus ou moins exclusifs. Les personnes qui connaissent/se soucient de la conception du langage, de l'analyse et des choses de ce genre peuvent se concentrer sur les frontaux du compilateur, tandis que les personnes qui connaissent les jeux d'instructions, la conception du processeur et des choses de ce genre peuvent se concentrer sur le backend.

Ainsi, par exemple, étant donné quelque chose comme LLVM, nous avons beaucoup de frontaux pour différentes langues différentes. Nous avons également des back-ends pour de nombreux processeurs différents. Un spécialiste de la langue peut écrire un nouveau front-end pour sa langue et prendre en charge rapidement de nombreuses cibles. Un processeur peut écrire un nouveau back-end pour sa cible sans avoir à gérer la conception du langage, l'analyse, etc.

La séparation des compilateurs en un front-end et un back-end, avec une représentation intermédiaire pour communiquer entre les deux n'est pas originale avec Java. C'est une pratique assez courante depuis longtemps (depuis bien avant Java est arrivé, de toute façon).

Modèles de distribution

Dans la mesure où Java a ajouté quelque chose de nouveau à cet égard, c'était dans le modèle de distribution. En particulier, même si les compilateurs ont été séparés en composants front-end et back-end en interne pendant une longue période temps, ils étaient généralement distribués en tant que produit unique. Par exemple, si vous avez acheté un compilateur Microsoft C, en interne, il avait un "C1" et un "C2", qui étaient respectivement le front-end et le back-end - mais quoi que vous avez acheté était juste "Microsoft C" qui comprenait les deux pièces (avec un "pilote de compilateur" qui coordonnait les opérations entre les deux). Même si le compilateur était construit en deux pièces, pour un développeur normal utilisant le compilateur, ce n'était qu'une seule chose qui traduit du code source en code objet, sans rien visible entre les deux.

Java, à la place, a distribué le frontal dans le Java, et le back-end dans la machine virtuelle Java. Chaque Java avait un back-end de compilateur pour cibler le système qu'il utilisait. Java distribuaient du code au format intermédiaire, donc quand un utilisateur le chargeait, la JVM faisait tout ce qui était nécessaire pour l'exécuter sur leur machine particulière.

Précédents

Notez que ce modèle de distribution n'était pas entièrement nouveau non plus. Par exemple, le système P UCSD fonctionnait de la même manière: les frontaux du compilateur produisaient du code P, et chaque copie du système P incluait une machine virtuelle qui faisait le nécessaire pour exécuter le code P sur cette cible particulière1.

Octet-code Java

Le code d'octet Java est assez similaire au code P. Il s'agit essentiellement d'instructions pour une machine assez simple. Cette machine est destinée à être une abstraction des machines existantes, il est donc assez facile de la traduire rapidement vers presque n'importe quelle cible spécifique. La facilité de traduction a été importante au début car l'intention initiale était d'interpréter les codes d'octets, un peu comme P-System l'avait fait (et, oui, c'est exactement comme cela que les premières implémentations ont fonctionné).

Forces

Le code d'octet Java est facile à produire pour un frontal de compilateur. Si (par exemple) vous avez un arbre assez typique représentant une expression, il est généralement assez facile de parcourir l'arbre et de générer du code assez directement à partir de ce que vous trouvez à chaque nœud.

Les codes d'octets Java sont assez compacts - dans la plupart des cas, beaucoup plus compacts que le code source ou le code machine pour la plupart des processeurs typiques (et, en particulier pour la plupart des processeurs RISC, tels que le SPARC that Sun a vendu lors de la conception de Java), ce qui était particulièrement important à l'époque, car l'une des principales intentions de Java était de prendre en charge les applets - du code intégré dans des pages Web qui seraient téléchargées avant l'exécution - à une époque où la plupart des gens accédaient au we via des modems via des lignes téléphoniques à environ 28,8 kilobits par seconde (bien que, bien sûr, il y avait encore pas mal de personnes utilisant des modems plus anciens et plus lents).

Faiblesses

La principale faiblesse de Java codes d'octets est qu'ils ne sont pas particulièrement expressifs. Bien qu'ils puissent exprimer les concepts présents dans Java assez bien, ils ne le font pas fonctionnent presque aussi bien pour exprimer des concepts qui ne font pas partie de Java. De même, bien qu'il soit facile d'exécuter des codes d'octets sur la plupart des machines, c'est beaucoup plus difficile à faire d'une manière qui tire pleinement parti d'une machine particulière.

Par exemple, il est assez routinier que si vous voulez vraiment optimiser les codes d'octets Java, vous faites essentiellement de la rétro-ingénierie pour les traduire en arrière à partir d'une représentation semblable à un code machine et les retourner en SSA instructions (ou quelque chose de similaire)2. Vous manipulez ensuite les instructions SSA pour effectuer votre optimisation, puis traduisez à partir de là quelque chose qui cible l'architecture qui vous tient vraiment à cœur. Cependant, même avec ce processus plutôt complexe, certains concepts qui sont étrangers à Java sont suffisamment difficiles à exprimer qu'il est difficile de traduire à partir de certains langages source en code machine qui s'exécute (même de près) de manière optimale sur la plupart des machines typiques.

Sommaire

Si vous demandez pourquoi utiliser des représentations intermédiaires en général, deux facteurs principaux sont:

  1. Réduire un problème O (N * M) en un problème O (N + M), et
  2. Décomposez le problème en morceaux plus faciles à gérer.

Si vous posez des questions sur les détails des codes d'octets Java, et pourquoi ils ont choisi cette représentation particulière au lieu d'une autre, alors je dirais que la réponse revient en grande partie à leur intention initiale et les limites du Web à l'époque, conduisant aux priorités suivantes:

  1. Représentation compacte.
  2. Décodage et exécution rapides et faciles.
  3. Rapide et facile à mettre en œuvre sur la plupart des machines courantes.

Être capable de représenter de nombreuses langues ou de s'exécuter de manière optimale sur une grande variété d'objectifs était des priorités beaucoup plus faibles (si elles étaient considérées comme des priorités).


  1. Alors pourquoi le système P est-il presque oublié? Surtout une situation tarifaire. Le système P s'est vendu assez décemment sur Apple II, Commodore SuperPets, etc. Lorsque le PC IBM est sorti, le système P était un système d'exploitation pris en charge, mais MS-DOS coûtait moins cher (du point de vue de la plupart des gens) , a été essentiellement jeté gratuitement) et a rapidement eu plus de programmes disponibles, car c'est pour cela que Microsoft et IBM (entre autres) ont écrit.
  2. Par exemple, voici comment fonctionne Soot .
8
Jerry Coffin

À l'origine, la JVM était un pur interprète. Et vous obtenez l'interprète le plus performant si la langue que vous interprétez est aussi simple que possible. C'était le but du code d'octet: fournir une entrée interprétable de manière efficace à l'environnement d'exécution. Cette seule décision a placé Java plus près d'un langage compilé que d'un langage interprété, à en juger par ses performances.

Ce n'est que plus tard, lorsqu'il est devenu évident que les performances des JVM d'interprétation étaient toujours nulles, que les gens ont investi l'effort pour créer des compilateurs juste à temps performants. Cela a quelque peu réduit l'écart avec les langages plus rapides comme le C et le C++. (Certains Java problèmes de vitesse inhérents restent cependant, donc vous n'obtiendrez probablement jamais un Java qui fonctionne aussi bien qu'un code C bien écrit.)

Bien sûr, avec les techniques de compilation juste à temps à portée de main, nous pourrions revenir à la distribution effective du code source et à la compilation juste à temps en code machine. Cependant, cela diminuerait considérablement les performances de démarrage jusqu'à ce que toutes les parties pertinentes du code soient compilées. Le code d'octets est toujours une aide importante ici car il est tellement plus simple à analyser que le code équivalent Java.

Le sens est que la compilation du code octet en code machine est plus rapide que l'interprétation de votre code d'origine en code machine juste à temps. Mais nous avons besoin d'interprétations pour rendre notre application multiplateforme, car nous voulons utiliser notre code d'origine sur chaque plate-forme sans modifications et sans aucune préparation (compilations). Ainsi, javac compile d'abord notre source en code octet, puis nous pouvons exécuter ce code octet n'importe où et il sera interprété par Java Machine virtuelle pour coder machine plus rapidement. La réponse: cela fait gagner du temps.

0
Sergey Orlov

En plus des avantages que d'autres personnes ont soulignés, le bytecode est beaucoup plus petit, il est donc plus facile à distribuer et à mettre à jour et prend moins de place dans l'environnement cible. Ceci est particulièrement important dans les environnements fortement limités en espace.

Cela facilite également la protection du code source protégé par des droits d'auteur.