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?
37
Réponses:
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.
la source
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 autres avantages d’une représentation intermédiaire incluent:
la source
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:
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.
la source
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:
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:
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).
la source
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.
la source
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.
la source
À 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.
la source
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:
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".
la source