Accueil Nos publications Blog Soirée 3T – Les mains dans le bytecode

Soirée 3T – Les mains dans le bytecode

« Write once, run everywhere ». Si ce slogan ne vous dit rien et que vous êtes développeur Java, deux options sont possibles. Soit vous êtes trop jeune soit vous ne vous êtes jamais vraiment penché sur le fonctionnement de la JVM.

Quelle que soit votre situation pas de panique, Yohan Beschi peut vous aider. Le traitement est assez simple et commence par une dose de soirée 3T dans les locaux de Soat. Prenant le contrepied des soirées précédentes Yohan a déroulé une présentation orientée bas-niveau pendant laquelle les mécanismes régissant la JVM et les principes de génération du bytecode furent mis en avant.

La vocation de cet article ne sera pas de vous relater le contenu complet de la présentation de Yohan. Je passerai d’ailleurs volontairement sous silence certains aspects comme l’organisation de l’espace de données ou le pool de constantes. Je ne peux donc que vous conseiller fortement de compléter votre lecture par le visionnage de la vidéo de la soirée ci-dessous.

Ceci étant dit, plongeons-nous un peu plus dans les tréfonds de la JVM en commençant par le dit bytecode.

Le byte-quoi ?

Le bytecode est tout simplement la forme que prennent nos classes Java une fois compilées. Il s’agit d’un format intermédiaire entre le code source et le code machine qui est réellement exécuté par la suite.

Mais pourquoi s’encombrer d’un intermédiaire me direz vous ? Pour deux raisons principalement :

  • Pour fournir une abstraction de la couche matérielle et ainsi permettre interopérabilité du langage comme énoncé en début d’article.
  • Pour disposer d’une base plus facilement interprétable et ainsi ouvrir la porte à des mécanismes comme l’optimisation du coche, la mise en cache et la compilation à l’exécution (compilation Just In Time ou JIT)

Le deuxième point est surtout lié au fait que le bytecode s’exprime sous une forme condensée notamment grâce à l’utilisation de mnémoniques qui, dans le cas du bytecode, sont les représentations textuelles de nombres appelés code d’opération.

En produisant du code intermédiaire plus proche du code machine la JVM adopte le fonctionnement inverse d’un émulateur. En effet les émulateurs ont pour but de reproduire le plus exactement possible la couche matérielle d’une plateforme alors que le bytecode permet au contraire de s’en abstraire.

Une fois la définition du bytecode établie une bonne partie de la présentation de Yohan fut ensuite consacrée à présenter les types et opérations supportées par la JVM ainsi que les différentes mnémoniques établies.

(NDA : Les caractéristiques de la JVM présentées ci-après sont celles de la version 1.4 pour des raisons de simplicité, les autres versions introduisant un nombre non négligeable de nouvelles fonctionnalités)

Parlez-vous le bytecode ?

Durant toute la durée de la présentation Yohan a utilisé le langage assembleur de la JVM, le virtual machine assembly language. Cette forme de bytecode compréhensible par des humains est obtenue grâce à l’outil javap fourni avec le JDK.

Un exemple d’utilisation de javap :

$ javap <path_to_your_class_file>/my_file.class

Une fois le bytecode traduit on constate que les types et méthodes supportées par la JVM sont globalement assez réduits. Malgré l’utilisation des nombreuses mnémoniques (plus de 200) ces dernières ne sont souvent qu’une même opération déclinée pour chacun des types supportés en Java.

Préfixes représentant les types
Préfixe Type Java
i int
l long
s short
b byte
c char
f float
d double
z boolean
a reference

Comme vous le constatez ces préfixes sont assez intuitifs à l’exception du type boolean mais ceci s’explique par la nécessité d’éviter toute confusion avec le type byte.

Vous remarquerez aussi la présence du type reference pour la gestion des références vers la heap et qui jouera par la suite un rôle important dans l’utilisation des méthodes portées par les classes.

Les autres mnémoniques décrites ensuite furent celles des opérations comme par exemple l’addition.

Addition de deux variables
Code d’opération Mnémonique Fonction
96 (0x60) iadd addition de 2 int
97 (0x61) ladd addition de 2 long
98 (0x62) fadd addition de 2 float
99 (0x63) dadd addition de 2 double

Les mnémoniques sont facilement visibles ici et prennent la forme d’une instruction préfixée par le type sur laquelle elle opère.

Maintenant que nous avons appris à lire du bytecode nous allons essayé de l’exécuter.

Vérification et exécution du bytecode

Le bytecode et le fichier .class dans lequel il est contenu sont soumis à des contraintes supplémentaires avant de pouvoir s’exécuter au sein de la JVM.

Un fichier .class est tout d’abord un flux d’octets. Ce flux est stocké au format big-endian toujours dans le but d’assurer la compatibilité du bytecode sur toutes les JVM et s’affranchir encore une fois de la couche matérielle qui peut utiliser un autre format (comme le little-endian). Toujours dans cette optique d’indépendance l’encodage du flux se fait en UTF-8 modifié. Le choix de l’UTF-8 modifié impose néanmoins d’être vigilant dans la manipulation de flux de données externes (fichier, réseau, …) dont l’encodage peut différer.

Le fait de présenter les fichiers .class sous forme de flux est intéressant car Java peut, grâce à des classes comme java.io.DataInputStream et java.io.DataOutputStream, lire ou générer de tels flux. On peut donc tout à fait imaginer du code Java ayant pour but la création d’un fichier .class directement prêt à être envoyé à la JVM. Néanmoins, le fait de savoir manipuler un tel flux ne garantit pas que la JVM acceptera de l’exécuter tel quel.

Un fichier .class se doit en effet de respecter un arrangement précis décrit dans la pseudo-structure ci-après :

ClassFile {
  u4 magic;
  u2 minor_version;
  u2 major_version;
  u2 constant_pool_count;
  cp_info constant_pool[constant_pool_count-1];
  u2 access_flags;
  u2 this_class;
  u2 super_class;
  u2 interfaces_count;
  u2 interfaces[interfaces_count];
  u2 fields_count;
  field_info fields[fields_count];
  u2 methods_count;
  method_info methods[methods_count];
  u2 attributes_count;
  attribute_info attributes[attributes_count];
}

Sans rentrer dans les détails, on remarque par exemple que le fichier .class contient la version du format à laquelle il doit se conformer (champs minor_version et major_version) et que ces informations occupent un espace déterminé de 2 octets (le type u2 désignant des entiers non signés de 2 octects de long).

Le format des fichiers .class a en effet évolué au fur et à mesure des versions de la JVM et ces évolutions imposent de vérifier la cohérence entre le fichier et la JVM sur laquelle on tente de le lancer. Bien évidemment d’autres vérifications existent mais l’idée générale est de comprendre qu’un fichier .class bien formé se doit de respecter les conventions fixées dans la spécification de la JVM.

Et après ?

Jusqu’ici connaître le fonctionnement de la JVM et les principes de génération du bytecode ne semblent avoir qu’un intérêt purement théorique. En effet, vous n’allez pas subitement mieux coder après avoir lu cette introduction. Mais pourtant…

Si on élargi un peu le champ d’horizon il apparaît que ces mécanismes de base sont les fondations de technologies qui font actuellement le « buzz » dans notre métier. A commencer par les langages alternatifs tels que Scala, Groovy, Ceylon ou Kotlin. Ces derniers utilisent en effet la JVM car leurs compilateurs respectifs produisent du bytecode à son intention. Dans la même lignée la JSR 292 portant sur le support des langages dynamiques au sein de la JVM a introduit (et continuera d’introduire à plus ou moins long terme) son lot de modifications dans la façon dont le bytecode est construit et exécuté. Et pour s’en convaincre on citera les réalisations de Charles Nutter (lead developper de JRuby) ou encore les conférences de Rémi Forax sur le sujet.

Tout ceci pour dire que connaître les mécanismes et principes de base de la JVM n’est pas forcément un si mauvais investissement étant donné le nombre de problématiques gravitant aujourd’hui autour de ce sujet. Alors n’hésitez plus, précipitez vous sur la vidéo de Yohan et restez à l’écoute du blog Soat pour une série d’articles sur les dessous du Java.

Pour aller plus loin

Vous pouvez visionner la vidéo sur Developpez.com :

video java bytecode yohan beschi

The Java® Virtual Machine Specification, Java SE 7 Edition,
Tim Lindholm, Frank Yellin, Gilad Bracha & Alex Buckley,
2013-02-28,
https://docs.oracle.com/javase/specs/jvms/se7/html/index.html

JSR 292: Supporting Dynamically Typed Languages on the JavaTM Platform,
https://jcp.org/en/jsr/detail?id=292

javap – The Java Class File Disassembler
Oracle,
https://docs.oracle.com/javase/1.5.0/docs/tooldocs/windows/javap.html

Java bytecode,
Wikipedia,
révision du 08/09/2013,
https://en.wikipedia.org/wiki/Java_bytecode

Invoke Dynamic Them All,
Rémi Forax,
Devoxx Fr 2012,
https://www.parleys.com/play/5148922a0364bc17fc56c7a7

The bright dynamic future of Java,
Rémi Forax,
Paris JUG 2012,
https://www.parleys.com/play/518c1a30e4b05d58d9dfe87c/chapter0/about