Neodante (Julien CHABLE) - Version 1.11 - 13/05/2004ATTENTION : Article de niveau avancé - nécessite une connaissance générale de Java
Le binaire Java
Lorsque vous compilez votre code Java à l'aide de javac par exemple, le code résultant de cette opération est appelé byte-code.Cecode compilé peut-être exécuté par n'importe quelle JVM répondantauxspécifications de SUN, de plus le format de ce code binaire estindépendant du matériel et de la plateforme sur laquelle il estexécuté. Le code peut-être, mais pas nécessairement, contenu dans unfichier (avec généralement l'extension .class). Le format de fichier class définit précisemment la représentation d'une classe ou d'une interface Java dans un fichier de ce type.
Révision et nouveautés
Les types
Lelangage de programmation Java, tout comme la machine virtuelle, opèresur 2 sortes de type : les types dit 'primitif' et les typesdit'référence'. De ce fait, une variable peut contenir ces 2 sortes detypes.
Une référence peut-être de plusieurs types : de type classe, de type tableau, et de type interface.
Parmi les types supportés par la JVM, nous pouvons distinguer les types numériques, les booléen et les types de retour d'adresse (returnAddress).
Les numériques
Les types numériques sont représentés par les types dit intégral et les types dit à virgule flottante. Les types intégrals sont :
- byte, valeur entière sur 8 bits signée (complément à 2) de -128 à 127 inclus.
- short, valeur entière sur 16 bits signée (complément à 2) de -32768 à 32767 inclus.
- int, valeur entière sur 32 bits signée (complément à 2) de -2147483648 à 2147483647 inclus.
- long, valeur entière sur 64 bits signée (complément à 2) de -9223372036854775808 à 9223372036854775807 inclus.
- char, valeur entière sur 16 bits non signée représentant un caractère Unicode de 0 à 65535 inclus.
Les types à virgule flottante sont:
- float, valeur à virgule flottante sur 32 bits à simple précision IEEE
- long, valeur à virgule flottante sur 64 bits à double précision IEEE
Le booléen
Le type booléen :
- boolean, encode une valeur de vérité true (vrai) ou false (faux)
Le retour d'adresse
Le type de returnAddress est un pointeur vers un des opcodes de la JVM (utilisé par les instructions jsr, ret, et jsr_w). Le type returnAddress n'est pas directement associé au langage de programmation Java.
Zone de données au runtime
LaJVM définit de nombreuses zones de données pendant le runtime,qui sontutilisées pendant l'exécution d'un programme. Certaines zones sontcréées au démarrage de la JVM et sont détruites seulement lorsquecelle-ci s'arrête. D'autres zones de données sont utilisées pourchaqueThread, elles sont créées lorsque le Thread est créé, etdétruites à ladestruction du Thread.
Afin de mieux comprendre lemécanisme d'exécution des byte-codes, nous allons voir rapidement cesdifférents espaces de stockage dedonnées.
Le registre pc
Lamachine virtuelle supporte plusieurs threads à la fois pendantl'exécution d'un programme. Chaque thread de la machine virtuelleJavapossède son propre pc (program counter). Nous n'entrerons pas plusdansles détails car le sujet dépasse l'objet de cet article.
La pile de la JVM
Chaque thread de la JVM possède une pile privée, créée en même temps que la Thread. Une pile de JVM stocke des frames (cf la partie sur les frames).
Le tas
Lamachine virtuelle Java possède un tas commun à toutes les threadsde lamachine virtuelle. La tas est l'espace mémoire depuis lequel toutes lesinstances de classes et les tableaux sont alloués.Le tas est créé audémarrage de la machine virtuelle Java.
Le stockage d'objet dans le tas est géré par un système de gestion de stockage automatique (connu sous le nom de garbage collector), de ce fait les objets ne sont jamais désalloués explicitement.
Zone de méthodes
LaJVM possède une zone de méthodes qui est partagée parmi tous lesthreads de celle-ci. Cette zone stocke les structures par classe.Chaquestructure comprend la constant pool, les champs, les donnéesdes méthodes, et le code pour les méthodes et les constructeurs,incluant les méthodes spéciales utilisées pour l'initialisation desclasses et des instances.
La constant pool du runtime
La constant pool est une représentation par classe ou par interface de la table constant_pool d'un fichier class (nousreviendrons ultérieurement dessus). Elle contient plusieurs sortes deconstantes, allant du litéral numérique connu à la compilation,jusqu'aux références de méthodes ou de champs qui doivent être résoluespendant le runtime.
Une constant pool est allouée dans la zone des méthodes, pour chaque classe ou interface créée par la machine virtuelle Java.
La pile des méthodes natives
Nous ne nous attarderons pas sur cette zone de données, le sujet dépassant l'objectif de cet article.
Les frames
Uneframe est utilisée pour stocker des données et des résultats partiels,tout comme pour effectuer les liaisons dynamiques (dynamic linking),retourner les valeurs des méthodes, et dispatcher les exceptions.
Unenouvelle frame est créée chaque fois qu'une méthode est invoquée, etest détruite lorsque l'invocation se termine. Une frame est allouée surla pile de la thread qui a créée cette frame. Ainsi chaque framepossède son propre tableau de variables locales, sa propre piled'opérandes, et une référence vers la constant pool de la classe de laméthode courante.
Seulement une frame, la frame de la méthode qui s'exécute, est active à un moment donné. La frame est identifiée comme la frame courante, et sa méthode est identifiée comme étant la méthode courante. La classe dans laquelle la méthode courante est définie s'appelle la classe courante.Lorsque l'on parlera d'opérations sur les variables locales et la piledes opérandes, ces opérations s'effectueront toujours sur ceux de laframe courante.
Une frame cesse d'être courante si sa méthodeinvoque une autre méthode ou si la méthode se termine (normalement oupas ->exception). Quand une méthode est invoquée, une nouvelle frameest créée et devient courante. Lors du retour d'une méthode, la framecourante passe le résultat de son invocation de méthode (la valeurderetour de la méthode), à la frame précédente, si elle existe. Laframe courante est alors détruite et la frame précédente redevient laframe courante.
Les variables locales
Chaque frame contient un tableau de variables connu sous le nom de variables locales.Unevariable locale simple peut contenir un type boolean, byte, char,short, int, float, reference, ou returnAddress. Une paire de variableslocales (soit 2 variables simples) peut contenir un type long ou double.
Lesvariables locales sont adressées par indexation. L'index de la premièrevariable local est 0. L'intervalle d'un index est donc [0;taille dutableau des variables locales - 1]. Une valeur de type long ou de type double occupe 2 variables locales consécutives.
La JVM utilise les variables locales pour passer des paramètres lors de l'invocation de méthode.
Lors de l'invocation d'une méthode de classe (méthode statique, cf Définitionsenfin d'article), tous les paramètres sont passés dans des variableslocales consécutives en partant de la variable locale d'index 0.
Lors de l'invocation d'une méthode d'instance (cf Définitions enfin d'article), la variable locale d'index 0 est toujours utilisée pourpasser une référence à l'objet à partir duquel cette instance deméthode a été invoquée (this en langage de programmation Java).
Tous les autres paramètres sont passés dans les variables locales consécutives en partant de l'index 1.
La pile des opérandes
Chaque frame contient une pile (Last In First Out) connue sous le nom de pile des opérandes.La pile des opérandes est vide quand la frame est créée. La JVM fournitdes instructions pour charger des constantes ou des valeurs, depuis lesvariables locales ou les champs, sur la pile des opérandes. D'autresinstructions prennent des opérandes de la pile des opérandes,effectuent une opération sur celles-ci, et remettent le résultat sur lapile des opérandes. Comme nous l'avons vu juste au-dessus avec lesvariables locales, la pile des opérandes est aussi utilisée pourpréparer les paramètres à passer aux méthodes, et pour recevoir lesrésultats de celles-ci.
Par exemple, l'instruction iadd additionne 2 valeurs de type int.Elle requiert que les valeurs int devant être additionnées, soit lesdeux valeurs au sommet de la pile des opérandes. Elles auront étéplacées ici par de précédentes instructions (action sur la pile ditde'push', par exemple l'instruction iload). Les deux valeurssont alors retirées de la pile (action dit de 'pop'), puisadditionnées, et leur somme est placée au sommet de la pile desopérandes (de nouveau un 'push').
Pile début -> instruction -> Pile fin
..., val1, val2 -> xadd -> ..., result
Préparation aux instructions de la JVM
Une instruction de la machine virtuelle Java consiste en un opcode sur1 byte (1 octet) spécifiant l'opération à effectuer, suivie par zéro ouplusieurs opérandes servant d'arguments ou de données qui serontutilisés par l'opération. Plusieurs instructions n'ont pas d'opérandeet consistent donc simplement en un opcode.
Voici un pseudo-code de l'exécution du byte-code :
do {
fetch an opcode;
if (operands) fetch operands;
execute the action for the opcode;
} while (there is more to do);
Le nombre, le type et la taille des opérandes sont déterminés par l'opcode.
Les types de la JVM
Laplupart des instructions de la JVM 'encodent' les informations de typeà propos de l'opération qu'elles effectuent dans leur nom. Par exemple,l'instruction iload charge le contenu d'une variable locale, qui doit être un int, au dessus de la pile des opérandes. L'instruction floadfait de même pour une value de type float. Plusieurs instructionspeuvent avoir la même fonction, mais des opcodes différents;parexemple, c'est le cas de iload, fload, dload, ..., tous chargent une variable locale sur la pile des opérandes.
Decette façon, pour la majorité des instructions typées, le type del'instruction est représenté explicitement dans le nom de l'opcode parune lettre : i pour une opération sur un int, l pour une opération sur un long, s pour une opération sur un short, b pour une opération sur un byte, c pour une opération pour un char, f pour une opération pour un float, d pour une opération sur un double, et enfin a pour une opération sur une référence.
Quelquesinstructions pour lesquelles le type n'est pas ambigue (c'est à direqu'un seul type est autorisé, par exemple les opcodes putfield, jsr, ...) n'ont pas de lettre spécifique dans leur nom.
Lalongueur de 1 byte (1 octet -> 8 bits) d'un opcode empêche d'avoirun jeu d'instructions supérieur à 256 opcodes. Par conséquent ce faiblenombre d'instructions réduit le nombre de types supportés pourcertaines opérations (on ne va pas trouver toutes les instructions dujeu d'instructions pour chaque type de données). En d'autres termes, lejeu n'est pas orthogonal, des instructions supplémentaires peuvent êtreutilisées pour convertir les types de données non supportés en types dedonnées supportés. Cela peut réduire les performances dans les codes depersonnes non initiées, mais cela est nécessaire pour garder un jeud'instructions et des fichiers class compacts.
Le tableausuivant résume le support des types des instructions de la JVM. Uneinstruction spécifique, avec l'information de type, est construite enremplaçant le T dans la colonne des opcodes par la lettre du type de lacolonne. Si la colonne de type pour certains modèles d'instructions estblanche, cela signifie qu'il n'existe pas de support pour ce typeconcernant cette opération. Par exemple, il y a une instruction dechargement (Tload) pour le type int (iload), mais paspour le type byte.
La plupart des opérations ne supportent pas les types boolean, byte, char, et short. Par conséquent, les valeurs de ces types, sont implicitement converties en type int à la compilation ou à l'exécution, et ensuite exécutées par des instructions de type int. Regardez les spécifications pour plus de détails.
| opcode | byte | short | int | long | float | double | char | reference |
| Tipush | bipush | sipush | | | | | | |
| Tconst | | | iconst | lconst | fconst | dconst | | aconst |
| Tload | | | iload | lload | fload | dload | | aload |
| Tstore | | | istore | lstore | fstore | dstore | | astore |
| Tinc | | | iinc | | | | | |
| Taload | baload | saload | iaload | laload | faload | daload | caload | aaload |
| Tastore | bastore | sastore | iastore | lastore | fastore | dastore | castore | aastore |
| Tadd | | | iadd | ladd | fadd | dadd | | |
| Tsub | | | isub | lsub | fsub | dsub | | |
| Tmul | | | imul | lmul | fmul | dmul | | |
| Tdiv | | | idiv | ldiv | fdiv | ddiv | | |
| Trem | | | irem | lrem | frem | drem | | |
| Tneg | | | ineg | lneg | fneg | dneg | | |
| Tshl | | | ishl | lshl | | | | |
| Tshr | | | ishr | lshr | | | | |
| Tushr | | | iushr | lushr | | | | |
| Tand | | | iand | land | | | | |
| Tor | | | ior | lor | | | | |
| Txor | | | ixor | lxor | | | | |
| i2T | i2b | i2s | | i2l | i2f | i2d | | |
| l2T | | | l2i | | l2f | l2d | | |
| f2T | | | f2i | f2l | | f2d | | |
| d2T | | | d2i | d2l | d2f | | | |
| Tcmp | | | | lcmp | | | | |
| Tcmpl | | | | | fcmpl | dcmpl | | |
| Tcmpg | | | | | fcmpg | dcmpg | | |
| if_TcmpOP | | | if_icmpOP | | | | | if_acmpOP |
| Treturn | | | ireturn | lreturn | freturn | dreturn | | areturn |
Certaines instructions de la JVM comme pop et swapopèrent sur la pile des opérandes sans faire attention à leur type ;cependant, de telles instructions sont contraintes d'être utiliséesavec des valeurs d'une certaine catégorie, donnée dans letableauci-dessous :
| Type | Type calculé | Categorie |
boolean | int | 1 |
byte | int | 1 |
char | int | 1 |
short | int | 1 |
int | int | 1 |
float | float | 1 |
reference | reference | 1 |
returnAddress | returnAddress | 1 |
long | long | 2 |
double | double | 2 |
Par exemple, les instructions pop et dup sont utilisées pour les types de la catégorie 1, et les instructions pop2 et dup2sont utilisées pour les types de la catégorie 2. On comprendra aisémentque les types ayant une taille de 64 bits (8 octets) appartiennent à lacatégorie 2.
Charger et stocker des instructions
Lesinstructions de chargement et de stockage transfèrent les valeurs entreles variables locales et la pile d'opérandes d'une frame de la JVM :
- Charge une variable locale sur la pile des opérandes : iload, iload_N, lload, lload_N, fload, fload_N, dload, dload_N, aload, aload_N.
- Stocke une valeur depuis la pile des opérandes vers une variable locale : istore, istore_N, lstore, lstore_N, fstore, fstore_N, dstore, dstore_N, astore, astore_N.
- Charge une constante sur la pile des opérandes : bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_m1, iconst_N, lconst_N, fconst_N, dconst_N.
- Avoiraccèsà plus de variables locales en utilisant un index plus grand, oua uneopérande immédiate plus large (par exemple, avoir un long au lieu de unint) : wide.
Les instructions qui accèdent auxchamps des objects et aux éléments des tableaux, transfèrent aussi desdonnées depuis et vers la pile desopérandes.
Le format des instructions avec des lettres génériques N (par exemple, iload_N)dénote des familles d'instructions (avec les membres iload_0, iload_1,iload_2 et iload_3 dans le cas de iload_N). De telles famillesd'instructions sont des spécialisations d'une instruction générique(iload) qui ne prend qu'un paramètre. Pour les instructions génériques,l'opérande est implicite et l'instruction iload_0 signifie la même chose que iload.
Les instructions arithmétiques
Lesinstructions arithmétiques calculent un résultat, ce sont typiquementdes fonctions qui prennent 2 valeurs sur la pile des opérandes, et quimettent le résultat au-dessus de cette même pile une fois l'opérationeffectuée. Il existe 2 types principaux d'instructions arithmétiques :ceux opérant sur des valeurs entières et ceux opérant sur des valeurs àvirgule flottante. Comme nous l'avons vu un peu plus haut, il n'existepas de support direct pour une arithmétique entière sur les types byte, short, char, et boolean; ces opérations sont gérées par les instructions opérant sur le type int. Les instructions arithmétiques sont les suivantes :
- Addition : iadd, ladd, fadd, dadd.
- Soustraction : isub, lsub, fsub, dsub.
- Multiplication : imul, lmul, fmul, dmul.
- Division : idiv, ldiv, fdiv, ddiv.
- Reste (modulo) : irem, lrem, frem, drem.
- Négation : ineg, lneg, fneg, dneg.
- Shift (décalage de bit) : ishl, ishr, iushr, lshr, lshl, lushr.
- OR (bit à bit) : ior, lor.
- AND (bit à bit) : iand, land.
- XOR (bit à bit) : ixor, lxor.
- Incrémentation : iinc.
- Comparaison : dcmpg, dcmpl, fcmpg, fcmpl, lcmp.
Lesinstructions entière et à virgule flottante diffèrent aussi dans leurcomportement pour ce qui est de l'overflow et la division parzéro. Lesopérations sur les types entiers ne renvoient pas d'overflow; la seuleerreur pour ces opérations (idiv, ldiv, irem, lrem) est la division parzéro qui lance un ArithmeticException (cf le paragraphesur lesexceptions en fin d'article).
Les comparaisons sur les valeurs de type long (lcmp) effectue une comparaison signée. Les comparaisons sur les types à virgule flottante (dcmpg, dcmpl, fcmpg, fcmpl) sont effectuées en utilisant la comparaison IEEE 754.
Instructions de conversion de types
Lesinstructions de conversion de types permettent la conversion entre lesdifférents types numériques de la JVM. Celles-ci peuvent être utiliséespour implémenter une conversion explicite. La conversion d'une valeurd'un type vers un autre type possédant un intervalle de valeurs pluspetit, nécessite une conversion explicite. Par exemple la conversiond'un long vers un int.
Voici les différentes conversions réalisables (implicites et explicites) par la JVM :
La JVM supporte directement les conversions d'élargissement d'intervalle suivantes :
- int vers long, float ou double
- long vers float ou double
- float vers double
Les instructions de conversion numerique d'élargissement sont i2l, i2f, i2d, l2d, et f2d. La signification de ces opcodes est relativement simple à comprendre. Par exemple, l'instruction i2f convertie une valeur de type int vers une valeur de type float.
Anoter que la conversion numérique d'élargissement est automatique pourle langage de programmation Java (le programmeur n'a pas besoin de lefaire explicitement). Egalement, la conversion n'existe pas pour lestypes byte, char, et short vers le type int. Car comme spécifié plushaut, les valeurs de type byte, char, et short sont élargiesintrinsèquement vers le type int, par une conversion implicite.(Rappel: la conversion d'un type numérique vers un booléen n'est pasautorisé en Java.)
La JVM supporte également les conversions numériques de rétrécissement d'intervalles :
- int vers byte, short ou char
- long vers int
- float vers int ou long
- double vers int, long ou float
Les instructions de conversion numérique de 'rétrécissement' sont i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l, et d2f.Les règles de conversion sont celles du langage de programmationJava.Une conversion de ce type peut entraîner un résultat ayant unsigne différent de la valeur initiale, et/ou une perte de précision,mais cela reste pour des cas particuliers. Veuillez vous reporter aux spécifications pour de plus amples informations sur les erreurs de conversion.
Manipulation et création d'objet
Bienque les instances de classe et les tableaux soient des objets, la JVMcrée et manipule les instances de classe et les tableaux de façondistincte avec un jeu d'instructions propre à chacun :
- Création d'une nouvelle instance de classe : new.
- Création d'un nouveau tableau : newarray, anewarray, multianewarray.
- Accèsauxchamps d'une classe (champs statiques, aussi appelés variablesdeclasse) et les champs d'instance de classe (champs non statiques,aussi appelés variables d'instance) : getfield, putfield, getstatic, putstatic
- Chargement d'un élément d'un tableau sur la pile des opérandes : baload, caload, saload, iaload, laload, faload, daload, aaload.
- Stocker une valeur depuis la pile des opérandes en tant qu'élement de tableau : bastore, castore, sastore, iastore, lastore, faastore, dastore, aastore.
- Obtenir la longueur d'un tableau : arraylength
- Vérifier les propriétés d'instances de classe ou de tableaux : instanceof, checkcast.
Les instructions de gestion de la pile des opérandes
Un certain nombre d'instructions est fourni pour la manipulation directe de la pile des opérandes : pop, pop2, dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2, swap.
Les instructions de controle
Lesinstructions de branchements conditionnels ou inconditionnelspermettent à la JVM de continuer de s'exécuter avec une instructiondifférente de celle suivant l'instruction de branchement. Lesinstructions de branchement sont :
- Branchement conditionnel : ifeq,iflt, ifle, ifne,ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne,if_icmplt,if_icmpgt, if_icmple, if_icmpge, if_acmpeq, if_acmpne.
- Branchement conditionnel composé : tableswitch, lookupswitch.
- Branchement inconditionnel : goto, goto_w, jsr, jsr_w, ret.
Toutes ces comparaisons sont effectuées en tenant compte du signe.
Instructions d'invocation de méthode et retour
Les instructions suivantes invoquent une méthode :
- invokevirtualinvoque une méthode d'une instance d'un objet, en appelant la bonneméthode virtuelle de l'objet. Ceci est le comportement normal dulangage de programmation Java.
- invokeinterface invoqueune méthode qui est implémentée par une interface, en cherchant lesméthodes implémentées par cet objet pour trouver la méthode appropriée.
- invokespecialinvoque une instance qui requiert un traitement special, c'est à direune méthode d'initialisation d'instance, une méthode privée, ou uneméthode de la super classe.
- invokestatic invoque une méthode de classe (statique) dans la classe nommée.
Les instructions de retour de méthodes, qui sont distinguables par le type de retour, sont ireturn (utilisée pour retourner des valeurs de type boolean, byte, char, short, ou int), lreturn, freturn, dreturn, et areturn. De plus, l'instruction returnest utilisée pour retourner depuis des méthodes déclarées void, desméthodes d'initialisation d'instance, et des méthodes d'initialisationde classe ou d'interface.
Lancement d'exception
Une exception est lancée 'programmaticalement' en utilisant l'instruction athrown.Les exceptions peuvent aussi être lancées par d'autres instructions dela JVM lorsqu'une condition inhabituelle est détectée (par exemple, ladivision par zéro).
Définitions :
Méthode d'instance :une sous-routine ou une fonction appartenant à l'objet courant. Lesméthodes font toujours parties d'une classe en Java, vous ne pouvez pasavoir de méthodes seules comme le permet le C ou le C++. Une méthoded'instance a accès à toutes les variables de l'instance, à toutes lesautres méthodes d'instances, et aux méthodes et variables statiques.
Méthode de classe :une méthode statique dans uneclasse. Elle n'a pas accès aux variablesd'instance, seulement auxvariables statiques de cette classe. De plus,elle ne peut pas invoquerde méthode d'instance (non statique) à moinsqu'elle ne possède uneréférence vers cet objet.