Calinours découvre... la JVM et son bytecode !

(Nhat Minh Lê (rz0) @ 2010-11-14 09:17:10)

Et non, vous ne rêvez pas, avec cet article, l’hérésie s’installe chez Ours & Hippy : on y parle de Java ! La fin est proche…

Pour anticiper sur la libération prochaine du code du framework de programmation par contrats pour Java que j’ai écrit durant mon stage à Google, j’ai décidé de vous parler un peu de mon expérience avec la JVM et le bytecode Java.

C’est un article qui se veut accessible ; si vous êtes un débutant (averti) et ne comprenez pas, c’est que j’ai raté mon coup. :-°

Des machines qui n’existent pas en vrai

La distinction entre langages interprétés et langages compilés est une dichotomie bien connue des langage-trolls amateurs. Et vient alors immanquablement la question qui déjoue les cabales et divise les cultes :

Mais, Java est-il compilé ou interprété ?

Aha ! Ça vous en pose, une colle ! :-° Mais revenons plutôt aux faits : en Java, les fichiers source (.java) sont compilés… en fichiers bytecode (.class). Ces derniers sont similaires aux fichiers objets (.o ou .obj) traditionnellement obtenus par compilation de fichiers source C, excepté qu’ils visent une machine un peu différente… une machine virtuelle !

A priori, le terme « machine virtuelle » n’implique rien de plus que le fait qu’une telle architecture n’a pas été conçue pour être implantée au niveau matériel, directement par un processeur. Cela n’est pas impossible, en théorie, mais ce n’est pas forcément intéressant.

Le plus souvent, une machine virtuelle sera implémentée au niveau logiciel, à l’aide d’un interpréteur exécutant les instructions de celle-ci. L’avantage de cette approche peut se résumer à ceci : on profite ce faisant des plates-formes et services en place. De cela découle entre autre tout ce que l’on entend (trop) souvent sur la portabilité de Java.

De plus, l’évolution de l’architecture est bien plus flexible (changer de JVM logicielle est bien plus aisé que changer de matériel), et cela a sans nulle doute contribué à donner à la JVM son jeu d’instructions particulier, de haut niveau, et taillé sur mesure pour le langage Java, sans grande considération pour une utilisation plus générale.1 En particulier, le modèle objet de Java est omniprésent dans le jeu d’instructions.

1 Je m’étonne ainsi de voir apparaître autant de langages ciblant la JVM, en particulier des langages tels que Clojure, car, de mon expérience personnelle, le modèle adopté n’est pas ce que l’on pourrait appeler propice à un style fonctionnel.

Une machine à piles… ?

Mais avant de jeter un œil aux instructions, intéressons nous un peu à l’architecture en elle-même : dans quel environnement un programme Java évolue-t-il ?

Et bien dans le monde de Java, il était une fois… des piles, des tables de variables locales, un tas d’objets, et des tables de constantes. Le tas et les tables des constantes sont simples à décrire.

Le tas est un énorme dictionnaire associant des références à des objets. Lors d’un new, un nouvel objet prend vie dans le tas et sa référence est retournée au programme ; c’est durant la phase de collecte de la mémoire (garbage collection) qu’elle sera recyclée, si devenue inutile.

Une table de constantes est une structure encore plus primitive ; il y en a une par fichier classe, et on y trouve les unes à la suite des autres les valeurs de toutes les constantes utilisées dans le code de la classe. L’accès se fait simplement en indexant ce gros tableau. Chaque constante non triviale est ainsi remplacée dans le programme par une entrée dans la table qui va bien.

Un peu plus intéressant, les piles. La JVM est une machine à piles, oui, mais que cela signifie-t-il au juste ? Pour bien comprendre la différence, rien ne vaut un petit exemple : une simple addition ; imaginons que l’on veuille ajouter deux nombres a et b.

Sur une machine à registres classique, cela donne ça :

  1. charger a dans un registre r1 ;
  2. charger b dans un registre r2 ;
  3. additionner r1 et r2 et placer le résultat dans r3 ;
  4. faire ce que l’on veut avec r3

Pour une machine à piles, cela ressemble plutôt à ça (ici, on ne considère qu’une seule pile) :

  1. empiler b ;
  2. empiler a ;
  3. remplacer les deux nombres au sommet de la pile par un seul : leur somme ;
  4. faire ce que l’on veut avec le résultat au sommet de la pile…

Dans le premier cas, on a un jeu de registres (qui peuvent avoir un nom, une adresse, etc. selon les modèles), et dans le second, une pile. Dans les deux cas, cela sert à stocker des valeurs à manipuler.

Tout cela est très bien, mais la JVM est une machine à piles, au pluriel ; pourquoi donc a-t-on besoin de plusieurs piles ? Strictement parlant, ce n’est pas une nécessité. C’est un choix de conception : dans la JVM, chaque appel de méthode dispose virtuellement de sa propre pile. C’est une contrainte assez forte, dans le sens où il est ainsi impossible d’écrire des méthodes manipulant la pile de leur appelant librement, mais d’un autre côté, cela signifie également que le programmeur Java ne pourra jamais foutre en l’air son programme (car en Java, cette notion de pile est invisible) en invoquant une méthode « magique » de la JVM.

En cela, la JVM diffère sensiblement du modèle avancé par les langages à piles tels que Forth, ou, plus récemment, Factor.

En plus d’une pile, chaque appel de méthode réserve un tableau de variables locales ; celles-ci sont accessibles directement par leur numéro, mais ne sont pas adressables (par une référence) comme le serait un objet dans le tas.

Petit aperçu du jeu d’instructions

Passons donc aux instructions. Grossièrement, les instructions se divisent en deux groupes :

Dans tous les cas, l’exécution d’une instruction, qu’elle soit de copie ou non, suit typiquement le schéma suivant :

  1. l’instruction consomme un certain nombre de valeurs au sommet de la pile, ce sont les arguments ;
  2. elle fait son travail à proprement parler, en utilisant les arguments fraîchement récupérés ;
  3. elle empile les résultats, s’il y en a.

D’autre part, comme je l’ai signalé plus haut, la JVM dispose d’un grand nombre de codes spécifiquement adaptés au modèle du langage Java. Entre autres, la JVM intègre des notions plus ou moins primitives de variables, objets, classes, interfaces, champs, et méthodes. On y trouve, par exemple, une instruction de copie d’un champ d’un objet du tas vers la pile (getfield), à partir de sa référence.

Pour les curieux (et les masos), une liste complète des instructions de la JVM est disponible sur Wikipédia.

Que retenir ?

En résumé, la JVM est une machine virtuelle à piles, principalement destinée à être implémentée côté logiciel (du moins, c’est mon ressenti), et fortement liée au modèle objet du langage Java.

La conséquence la plus directe est que si vous voulez écrire du bytecode en vous basant sur du Java, ce sera simple et (probablement) sans douleur. En revanche, je suis d’avis que le bytecode Java, de ce que j’en ai vu, n’est pas adapté à d’autres types de langages.