JVM (Java Virtual Machine) - #2

Dans le premier article, nous avons vu les différents moyens que possédait la JVM pour optimiser le bytecode jusqu’à compiler en assembleur certaines méthodes. Dans ce 2ème  article,  nous allons voir les diverses options permettant de rentrer au sein de la JVM et ainsi de vérifier la façon dont la compilation JIT se déroule.

A) Le monitoring de la compilation dynamique

La compilation du bytecode en code assembleur est effectuée sur des portions de méthodes ou sur des méthodes entières si elles sont assez courtes et appelées très souvent.

Cette compilation prend du temps (ce qui peut être tracé) et de la place mémoire (qui peut également être tracé).

Sur la JVM Oracle (et OpenJDK), il est possible d’afficher les temps de compilation et la taille du code compilé qui dépendent du compilateur C1, C2 ou Tiered (C1+C2).

L’option à ajouter au démarrage «  -XX:+CITime »  

Informations du -XX:+CITime avec le compilateur C1 (-client)
Accumulated compiler times (for compiled methods only)
————————————————
Total compilation time   :  0.684 sStandard compilation   :  0.287 s, Average : 0.008On stack replacement   :  0.397 s, Average : 0.011Detailed C1 TimingsSetup time:         0.000 s ( 0.0%)Build IR:           0.392 s (57.6%)

Optimize:            0.006 s ( 0.8%)

Emit LIR:           0.244 s (35.8%)

LIR Gen:           0.111 s (16.3%)

Linear Scan:       0.132 s (19.4%)

LIR Schedule:       0.000 s ( 0.0%)

Code Emission:      0.038 s ( 5.6%)

Code Installation:  0.006 s ( 0.9%)

Instruction Nodes: 217463 nodes

Total compiled bytecodes : 377139 bytes

Standard compilation   : 147744 bytes

On stack replacement   : 229395 bytes

Average compilation speed: 551622 bytes/s

nmethod code size        : 1375616 bytes

nmethod total size       : 4064232 bytes

Informations du -XX:+CITime avec le compilateur C2 (-server)
Accumulated compiler times (for compiled methods only)
————————————————
Total compilation time   :  8.833 sStandard compilation   :  6.453 s, Average : 0.208On stack replacement   :  2.381 s, Average : 0.077Total compiled bytecodes : 175772 bytesStandard compilation   :  98127 bytesOn stack replacement   :  77645 bytes

Average compilation speed:  19898 bytes/s

nmethod code size        : 374432 bytes

nmethod total size       : 1469764 bytes

Pour obtenir des informations sur la compilation JIT, il faut utiliser l’option de la JVM suivante : -XX:+PrintCompilation

A noter que plus la version mineure du JDK augmente, plus les informations tracées sont précises.

Exemple :

36    1       3       java.lang.Object::<init> (1 bytes)37    2       1       java.lang.reflect.Field::getName (5 bytes)37    3       3       java.lang.Number::<init> (5 bytes)39    1       3       java.lang.Object::<init> (1 bytes)   made not entrant40    7       1       java.lang.reflect.Method::getName (5 bytes)41    9     n 0       java.lang.invoke.MethodHandle::linkToStatic(LL)L (native)   (static)91   54  s    3       java.io.ByteArrayInputStream::read (36 bytes)95   62   !   3       java.io.BufferedReader::readLine (304 bytes)188  166 %     3       fib_nc::_c1 @ 106 (157 bytes)

Explications :

  • 1ère colonne = temps en ms depuis le début du lancement de la JVM
  • 2ème colonne = n° d’ordre des méthodes compilées
  • 4ème colonne = que des chiffres de 0 à 4. Correspond au niveau de compilation (0 =native, 1 à 3 = C1, 4 = C2 -server)
  • ! = code contenant des exceptions (qui elles aussi sont compilées)
  • n = native
  • s= synchronized
  • % → compilation de la méthode à la volée ( OSR ). Sur le libellé “188  166 %     3       fib_nc::_c1 @ 106 (157 bytes)”,  le @106 indique l’offset à partir duquel la méthode OSR a été compilée
  • made zombie = méthode désoptimisée (en général en attente d’exceptions éventuelles)
  • made not entrant = compilée une fois mais on ne peut plus lui faire appel (en général en attente d’exceptions éventuelles)
  • uncommon trap = changement intervenu sur la méthode (ex: instrumentation JVMTI).

Il est possible d’avoir des informations aussi sur l’inlining et le temps de compilation de cette méthode précise en ajoutant : -XX:+UnlockDiagnosticVMOptions  -XX:+PrintCompilation2.

448  101       4       sqli.examples.TestHugeMethodv6::tresGrosseMethode17643opcode (17643 bytes)3069  101       4       sqli.examples.TestHugeMethodv6::tresGrosseMethode17643opcode (17643 bytes)   COMPILE SKIPPED: out of nodes before split (retry at different tier)3069  101   time: 2621 inlined: 0 bytes119   71       3       java.util.HashMap::get (23 bytes)119   71   size: 1392(928) time: 0 inlined: 20 bytesPour l’OSR :80697  141 %     3       sqli.examples.TestHugeMethodv6::doMainLoops @ 717 (1422 bytes)80715  141 % size: 180688(104960) time: 17 inlined: 4371 bytes

L’inlining peut procurer un gain de temps de traitement important. Il est utile d’analyser son utilisation par la JVM via les 2 options suivantes : -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining.

Exemple :

@ 1   java.lang.Object::<init> (1 bytes)@ 1   java.lang.Number::<init> (5 bytes)@ 66   java.lang.String::indexOfSupplementary (71 bytes)   too big@ 14   sun.misc.Unsafe::getObjectVolatile (0 bytes)   intrinsic@ 3   java.lang.String::indexOf (70 bytes)   callee is too large@ 15   java.lang.Double::<init> (10 bytes)   inline (hot)

Explications :

  • La 1ère colonne avec le « @ » indique l’offset (en bytecode) de la méthode où on a mis en place l’inlining.
  • Dans l’exemple ci-dessous, avec «@ 3   java.lang.String::indexOf »  puis la ligne en dessous décalée @15, cela veut dire que l’on a fait un « inlining » sur 2 méthodes et qu’elles ont été fusionnées ensemble.
  • inline (hot) : c’est quand une méthode ne dépasse pas les 35 octets de bytecode, elle est directement “inlinée”.
  • intrinsic : remplacement du bytecode par le code natif de la fonction (ex: pour les System.arrayCopy, les String.equals, la plupart des fonctions mathématiques).
  • too big : méthode trop grande pour profiter d’un inlining.
  • callee is too large : la méthode appelée est trop grande pour fusionner les 2 méthodes appelantes / appelés.

Globalement, en JDK 1.8 pour l’option -XX:+PrintInling, les explications sont plus nombreuses et axées sur la non réussite de l’inlining. En JDK 1.7, on indiquait plutôt les méthodes ayant réussies leur inlining et assez peu les cas d’échecs.

Pour les plus curieux, voici l’option pour voir le code assembleur généré :

Le code assembleur généré apparaît grâce aux options suivantes :

-XX:+UnlockDiagnosticVMOptions et -XX:+PrintAssembly -XX:+DebugNonSafepoints

Il faut par contre ajouter une DLL dans le répertoire bin du JDK (hsdis-i386.dll pour un JDK 32 bit).

Exemple de résultat :

0x02495e8a: adc     ,0x3a8() ;*invokevirtual doubleValue; – TestHugeMethod::moyenneMethode@1387 (line 1110)0x02495e90: mov    $0x347e0778,   ;   {oop(« 445 »)}0x02495e95: xchg   %ax,%ax0x02495e97: call   0x0242d700 ; OopMap{ebp=Oop off=4316};*invokestatic valueOf; – TestHugeMethod::moyenneMethode@1396 (line 1110);   {static_call}

Pour les autres JVM, côté IBM, il y a l’option -Xaot:verbose et -Xjit:verbose

Pour Jrockit, il y a l’option -Xverbose ou en plus complet : -Xverbose:opts,gen

B) Les limites de l’optimisation

Nombre d’exécutions d’une boucle avant compilation JIT :

  • au bout de 1500 en mode -client
  • au bout de 10000 en mode -server

Pour changer ce nombre côté Oracle, il faut ajouter l’option suivante au démarrage :

-XX:CompileThreshold=xxx

Pour IBM, c’est -Xjit:count=xxxx et -Xaot:count=xxxx

Taille maximum du code source d’une méthode :

  • 64Ko de code source pour la taille d’une seule méthode (ce chiffre pour une seule méthode peut paraître ridicule mais les compilateurs comme JSC de Rhino peuvent produire un code source java supérieur à 65ko sur une seule méthode selon la taille du fichier javascript à compiler).

Taille maximum en bytecode d’une méthode :

  • 8Ko de bytecode : seuil de la JVM Oracle à ne pas dépasser, sinon il n’y a pas de compilation, on reste en interprété ⇒ contournable par l’option -XX:-DontCompileHugeMethods en Oracle ou passer à IBM Java 7 (qui n’a pas de limite de taille).
  • 4870 octets de bytecode : seuil de la JVM 1.6 d’IBM où toute méthode en dessous de cette taille a un gain significatif (jusqu’à 10 fois) de la vitesse d’exécution.
  • 4Ko de bytecode : seuil de la JVM JRockit en dessous duquel la vitesse est significativement plus élevée.
  • 2Ko de bytecode : seuil en dessous duquel que j’ai constaté sur la JVM 1.7 d’Oracle que toute méthode répond beaucoup plus vite que dans l’intervalle ]2Ko, 8Ko[
  • 1000 octets de bytecode : seuil au-delà duquel il n’y a pas d’inlining direct de la méthode (seulement une partielle) ⇒ Contournable par l’option -XX:InlineSmallCode=xxxx
  • 35 octets de bytecode. Seuil au-delà duquel il n’y a pas de full inlining ⇒ contournable par l’option -XX:MaxInlineSize=xxxx.

C) Un outil minimaliste pour calculer la taille des méthodes d’une classe

Pour déterminer si on dépasse un seuil important 8Ko, 4Ko, 1Ko ou 35 octets de bytecode, on a l’outil de base via javap (du JDK) qui permet de décompiler un fichier .class et de connaître la taille de chaque méthode au prix d’une analyse non intuitive.

Exemple avec javap -c MaClasseXxx.class

public static void loopFib(long);Code:0: lconst_0…16: lstore_217: goto          220: return

Ici, la dernière ligne de cette méthode contient “20”, c’est la taille en bytecode de la méthode.

Voici l’outil minimaliste qui permet d’analyser la taille des méthodes potentiellement lors de l’intégration continue, pour rester concis, cet article ne contient que le squelette de code pour scruter une classe entière en se basant sur l’api ASM pour l’analyse du bytecode.

import org.objectweb.asm.ClassReader;import org.objectweb.asm.MethodVisitor;import org.objectweb.asm.Opcodes;import org.objectweb.asm.Type;import org.objectweb.asm.commons.CodeSizeEvaluator;import org.objectweb.asm.tree.AbstractInsnNode;import org.objectweb.asm.tree.ClassNode;import org.objectweb.asm.tree.MethodNode;import org.objectweb.asm.tree.analysis.Frame;

File maClasse = new File(« MaclasseDeTest.class »);

ClassReader cr = new ClassReader(new FileInputStream(maClasse));

ClassNode cn = new ClassNode();

cr.accept(cn, ClassReader.SKIP_DEBUG);

List<MethodNode> methods = cn.methods;

for (int i = 0; i < methods.size(); ++i) {

MethodNode method = methods.get(i);

if (method.instructions.size() > 0) {

try {

MethodNode m1 = new MethodNode();

m1.access = method.access;

m1.tryCatchBlocks = method.tryCatchBlocks;

CodeSizeEvaluator mv2 = new CodeSizeEvaluator(m1);

method.accept(mv2);

int tailleByteCode = mv2.getMinSize();

// TODO faire quelque chose avec la taille

// trouvée en bytecode de la méthode…

} catch (Exception e) {

log.error(e);

}

}

} // fin du FOR chq méthode

Conclusion

Avec ce squelette de programme, vous allez pouvoir vérifier plus facilement la taille des méthodes en bytecode. Dans le prochain post, nous verrons au travers de 3 types de tests le comportement de la JVM face à la taille des méthodes et des JVM respectives.

Architecte JEE

0 commentaires

votre commentaire

Se joindre à la discussion ?
Vous êtes libre de contribuer !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Inscription newsletter

Ne manquez plus nos derniers articles !