Démarrage JVM 8 vs 9
Introduction
En parcourant la mailing liste d’open JDK (core-lib-dev) j’ai vu plusieurs threads de mail à propos d’optimisation de temps de démarrage et d’occupation mémoire d’une JVM « minimale« .
Ce travail a été réalisé en grande partie par Claes Redestad (Oracle) lors du développement de Java 9.
J’ai donc décidé de tester la différence entre un HelloWorld (en version standard et avec utilisation de Lambda) entre Java 8 (update 51 pour ne pas risquer de backport des optimisations en question sur cette version) et Java 9 (ea build 176). Pour réaliser ces tests j’ai utilisé les images docker azul/zulu-openjdk:8u51 et azul/zulu-openjdk:9ea et les deux classes suivantes :
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); } } public class HelloWorldLambda { public static void main(String[] args) { Runnable hello = () -> System.out.println("Hello lambda world"); hello.run(); } }
J’ai ensuite utilisé la commande Linux time pour connaître le temps de traitement et la fonctionnalité de Native Memory Tracking de Java pour connaître la mémoire allouée.
Java 8
time java HelloWorld real 0m0.085s user 0m0.078s sys 0m0.019s time java HelloWorldLambda real 0m0.185s user 0m0.236s sys 0m0.032s
java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary \ -XX:+PrintNMTStatistics -Xmx16m -Xms16m HelloWorld Total: reserved=1356124KB, committed=57156KB java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary \ -XX:+PrintNMTStatistics -Xmx16m -Xms16m HelloWorldLambda Total: reserved=1358869KB, committed=59901KB
Constatation : une JVM ça démarre vite (moins de 100ms) mais ça utilise quand même plus de 57Mo de RAM juste pour un HelloWorld! L’utilisation de Lambda implique un temps de démarrage plus que doublé et un overhead de mémoire de 2Mo.
Hum, 57Mo quand même d’utilisation mémoire pour un simple HelloWorld, je me suis souvenu d’un article fort intéressant sur le sujet écrit par Alexey Shipilev, je me suis replongé dedans pour creuser la question : https://shipilev.net/jvm-anatomy-park/12-native-memory-tracking/ et ai ajouté quelques options au démarrage de ma JVM :
- SerialGC : le garbage collector qui démarre le plus vite et nécessite le moins de mémoire
- Une heap à 16Mo : largement suffisant pour un HelloWorld
- Désactivation du TieredCompilation qui lui aussi nécessite plus de mémoire
java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics \ -Xmx16m -Xms16m -XX:+UseSerialGC -XX:-TieredCompilation HelloWorld Total: reserved=1134567KB, committed=35279KB java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics \ -Xmx16m -Xms16m -XX:+UseSerialGC -XX:-TieredCompilation HelloWorldLambda Total: reserved=1135918KB, committed=36630KB
time java -Xmx16m -Xms16m -XX:+UseSerialGC -XX:-TieredCompilation HelloWorld real 0m0.084s user 0m0.077s sys 0m0.014s time java -Xmx16m -Xms16m -XX:+UseSerialGC -XX:-TieredCompilation HelloWorldLambda real 0m0.172s user 0m0.197s sys 0m0.018s
Constatation : Le temps de démarrage n’est pas significativement réduit dans le cas standard (mais l’est avec lambda) mais l’occupation mémoire l’est grandement. On gagne près de 20Mo grâce ces options de démarrage.
Java 9
time java HelloWorld real 0m0.161s user 0m0.177s sys 0m0.048s time java HelloWorldLambda Hello lambda world real 0m0.199s user 0m0.228s sys 0m0.042s
java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary \ -XX:+PrintNMTStatistics -Xmx16m -Xms16m HelloWorld Total: reserved=1389612KB, committed=97536KB java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary \ -XX:+PrintNMTStatistics -Xmx16m -Xms16m HelloWorldLambda Total: reserved=1390008KB, committed=98188KB
Constatation : En Java 9, malgré le travail d’optimisation du démarrage de la JVM, le temps de démarrage est moins bon hors lambda mais meilleur avec lambda qu’en Java 8. Il reste donc du travail à faire, JPMS (project Jigsaw) est peut-être le fautif ici. Côté mémoire, il y a une forte augmentation.
java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics \ -Xmx16m -Xms16m -XX:+UseSerialGC -XX:-TieredCompilation HelloWorld Total: reserved=1137041KB, committed=38117KB java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics \ -Xmx16m -Xms16m -XX:+UseSerialGC -XX:-TieredCompilation HelloWorldLambda Total: reserved=1137178KB, committed=38510KB
time java -Xmx16m -Xms16m -XX:+UseSerialGC -XX:-TieredCompilation HelloWorld real 0m0.152s user 0m0.143s sys 0m0.022s time java -Xmx16m -Xms16m -XX:+UseSerialGC -XX:-TieredCompilation HelloWorldLambda real 0m0.202s user 0m0.188s sys 0m0.020s
Constatation : En optimisant les options JVM, le temps de démarrage n’évolue pas beaucoup mais l’utilisation mémoire baisse drastiquement et retourne quasiment au même niveau qu’en Java 8 dans la même configuration. Sachant que Java 9 vient avec Le garbage collector G1 par défaut (au lieu du Parallel GC), la consommation excessive doit provenir de celui-ci.
Essayons donc de vérifier en activant le ParallelGC qui était le défaut en Java 8.
java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics \ -Xmx16m -Xms16m -XX:+UseParallelOldGC -XX:-TieredCompilation HelloWorld Total: reserved=1157722KB, committed=58798KB java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics \ -Xmx16m -Xms16m -XX:+UseParallelOldGC -XX:-TieredCompilation HelloWorldLambda Total: reserved=1157860KB, committed=59192KB
Constatation : C’est donc bien l’utilisation de G1 par défaut dans Java 9 qui est la cause de l’augmentation de l’utilisation mémoire plus importante en 9 qu’en 8.
Conclusion
Première constatation : L’idée préconçue que java est lent à démarrer et ne peux être utilisé pour des applications de type ligne de commande est erronée! Une JVM démarre en moins de 100ms!
Deuxième constatation : Malgré tout le travail fait par les ingénieurs d’Oracle Java 9 démarre plus lentement que Java 8 et utilise plus de mémoire. Certain choix entre en contradiction directe avec l’optimisation du démarrage d’une JVM (entre autre, le passage de G1 comme garbage collector par défaut).
Troisième constatation : L’écart entre code utilisant et n’utilisant pas les lambda s’est énormément réduit entre Java 8 et Java 9
Attention : le protocole de test utilisé ici n’était peut-être pas très rigoureux, lancer plusieurs fois un process via la commande time n’est pas la panacée. De plus le développent de Java 9 n’est pas encore terminé, on peut espérer que la version finale comblera en partie ou totalement l’écart de temps de démarrage entre une JVM 8 et 9. A tester donc avec une version finale.