L’exécution de Java

images/bluebelt.png

Nous avons entraperçu le principe de la machine virtuelle.

On a un code binaire portable qui est exécuté par un programme spécifique à la machine (la J.V.M : Java Virtual Machine).

On a une spécification qui décrit le pseudo-code et ce qu’on doit en faire. Maintenant la réalisation d’une application JVM est libre de connaître des réalisations différentes tout en restant dans ce cadre.

Il existe des langages pour lesquels la compilation donne directement un code binaire directement exécutable par une machine donné. Les compilateurs réalisent des optimisations au moment de cette compilation.

Les J.V.M. ont une autre stratégie: certes les compilateurs Java ont quelques optimisations de compile time mais les optimisations les plus critiques sont réalisées à l’exécution (run_time).

Cela a un inconvénient au niveau de performances puisque du temps d’exécution est consacré à ces optimisations mais aussi de gros avantages car les stratégies peuvent alors s’adapter à des circonstances de l’exécution et aussi à des caractéristiques particulières du fonctionnement de bas niveau des processeurs modernes.

Pour votre culture lire: pipeline (architecture des processeurs) et mémoire cache

Les exécuteurs: évolutions de la technologie

schema: évolutions des J.V.M

Dans les premières versions de l’exécuteur Java on avait un interprète spécialisé (dit de threaded code) Bien entendu les performances n'étaient pas extraordinaires.

Dans un version ultérieure on a eu de la "compilation à la volée" (compilateur JIT: Just In Time): ici le pseudo-code une fois chargé était compilé dans le code natif de la machine. On perdait un peut de temps pour espérer en gagner ensuite (ce qui était un pari souvent gagné mais pas toujours).

Les compilateurs dynamiques ont porté cette stratégie vers un niveau de sophistication supplémentaire. Un hot spot interprète un code … si celui-ci "chauffe" (c.a.d apparaît comme devant s’exécuter souvent) il est alors compilé en code natif. Mais ce code natif n’est pas statique: il peut faire l’objet de diverses optimisations stratégiques en fonction de son comportement (il peut même être "désoptimisé" si on s’aperçoit que les décisions d’optimisation rencontrent des circonstances défavorables!)

[Avertissement]

Les comparaisons de performance entre des langages à compilation préalable (comme C, C\++,…) et des exécutions comme celle du hot-spot n’ont pas de sens dans l’absolu.

On peut être surpris de trouver des programmes qui sont bien "chauds" en compilation dynamique s’exécuter plus rapidement que des codes natifs! (voir ci-après la note sur les performances).

La J.V.M: chargement et exécution des codes

schema: chargement

Des codes particuliers appelés ClassLoader sont chargés de rechercher les pseudo-codes demandés par l’application et de les vérifier avant exécution.

Un fichier de pseudo-code qui ne serait pas conforme aux règles serait rejeté!

Au moment de la compilation il a été vérifié la conformité du code courant aux utilisations des autres codes (qui doivent donc être consultables par le compilateur).

Au moment de l’exécution il faut de nouveau être sûr que les codes que l’on va utiliser sont bien présents. Le ClassLoader va donc s’assurer de la présence dans les modules accessibles des codes nécessaires à l’application. Toutefois il existe des cas où il y aura quand même une découverte dynamique de certains codes (ce qui peut s’avérer délicat en cas d’absence des codes demandés!)

(Il est possible d’avoir des fichiers contenant des codes pré-chargés : CDS -Class Data Sharing- ceci pour des serveurs nécessitant un temps de démarrage plus court; mais les conditions d’exécution étant très particulières il vaut mieux lire en détail les notes de version de la JVM)

compile time, load time, run time

images/brownbelt.png

Il faut avoir conscience du comportement des codes lors des différentes phases de vie des pseudo-codes.

On doit considérer plusieurs phases:

Compile_time
Les modifications du code qui sont générées au moment de la compilation.
Link_time
Opérations effectuées lors de la génération de certains formats de livraison des applications (sera expliqué ultérieurement).
Load_time
Les exécutions qui se produisent juste après que le ClassLoader ait chargé le code. On dit que le code est initialisé.
Run_time
Ce qui se passe quand le code est en cours "normal" d’exécution.

Reprenons quelques détails au sujet de ces phases:

Compile-time

Faire attention aux constantes initialisées! (nous avons déjà vu ce point mais il faut le reprendre)

Supposons que dans une classe Valeurs nous ayons un constante comme:

public static final Rapport = 6.28 ;

Et que dans un autre code nous ayons une utilisation de cette constante:

    donnée * Valeurs.Rapport ;

Ici la valeur 6.28 est directement recopiée dans le code de cette classe. C’est certes une optimisation … mais elle a une contrepartie: si vous décidez un jour de changer la valeur de Rapport alors il faudra recompiler tous les codes qui l’utilise (sinon ils continueront à utiliser l’ancienne valeur).

Un autre cas de comportement de compile time est une forme de compilation conditionnelle. Si on a un variable public static final DEBUG=false; le code suivant ne se compilera pas:

   °°°°
   if(DEBUG) {
      // on trace ou on exécute quelque chose
      // CE CODE N'EST PAS COMPILE
      // PAS UNE ERREUR
   }

Par contre il se compilera si le compilateur trouve DEBUG à true.

Load_time

au moment de l’initialisation du code de la classe il y a des codes qui s’exécutent:les initialisations des variables membres static ; et des blocs de code étiquetés static (voir ci-après).

Ces exécutions se font dans l’ordre de définition dans le code de la classe.

class PackCst {
   // Properties est une forme de HashMap
   static final Properties DAOPROPS = new Properties() ;
   // voici un bloc static
   static {
   //ici on va chercher des valeurs dans un fichier
        // et on gère les exceptions qui peuvent se produire
   }
}

(nota: il est même possible d’avoir une variable static final qui n’est initialisée que dans le bloc static)

images/blackbelt.png (3° dan!)

Comme les programmeurs peuvent manipuler eux-mêmes des ClassLoaders des situations étonnantes peuvent se produire: une même classe chargée par deux ClassLoaders différents peut avoir un même constante statique avec des valeurs différentes! On peut même avoir des objets de ces différentes versions de classe qui ne se reconnaissent pas entre eux (par exemple par instanceof).

La J.V.M. : gestion de la mémoire

images/orangebelt.png

schema: gestion de la mémoire

Le "glaneur de mémoire" (Garbage Collector) gère le tas: il désalloue les emplacements de données qui ne sont plus utilisés. Il déplace les données survivantes entre différentes zones.

On ne peut pratiquement pas affecter le comportement du glaneur pendant l’exécution mais des options de lancement permettent de moduler les stratégies.

Pour visualiser le comportement de la gestion du tas (zones "eden", "survivor spaces" 1 et 2, "old generation" et "perm gen") utiliser l’outil jvisualvm et mettre à jour les plugins comme Visual GC.

Les performances

Il est extrèmement difficile de voir un comportement déterministe dans le temps car plusieurs phénomènes interagissent au moment de l’exécution:

  • L’ordonnanceur de tâches du système d’exploitation (mais ça c’est vrai pour toutes les applications)
  • Les actions de l’ordonnanceur au niveau des micro-tâches (Threads) à l’intérieur de la JVM.
  • Les points de contention provoqués par la mise en place de verrous par le programme pour garantir l’intégrité des données (qui contrecarrent les stratégies de l’ordonnanceur)
  • les stratégies d’optimisation des "pipelines" du processeur (avec d'éventuels "défauts" du cache)
  • les stratégies préparatoires du compilateur dynamique de la JVM
  • les opérations de chargement "à chaud" de codes
  • les opérations du glaneur de mémoire

Il est donc recommandé de ne focaliser les recherches de gain de performances que sur les aspects algorithmiques et de ne pas chercher à faire des optimisations de bas niveau.

Il est aussi possible de faire des opérations de réglage de la JVM pour se conformer à une qualité de service souhaitée (par ex. "lissage": minimiser les pauses du glaneur tout en baissant la performance moyenne).

La J.V.M. : gestion de la sécurité

images/bluebelt.png

schema: gestion de la sécurité

L'AccessController met en oeuvre une politique de sécurité :

  • sandbox security policy (comportement par défaut)
  • politique configurée (pour les "code sources" reconnues)