Quelle est l'utilisation de la conversion du code source en bytecode Java?


37

Si l'on a besoin de machines virtuelles Java 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 différentes machines virtuelles Java. Quelle est donc la logique derrière l'introduction du concept de machine virtuelle virtuelle ou de cette étape supplémentaire?


1
Duplication possible de la compilation bytecode vs code machine
gnat le

12
@gnat: En fait, ce n'est pas un doublon. Il s’agit du "code source vs octet", c’est-à-dire de la première transformation. En termes de langage, il s'agit de Javascript contre Java; votre lien serait C ++ versus Java.
MSalters

2
Préférez-vous écrire un interpréteur de code-octet simple pour ces 50 modèles d’appliance auxquels vous ajoutez du codage numérique pour la mise à niveau ou pour 50 compilateurs pour 50 matériels différents. Java a été développé à l'origine pour les appareils et les machines. C'était son point fort. Gardez cela à l’esprit lorsque vous lisez ces réponses car java n’a plus de véritable avantage (en raison de l’inefficacité du processus d’interprétation). C'est juste un modèle que nous continuons d'utiliser.
Le grand canard

1
Vous semblez ne pas comprendre ce qu'est une machine virtuelle est . C'est une machine. Il pourrait être implémenté matériellement avec des compilateurs de code natif (et cela a été le cas pour la machine virtuelle Java). La partie « virtuelle » est ce qui est important ici: vous êtes essentiellement émulant que l' architecture au - dessus d'un autre. Supposons que j'ai écrit un émulateur 8088 pour fonctionner sur x86. Vous n'allez pas porter l'ancien code 8088 sur x86, vous allez simplement l'exécuter sur la plate-forme émulée. La machine virtuelle Java est une machine que vous ciblez comme toute autre machine, à la différence qu’elle s’exécute par-dessus les autres plates-formes.
Jared Smith

7
@TheGreatDuck processus d'interprétation? De nos jours, la plupart des machines virtuelles Java font une compilation juste à temps en code machine. Sans oublier que "interprétation" est un terme assez large de nos jours. Le processeur lui-même "interprète" le code x86 dans son propre microcode interne, et il est utilisé pour améliorer l'efficacité. Les derniers processeurs Intel sont également très bien adaptés aux interprètes (vous trouverez bien sûr des points de repère pour prouver tout ce que vous voulez prouver).
Luaan

Réponses:


79

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

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

L'analyse consiste à lire le code et à le transformer en une arborescence dans la mémoire du compilateur. L'analyse sémantique consiste à analyser cet arbre, à en comprendre le sens et à simplifier toutes les constructions de haut niveau, même les plus basses. Et la génération de code prend l’arbre simplifié et l’écrit dans une sortie plate.

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

Cela rend la tâche que le JIT doit accomplir beaucoup plus simplement, et donc beaucoup plus rapidement à 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.


7
Certaines des premières tentatives de distribution d'applets, telles que SafeTCL, ont effectivement distribué du code source. L'utilisation par Java d'un pseudo-code simple et étroitement spécifié rend la vérification du programme beaucoup plus facile à gérer, et c'était là le problème difficile à résoudre. Des bytecodes tels que p-code étaient déjà connus pour résoudre le problème de la portabilité (et ANDF était probablement en développement à ce moment-là).
Toby Speight le

9
Précisément. Les temps de démarrage de Java sont déjà un problème en raison de l’étape code à> - machine. Exécutez javac sur votre projet (non trivial), puis imaginez que vous exécutiez tout le code Java -> à chaque démarrage.
Paul Draper

24
Cela a un autre avantage énorme: si un jour nous voulons tous passer à un nouveau langage hypothétique, appelons-le "Scala" - nous n'avons besoin que d'écrire un compilateur Scala -> bytecode, plutôt que des dizaines de Scala -> code machine compilateurs. En prime, nous bénéficions gratuitement de toutes les optimisations spécifiques à la plate-forme de la machine virtuelle Java.
BlueRaja - Danny Pflughoeft

8
Certaines choses ne sont toujours pas possibles dans le code octet de la machine virtuelle Java, telles que l'optimisation de l'appel final. Je me souviens que cela compromet grandement un langage fonctionnel compilé sur JVM.
JDługosz

8
@ JDługosz à droite: JVM impose malheureusement un certain nombre de restrictions / idiomes de conception qui, même s'ils peuvent être parfaitement naturels si vous parlez d'un langage impératif, peuvent devenir un obstacle assez artificiel si vous voulez écrire un compilateur pour un langage qui fonctionne fondamentalement différent. Je considère donc que LLVM est une meilleure cible pour ce qui est de la réutilisation future du langage-travail - elle a aussi des limites, mais elles correspondent plus ou moins aux limites que les processeurs actuels (et probablement dans le futur) auront de toute façon.
gauche du

27

Les représentations intermédiaires de différentes sortes sont de plus en plus courantes dans la conception du compilateur / de l'exécution, pour plusieurs raisons.

Dans le cas de Java, la raison numéro un à l'origine était probablement la portabilité : Java avait été largement commercialisé au départ sous le nom "Write Once, Run Anywhere". Bien que vous puissiez y parvenir en distribuant le code source et en utilisant différents compilateurs pour cibler différentes plates-formes, cela présente 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, puisqu'il est plus proche du code exécutable par machine que de la source lisible par l'homme. ça signifie:
    • la compilation peut être lente comparée à l'exécution de bytecode
    • les compilateurs ciblant différentes plates-formes peuvent finir par produire un comportement différent ou ne pas suivre l'évolution de la langue
    • produire un compilateur pour une nouvelle plate-forme est beaucoup plus difficile que de produire un ordinateur virtuel (ou un compilateur code-octet en natif) pour cette plate-forme
  • la distribution du code source n'est pas toujours souhaitable; Le bytecode offre une certaine protection contre l'ingénierie inverse (bien qu'il soit encore assez facile à décompiler, sauf délibérément obfusqué)

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

  • l'optimisation , où les motifs peuvent être repérés dans le bytecode et compilés jusqu'à des équivalents plus rapides, ou même optimisés pour des cas particuliers lors de l'exécution du programme (en utilisant un "JIT", ou "Just In Time", compilateur)
  • interopérabilité entre plusieurs langues d'une même machine virtuelle; cela est devenu populaire avec la JVM (par exemple Scala), et est le but explicite du framework .net

1
Java était également orienté vers les systèmes intégrés. Dans de tels systèmes, le matériel avait plusieurs contraintes de mémoire et de processeur.
Laiv

Les fournisseurs peuvent-ils être développés de manière à compiler d’abord le code source Java en code octet, puis à le compiler en code machine? Cela éliminerait-il la plupart des inconvénients que vous avez mentionnés?
Sher10ck

@ Sher10ck Oui, il est tout à fait possible, autant que je sache, d'écrire un compilateur qui convertit de manière statique le bytecode de la machine virtuelle Java en instructions machine pour une architecture particulière. Mais cela n’aurait de sens que si les performances étaient suffisamment améliorées pour compenser soit l’effort supplémentaire pour le distributeur, soit le temps supplémentaire nécessaire à la première utilisation pour l’utilisateur. Un système embarqué de faible puissance pourrait en bénéficier; un PC moderne qui télécharge et exécute de nombreux programmes serait sans doute mieux avec un JIT bien réglé. Je pense qu'Android va quelque part dans cette direction, mais je ne connais pas les détails.
IMSoP

8

On dirait que vous vous demandez pourquoi nous ne distribuons pas simplement du code source. Permettez-moi de tourner la question: pourquoi ne pas simplement distribuer du code machine?

Clairement, la réponse est que, de par sa conception, Java ne suppose pas qu’il sait quelle est la machine sur laquelle votre code sera exécuté; il peut s'agir d'un ordinateur de bureau, d'un super-ordinateur, d'un téléphone ou de tout ce qui se trouve entre et au-delà. Java laisse de la place au compilateur JVM local pour faire son travail. En plus d'augmenter la portabilité de votre code, cela présente l'avantage de permettre au compilateur de tirer parti des optimisations spécifiques à une machine, si elles existent, ou de produire au moins du code fonctionnel, dans le cas contraire. Des éléments tels que les instructions SSE ou l'accélération matérielle ne peuvent être utilisés que sur les machines qui les prennent en charge.

Vu sous cet angle, le raisonnement en faveur de l'utilisation du code octet par rapport au 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à terminée.
  • Sécurité, car le format de code octet comporte un mécanisme intégré pour la signature des fichiers de distribution (la source pourrait le faire par convention, mais le mécanisme pour ce faire n’est pas intégré comme c’est le cas avec le code octet).

Notez que je ne mentionne pas l'exécution plus rapide. Le code source et le code 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 la maintenance du compilateur JVM pour produire de nouveaux chemins d'exécution à partir de l'ancien code. Cela peut être pour corriger des problèmes de sécurité, ou si de nouvelles optimisations sont découvertes, ou pour tirer parti des nouvelles instructions matérielles. En pratique, il est rare de voir de grands changements de cette façon, car cela peut exposer des bugs, mais c'est possible, et c'est quelque chose qui se produit de manière modeste tout le temps.


8

Il semble y avoir au moins deux questions différentes possibles ici. Il s’agit essentiellement des compilateurs en général, Java étant simplement un exemple du genre. L'autre est plus spécifique à Java les codes d'octet spécifiques qu'il utilise.

Compilateurs en général

Considérons 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 à cette question 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 des compilateurs N * M pour traduire tous ces langages sources en cibles (où une "cible" est quelque chose comme une combinaison d'un processeur et OS).

Si, toutefois, tous ces compilateurs s'accordent sur une représentation intermédiaire commune, nous pouvons avoir N frontaux de compilateur qui traduisent les langues source en représentation intermédiaire et M arrière-plans de compilateur qui traduisent la représentation intermédiaire en un élément approprié pour une cible spécifique.

Segmentation du problème

Mieux encore, il sépare le problème en deux domaines plus ou moins exclusifs. Les personnes qui connaissent / se soucient de la conception, de l'analyse syntaxique et autres choses du langage peuvent se concentrer sur les front-ends du compilateur, tandis que celles qui connaissent les jeux d'instructions, la conception des processeurs, etc., peuvent se concentrer sur le back-end.

Ainsi, par exemple, pour quelque chose comme LLVM, nous avons beaucoup de frontaux pour différentes langues. Nous avons également des interfaces pour de nombreux processeurs. Un mec de la langue peut écrire un nouveau frontal pour sa langue et prendre en charge rapidement de nombreuses cibles. Un responsable du traitement peut écrire un nouveau back-end pour sa cible sans s’occuper de la conception, de l’analyse, etc. du langage.

Séparer les compilateurs en deux parties, avec une représentation intermédiaire pour communiquer entre eux n'est pas original avec Java. C'est une pratique assez courante depuis longtemps (bien avant l'arrivée de Java, en tout cas).

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 longtemps été séparés en parties front-end et back-end, ils ont généralement été distribués en tant que produit unique. Par exemple, si vous avez acheté un compilateur Microsoft C, il possédait en interne un "C1" et un "C2", qui étaient respectivement le front-end et le back-end - mais ce que vous avez acheté était simplement "Microsoft C" qui incluait à la fois morceaux (avec un "pilote de compilateur" qui coordonne les opérations entre les deux). Même si le compilateur a été construit en deux parties, pour un développeur normal utilisant le compilateur, il ne s'agissait que d'une seule chose qui traduisait le code source en code objet, sans rien de visible entre les deux.

Au lieu de cela, Java a distribué le serveur frontal dans le kit de développement Java et le serveur principal dans la machine virtuelle Java. Chaque utilisateur Java disposait d'un back-end du compilateur pour cibler le système qu'il utilisait. Les développeurs Java ont distribué le code dans le format intermédiaire. Ainsi, lorsqu'un utilisateur le chargeait, la machine virtuelle Java faisait le nécessaire pour l'exécuter sur son ordinateur.

Précédents

Notez que ce modèle de distribution n'était pas entièrement nouveau non plus. À titre d’exemple, le système P d’UCSD fonctionnait de la même manière: les frontaux des compilateurs 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 1 .

Java byte-code

Java byte code est assez similaire à P-code. Ce sont essentiellement des instructions pour une machine assez simple. Cette machine est censée être une abstraction des machines existantes, il est donc assez facile de traduire rapidement vers presque n'importe quelle cible spécifique. La facilité de la traduction était importante dès le départ, car l'intention initiale était d'interpréter les codes d'octet, un peu comme l'avait fait P-System (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 du compilateur. Si (par exemple) vous avez un arbre assez typique représentant une expression, il est généralement assez facile de le parcourir et de générer du code assez directement à partir de ce que vous trouvez sur chaque nœud.

Les codes d'octets Java sont assez compacts - dans la plupart des cas, beaucoup plus compact que le code source ou le code machine pour la plupart des processeurs classiques (et en particulier pour la plupart des processeurs RISC, tels que le SPARC vendu par Sun lors de la conception de Java). Cela était particulièrement important à l'époque, car l'un des principaux objectifs de Java était de prendre en charge les applets (code incorporé dans des pages Web qui seraient téléchargées avant exécution), à un moment où la plupart des utilisateurs accédaient au modem via des lignes téléphoniques à environ 28,8. kilobits par seconde (bien que, bien sûr, quelques personnes utilisaient encore des modems plus anciens et plus lents).

Faiblesses

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

Par exemple, il est assez courant que si vous voulez vraiment optimiser les codes d'octets Java, vous devez faire du reverse engineering pour les convertir en arrière à partir d'une représentation de type code machine, puis les transformer en instructions SSA (ou quelque chose de similaire) 2 . Vous manipulez ensuite les instructions SSA pour effectuer votre optimisation, puis vous traduisez à partir de là un élément qui cible l'architecture qui vous tient à cœur. Même avec ce processus assez complexe, cependant, certains concepts étrangers à Java sont suffisamment difficiles à exprimer, de sorte qu'il est difficile de traduire certains langages sources en un code machine qui fonctionne (même presque) de manière optimale sur la plupart des machines classiques.

Sommaire

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

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

Si vous vous interrogez sur les spécificités des codes d'octets Java et sur les raisons pour lesquelles ils ont choisi cette représentation particulière plutôt qu'une autre, je dirais que la réponse revient en grande partie à leur intention initiale et aux limites du Web à l'époque. , menant aux priorités suivantes:

  1. Représentation compacte.
  2. Rapide et facile à décoder et à exécuter.
  3. Rapide et facile à mettre en œuvre sur les machines les plus courantes.

Pouvoir représenter plusieurs langues ou exécuter de manière optimale une grande variété de cibles constituait une priorité beaucoup plus basse (si elles étaient considérées comme des priorités).


  1. Alors, pourquoi le système P est-il le plus souvent oublié? Surtout une situation de prix. Le système P s'est assez bien vendu sur les ordinateurs 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, il était essentiellement gratuit). rapidement eu plus de programmes disponibles, puisque c'est ce que Microsoft et IBM (entre autres) ont écrit pour.
  2. Par exemple, voici comment fonctionne la suie .

Très proche des applets Web: l'intention initiale était de distribuer du code aux appliances (décodeurs, ...), de la même manière que RPC distribue les appels de fonction et que CORBA distribue les objets.
Ninjalj

2
C'est une excellente réponse et un bon aperçu de la façon dont différentes représentations intermédiaires font des compromis différents. :)
IMSoP

@ninjalj: C'était vraiment Oak. Au moment où il s'est transformé en Java, je crois que les idées de décodeurs (et similaires) ont été mises de côté (bien que je sois le premier à admettre qu'il est raisonnable de dire que Oak et Java sont la même chose).
Jerry Coffin

@TobySpeight: Ouais, l'expression convient probablement mieux ici. Merci.
Jerry Coffin

0

Outre les avantages soulignés par d'autres utilisateurs, 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 encombrés.

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


2
Le bytecode Java (et .NET) est si facile à reconstituer en source assez lisible qu'il existe des produits pour modifier les noms et parfois d'autres informations pour rendre cette tâche plus difficile. peut-être mettre sur un bytecode pour les navigateurs Web.
LnxPrgr3

0

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


0

À l’origine, la machine virtuelle Java était un pur interprète . Et vous obtenez l’interprète le plus performant si le langage que vous interprétez est aussi simple que possible. C’était l’objectif du code octet: fournir une entrée pouvant être interprétée efficacement dans l’environnement d’exécution. Cette décision unique plaçait 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, quand il est apparu évident que les performances des machines virtuelles d’interprétation étaient toujours médiocres, que les gens ont-ils investi dans la création de compilateurs juste-à-temps performants. Cela a quelque peu réduit l'écart avec les langages plus rapides comme C et C ++. (Cependant, certains problèmes de vitesse inhérents à Java subsistent, vous ne disposerez donc probablement jamais d'un environnement Java aussi performant que du code C écrit).

Bien sûr, avec les techniques de compilation juste-à-temps à disposition, nous pourrions revenir à la distribution du code source et à la compilation juste-à-temps en code machine. Toutefois, cela réduirait considérablement les performances de démarrage jusqu'à ce que toutes les parties pertinentes du code soient compilées. Le code octet est toujours une aide importante car il est beaucoup plus simple à analyser que le code Java équivalent.


Est-ce que le sous-votant pourrait s’expliquer pourquoi ?
cmaster

-5

Le code source de texte est une structure qui se veut facile à lire et à modifier par un humain.

Le code d'octet est une structure qui se veut facile à lire et à exécuter par une machine.

Étant donné que tout ce que la machine virtuelle Java effectue avec le code est lu et exécuté, le code d'octet est mieux adapté à la consommation par la machine virtuelle.

Je remarque qu'il n'y a pas encore eu d'exemples. Pseudo Exemples idiots:

//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

Bien sûr, le code octet ne concerne pas uniquement les optimisations. Une grande partie de cela consiste à être capable d'exécuter du code sans avoir à se soucier de règles compliquées, comme de vérifier si la classe contient un membre appelé "foo" quelque part plus bas dans le fichier quand une méthode fait référence à "foo".


2
Ces "exemples" de code octet sont lisibles par l'homme. Ce n'est pas du tout un code octet. Ceci est trompeur et ne répond pas non plus à la question posée.
Wildcard

@Wildcard Vous avez peut-être manqué que ce soit un forum, lu par des humains. C'est pourquoi je mets le contenu sous une forme lisible par l'homme. Étant donné que le forum concerne le génie logiciel, demander aux lecteurs de comprendre le concept de simple abstraction ne demande pas grand chose.
Peter

La forme lisible par l'homme est un code source, pas un code octet. Vous illustrez le code source avec des expressions pré-calculées, PAS de code octet. Et je n'ai pas manqué qu'il s'agisse d'un forum lisible par l'homme: vous êtes celui qui a critiqué les autres répondants pour ne pas inclure d'exemples de code octet, pas moi. Donc, vous dites: "Je remarque qu'il n'y a pas encore eu d'exemples", puis vous donnez des exemples non- exemples qui n'illustrent pas du tout le code octet. Et cela ne règle toujours pas la question du tout. Relisez la question.
Wildcard
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.