• Aucun résultat trouvé

Fichiers de bytecode

Dans le document Mémoire de stage de Master 2 (Page 9-0)

1.2 Java virtual Machine (JVM)

2.1.2 Fichiers de bytecode

La structure globale du fichier est décrite dans [GJS+12] : ClassFile {

Les fichiers debytecodeJava contiennent des informations générales sur la classe contenue ainsi que le code des méthodes. Lebytecodeutilise des références pour accéder aux différents éléments. Ces références sont dans leconstant pool.

Voici une classe minimalePersonne : public class Personne {

private String nom;

private String prenom;

private int age;

public Personne(String n, String p, int a) {

nom = n;

prenom = p;

age = a;

}

public void afficher() {

System.out.println("Nom : "+nom+" prenom : "+prenom+" age : "+age);

} }

Après compilation enbytecode, leconstant poolde la classePersonnecontient par exemple : Constant pool:

#1 = Methodref #16.#30 // java/lang/Object."<init>":()V

#2 = Fieldref #15.#31 // Personne.nom:Ljava/lang/String;

#3 = Fieldref #15.#32 // Personne.prenom:Ljava/lang/String;

#4 = Fieldref #15.#33 // Personne.age:I

Il en va de même pour tous les autres éléments (méthodes, noms des variables). Manipuler des entiers est plus efficace que des chaînes de caractères. De plus, ce système permet une résolution paresseuse de certains éléments qui peuvent être chargés uniquement quand c’est nécessaire. C’est pourquoi la JVM utilise un tel système, qui est par ailleurs présent dans une forme proche dans la machine virtuelle .NET.

Après le constant pool le fichier de bytecode contient le code des différentes méthodes.

Voici le code compilé de la méthode main de l’exemple précédent qui permet de tester le programme :

public static void main(java.lang.String[]);

flags: ACC_PUBLIC, ACC_STATIC Code:

stack=5, locals=2, args_size=1 0: new #2 // class Personne 3: dup

4: ldc #3 // String Dupond 6: ldc #4 // String Jean

8: bipush 25

10: invokespecial #5 // Method Personne."<init>":(Ljava/lang/String;Ljava/lang/String;I)V 13: astore_1

14: aload_1

15: invokevirtual #6 // Method Personne.afficher:()V 18: return

Ici une instance dePersonneest créée avec les valeurs suivantes : nom = Dupond, prénom

= Jean, âge = 25. Ce code permet d’observer deux types d’appels de méthode au niveau du bytecode:

invokespecial pour le constructeur de Personne

invokevirtual pour l’appel normal à la méthode afficher 2.1.3 Chargement dynamique

La JVM est caractérisée par le chargement dynamique de classes à l’exécution [LB98].

Celui-ci est décrit par : – Chargement paresseux

– Vérification des types au chargement – Extensible par le programmeur – Multiples espaces de nom

La JVM doit assurer un certain niveau de qualité du code. Pour cela, elle doit vérifier la correction des types. Les types ne doivent pas forcément être toujours corrects mais un mauvais type doit entraîner une erreur. Faire des vérifications de types à l’exécution est très coûteux, c’est pourtant l’approche choisie par SmallTalk, Lisp et Self. Java et la JVM font ces vérifications lors du chargement d’une classe, ceci entraîne un surcoût mais garantit un typage plus sûr. Le composant de la JVM qui gère le chargement des classes à l’exécution est appeléClass Loader. Ce composant prend en entrée un fichier.class et retourne l’objetClass correspondant.

En plus d’être paresseux, le chargement entraîne un test pour s’assurer que le type est définissable et provoque une erreur s’il ne l’est pas. Le type d’une classe est défini par une paire formée par (Nom de la classe, Chargeur). Le chargeur maintient une structure appeléeloaded class cachecontenant les différentes paires. Cela lui permet de ne pas recharger une classe déjà chargée. Il est possible de faire des manipulations avancées avec le chargeur, comme par exemple le redéfinir. Cela peut être utile pour charger des classes depuis un endroit spécifique, ou encore effectuer des vérifications particulières après le chargement.

Néanmoins, pour les classes de bases de Java (celles de la bibliothèque standard) ce n’est pas possible et le chargeur standard sera toujours appelé.

2.1.4 Gestion de la mémoire

La JVM utilise unGarbage Collector (GC, également nommé ramasse-miettes) pour gérer sa mémoire. Il s’agit d’un programme parcourant la mémoire et libérant les objets qui ne sont plus référencés. Cela permet au programmeur de ne pas gérer les aspects mémoires mais entraîne un surcoût à l’exécution.

La machine virtuelle Hotspot utilise un système par générations. Les objets sont dépla-cés dans des segments mémoires correspondant à leur âge, c’est à dire le nombre de cycles de collectage depuis leur création. Ce système est souvent repris par d’autres machines vir-tuelles pour ses bonnes performances. Dans Hotspot, il y a trois segments mémoires diffé-rents [KWM+08] :

– Young generation : objets venant d’être alloués – Old generation : objets alloués depuis longtemps – Permanent generation : structures internes

Ce fonctionnement par générations est appelé ramasse-miettes générationnel. Les objets dans layoung generation viennent d’être alloués, ils sont collectés quand il n’y a plus de réfé-rence sur eux. Si après plusieurs passages du ramasse-miettes les objets de layoung generation sont toujours là, ils sont déplacés en old generation dans lequel ils peuvent être compactés entre eux avec un algorithme dédié.Permanent generation est un segment contenant notam-ment les classes et données statiques.

Dans ces différents segments la gestion de la mémoire est différente et plusieurs algorithmes s’appliquent. Plusieurs autres VM utilisent ce fonctionnement par génération.

De plus, il existe plusieurs ramasses-miettes dans les JVM, par exemple Hotspot en possède trois et effectue un test pour choisir le meilleur au lancement d’un programme. Il existe :

– Serial GC : léger, monoprocesseur

– Parallel GC : bon temps de réponse, multiprocesseurs – Concurrent GC : faible latence sur plusieurs processeurs

La libération de la mémoire fonctionne généralement en trois étapes ( [C+03]) : la pre-mière étape consiste à trouver les références directes sur les objets depuis le programme. Pour gérer ces références sur les objets, unobjectsmapsest construit lors de la compilation. Il doit être maintenu en temps réel. Généralement, quelques bits sont positionnés dans l’entête des objets pour conserver les informations pour le GC. La deuxième étape consiste à trouver les objets atteignables depuis ces références. La troisième étape libère la mémoire, les objets non trouvés dans les deux premières phases sont désalloués, libérant ainsi de l’espace. Ce système par générations est assez courant dans les machines virtuelles, bien que plusieurs autres types deGarbage collector existent. Le parcours de la mémoire est une opération très coûteuse, c’est pourquoi les GC ont un rôle central dans les performances d’une VM.

2.1.5 Optimisations

Lors de la phase de compilation dubytecode Java, il est déjà possible d’optimiser l’exécu-tion future du programme. Une analyse de la hiérarchie de classes (Class Hierarchy Analysis, abrégée en CHA) permet par exemple de détecter certains sites d’appels monomorphes.

Définition 1. Site d’appel monomorphe : pour un site d’appel, une seule méthode est éligible pour l’appel de méthode

Lors de la détection d’un tel site d’appel, il y a deux possibilités d’optimisations : – Si la méthode est courte elle peut être inlinée

– Si la méthode est trop longue, un appel statique peut être fait avec l’instructionbytecode invokespecial.

Dans [PVC01] sont décrites des optimisations pouvant être faites en deux temps : lors de la compilation avec de l’analyse statique et lors de l’exécution avec duprofiling. En effet,

Hotspot interprète et fait de la compilation JIT. Si, lors de la compilation, le site n’est pas monomorphe mais qu’il s’avère l’être lors de l’exécution, il est possible d’effectuer uninlining ou un appel statique.

De plus, lors de cette compilation, des analyses sont faites pour permettre des optimisa-tions, par exemple l’analyse des structures des classes. Certaines JVM utilisent uniquement de l’analyse a priori. Mais la plupart utilisent les deux approches combinées.

L’article [vdB06] détaille quelques optimisations utilisées dans la JVM Hotspot. Cer-taines sont des techniques d’optimisations qui sont présentes également dans la compilation classique. Par exemple les optimisations suivantes relèvent de la compilation classique (des langages pas forcément objets) :

– Inlining : enlever le coût d’un appel de méthode

– Propagation de constantes : remplacement des variables par leurs valeurs – Suppression du code mort : élimination du code jamais exécuté

– Sortir des boucles des calculs récurrents

Peephole optimizations : optimisation de l’ordre des instructions générées

D’autres optimisations ont aussi pour objectifs d’améliorer le nombre des optimisations précédentes. Dans les JVM, la production de code natif n’est pas faite en une seule étape.

Toutes les optimisations ne sont pas faites dans la même phase de production de code.

Des optimisations typiques aux JVM (ou autres systèmes similaires) sont aussi réalisées.

Par exemple, lors d’un appel de méthode, la JVM s’assure que le receveur n’est pas nul.

Mais cette vérification doit être réalisée perpétuellement et dans la mesure du possible elle est supprimée. Entre deux affectations de la valeur NULL, l’objet a normalement une autre valeur, il est donc possible de supprimer tous les tests entre ces deux affectations. Pour des questions de sûreté du code, les JVM doivent aussi tester les bornes des tableaux pour ne pas dépasser.

Certaines JVM adoptent des systèmes hybrides faits d’analyse pré-exécution et de profi-lage pendant l’exécution. C’est la voie suivie par Hotspot et Jikes notamment. Ces systèmes sont qualifiés d’adaptatifs car ils s’adaptent en fonction et au cours de l’exécution du pro-gramme.

C’est par exemple le cas du système adaptatif dans la JVM Jikes. Les travaux de l’équipe sont assez poussés dans ce domaine précis. Le système est rapidement décrit dans la présen-tation de cette machine virtuelle.

Inlining et désoptimisation

Définition 2. Inlining : le code de la méthode appelée est placé à l’endroit de l’appel.

De manière générale, une analyse statique du code permet de détecter un certain nombre de sites monomorphes. Bien entendu, les appels statiques et les classes méthodes d’une classe finale sont éligibles à l’inlining. Il est aussi possible d’inliner les sites provisoirement mo-nomorphes voire inliner un site qui n’est pas monomorphe mais qui se révèle l’être lors de l’exécution.

Ce type de décision entraîne un appel statique ou uninlining. Par contre, il est possible que l’optimisation réalisée rentre en conflit avec le chargement dynamique. En particulier, si un appel statique ou inlining est réalisé (cette méthode est considérée finale donc non-redéfinie), le chargement ultérieur d’une classe peut invalider cette hypothèse. Il faut donc annuler cette optimisation et pouvoir revenir au code précédent. Cette opération est délicate, surtout si elle doit être faite pendant l’exécution de la méthode en question dont l’optimisation devient invalide. C’est possible si cette méthode entraîne le chargement d’une sous-classe qui redéfinie cette même méthode. Ce procédé est appelé désoptimisation.

Dans le cas de Hotspot, l’optimisation est défaite en repassant en mode interprété lors d’une désoptimisation. De manière générale, ce procédé entraîne souvent l’utilisation du On-Stack Replacementdans les JVM. La méthode en cours d’exécution est invalidée, il faut donc modifier les valeurs de retour et son code dans la pile d’exécution. Cela implique d’arrêter l’exécution pendant ce remplacement et de garder en mémoire le code avant l’optimisation.

Ainsi, le code continue à être correct malgré l’optimisation effectuée précédemment.

caller frame

frame of

method to be deoptimized

frame of runtime stub

caller frame interpreter frame for deoptimized method interpreter frame for

inlined method frame of runtime stub

Figure 2 – Schéma duOn-Stack Replacement dans Hotspot d’après [KWM+08]

Il est possible d’éviter d’utiliser ce mécanisme, qui est d’ailleurs assez coûteux, en utili-sant la propriété de pré-existence. C’est ce que proposent les auteurs de [DA99], c’est-à-dire inliner quand on est sûr que cela n’entraînera pas deOn-Stack Replacement. Au delà du coût intrinsèque du mécanisme, maintenir les informations permettant de revenir en arrière sur une optimisation est également très complexe.

2.2 Méta-modèle et définitions

L’article [DP11] propose une sémantique de l’héritage multiple basée sur la méta-modélisation.

Ce modèle permet que chaque nom dans le code du programme ne représente qu’une et une seule instance du méta-modèle. Ce méta-modèle permet une sémantique propre en plus de fournir du vocabulaire efficace pour décrire l’implémentation des langages à objets.

Le méta-modèle proposé possède trois entités : – Classe

– Propriété globale – Propriété locale

Une classe possède un certain nombre de méthodes. Parmi celles-là, il y a des méthodes qui ne sont pas redéfinies et d’autres qui le sont. Une propriété qui n’est pas encore définie est dite introduite par la classe.

Un propriété globale représente un ensemble d’une même méthode. Une même propriété globale regroupera différentes propriétés locales. C’est à dire plusieurs redéfinitions de la même méthode globale.

Une propriété locale appartient à une classe donnée et correspond à une propriété globale.

Ce méta-modèle est illustré dans la figure suivante :

*

Figure3 – Méta-modèle des classes et propriétés d’après [DP11]

Dans le cadre de Java et de ses interfaces, les signatures de méthodes déclarées dans les interface Java sont dites introduites par l’interface. Il s’agit donc de l’introduction d’une propriété. La méthode correspond donc à une propriété globale. L’implémentation concrète de cette méthode dans les sous-classes correspond à une propriété locale dans chaque classe où elle est redéfinie.

Dans les langages à objets, le type dynamique du receveur d’un appel de méthode n’est connu qu’à l’exécution. Il s’agit de sélectionner la propriété locale d’une propriété globale appelée. Cette propriété locale est connue par le type dynamique du receveur.

Les propriétés globales et locales représentent aussi bien des attributs que des méthodes.

Dans le cadre de cette étude nous nous focalisons essentiellement sur les méthodes mais ce méta-modèle est valable pour les attributs.

2.3 Test de sous-typage et sélection de méthode

La programmation par objets introduit la notion de type et donc de sous-typage. En particulier les langages à objets sont très dépendants d’une implémentation efficace de trois mécanismes de base :

– Accès aux attributs – Appel de méthode

– Test de sous-typage

Pour l’appel de méthode, l’adresse de la méthode à exécuter dépend du type dynamique de l’objet receveur (sur lequel s’applique la méthode). La sélection de la bonne méthode, également appelée liaison tardive ou envoi de message est donc un vrai problème. En héritage simple, l’implémentation est grandement facilitée.

Figure 4 – Implémentation de l’héritage simple dans les langages à objets.

Pour construire les tables de méthodes, il suffit de concaténer les méthodes introduites dans chaque classe. Chaque méthode a donc une position unique dans la table de méthodes quel que soit la classe. On considère donc l’invariant de position pour les méthodes. Grâce à cet invariant, la sélection de la méthode est facilement réalisée car il suffit de connaître sa position. En cas de redéfinition dans une sous classe de la méthode, on lui attribue la position de la méthode qu’elle redéfinie.

Pour un langage en héritage multiple, cette position invariante n’existe plus.

Définition 3. L’invariant de position signifie que chaque méthode a une position dans la table de méthodes invariante par spécialisation.

Définition 4. L’invariant de référence signifie que les références à un objet sont indépen-dantes du type statique de la référence.

Figure 5 – Implémentation de l’héritage multiple dans les langages à objets.

Dans le schéma ci-dessus on peut observer un diagramme d’héritage en losange. L’implé-mentation des tables de méthodes par concaténation des méthodes introduites par chaque classe pose problème. En effet, dans D, on ne sait pas si on doit positionner les méthodes de B avant celles de C ou l’inverse. De manière générale les méthodes héritées de B et de C n’auront pas de positions invariantes dans D. Il est possible d’utiliser la coloration pour résoudre ce problème. En laissant des espaces vides dans les tables de méthodes les blocs de méthodes peuvent être à la même position invariante. Ce problème nécessite un mécanisme pour connaître la position des méthodes, qui ne peut plus être déduite simplement comme en héritage simple.

Dans le contexte de Java, ces trois mécanismes cruciaux sont implémentés selon diffé-rentes techniques qui sont perfectibles. En typage statique ces mécanismes sont généralement difficiles à implémenter, et l’efficacité obtenue est inférieure à celle d’un langage fonctionnant en monde fermé. Le problème est également complexifié par l’héritage multiple. En typage statique, le type statique d’un objet est déterminé à la compilation mais son type dynamique n’est connu qu’à l’exécution. Il peut donc être nécessaire de tester si un objet est instance d’une classe. Ou encore, si le type de cet objet est sous-type d’un type donné.

Ce problème est encore complexifié avec le chargement dynamique car l’ensemble de classes grandit à l’exécution. Plusieurs tests de sous-typages et plusieurs mécanismes de sé-lection de méthode existent. Il est possible de mesurer leur efficacité par le nombre de cycles de processeur qu’ils utilisent.

L’objectif est d’avoir un mécanisme qui remplit les critères suivants [Duc08], déjà énoncés précédemment :

1. Temps constant 2. Espace linéaire

3. Compatible avec l’héritage multiple

4. Compatible avec le chargement dynamique 5. Compatible avec l’inlining

Le premier point est important car le test de sous-typage est très souvent utilisé (im-plicitement ou ex(im-plicitement). L’implémentation en temps constant d’un mécanisme souvent utilisé est donc un gage d’efficacité si ce temps est raisonnable. De plus, le temps constant garantit un aspect prédictible dans l’implémentation. L’espace linéaire n’est pas vraiment possible au sens strict du terme étant donné que certaines données des super-classes sont recopiées dans les sous-classes. L’objectif est d’avoir un espace linéaire dans la taille de la re-lation de spécialisation. Certaines implémentations sont quadratiques par rapport au nombre de classes dans le pire des cas, bien que linéaires dans la taille de la relation de spécialisation.

La problématique du test de sous-typage est surtout à explorer en héritage multiple. En sous-typage simple, le test de Cohen [Coh91] est l’implémentation usuelle. Il permet d’avoir un test efficace en temps et espace constant. Par contre, il ne fonctionne pas en héritage multiple et donc en sous-typage multiple. Il s’agit d’une méthode plus efficace qu’une recherche linéaire dans le tableau. Chaque classe se voit attribuer une position fixe (sa profondeur dans la hiérarchie depuis la racine) dans un tableau de super-types. Lors d’un test de sous-typage il suffit de tester si la case à l’indice indiqué contient l’identifiant de la classe pour connaître le résultat du test. Néanmoins ce test n’est pas compatible avec l’héritage multiple.

L’héritage multiple est une nécessité pour la modélisation. [Duc08] cite l’exemple des ontologies qu’il est difficile de modéliser sans héritage multiple. Une forme acceptable demeure néanmoins dans le sous-typage multiple comme Java et C#. Une autre preuve de la nécessité de l’héritage multiple est la rareté de langages à sous-typage simple. Les langages ne possédant pas de sous-typage multiple sont souvent en phase de mutation pour l’acquérir. Ada ne fonctionnait qu’en héritage simple, les interfaces ont été rajoutées dans sa version de 2005.

Le chargement dynamique est devenu assez courant aujourd’hui, l’implémentation du test de sous-typage doit donc en tenir compte. Une manière a priori simple de répondre à ce problème est de proposer une implémentation incrémentale. Une autre approche consistant à tout recalculer a déjà été expérimentée mais dans le pire des cas tout doit être recalculé ce qui est extrêmement coûteux.

Enfin, l’inlining est une optimisation très largement utilisée aujourd’hui. Le test de sous-typage car il est très souvent utilisé, doit contenir peu d’instructions pour pouvoir être inliné.

Dans cette section seront comparées différentes implémentations du test de sous-typage et de

Dans cette section seront comparées différentes implémentations du test de sous-typage et de

Dans le document Mémoire de stage de Master 2 (Page 9-0)

Documents relatifs