Blog de Samuel Liard
RSS icon Email icon Home icon
  • Java Virtual Machine Tool Interface

    En lisant l’excellent blog de xebia, j’ai découvert l’interface JVMTI : une API native de profiling. Elle permet non seulement de profiler des applications Java mais aussi de les manipuler directement au niveau du bytecode pendant leur exécution.

    Je travaille justement sur une application sur laquelle j’aimerais bien savoir s’il n’y a pas de goulot d’étranglement à cause d’un bloc synchronized. Donc ni une ni deux, je me lance !

    Pourquoi JVMTI ?

    Après tout il existe pas mal d’outils de profiling Java, à commencer par TPTP intégré dans Eclipse. Le problème avec ces outils c’est qu’ils fonctionnent tous à peu près bien avec une petite application, mais dès que l’on utilise un serveur d’application ou que l’on a un code un peu volumineux… c’est le drame. Au pire l’application ne démarre pas, au mieux les temps de traitement sont multipliés par 1000 et on ne voit plus grand chose. Vu que JVMTI utilise une interface native, je suis persuadé que ce sera beaucoup moins lourd que les autres outils.

    JVMTI remplace depuis la JDK5 les librairies JVMPI (Profiling) et JVMDI (Debug). Celles-ci sont passées deprecated sur la JDK5 et sont disparus en JDK6.

    Ecrire une DLL

    Une librairie native c’est bien, mais ça a un prix : il faut l’écrire en C.

    Et c’est à ce moment que je me suis rendu compte que ça fait un bon moment que je n’ai pas écrit du C. La dernière fois ça doit faire 10 ans pour programmer un 68HC11 à l’IUT. Mais bon, ça doit être comme le vélo, ça ne s’oublie pas. Et bien c’est tout à fait cela, refaire du C c’est comme refaire du vélo après une pose de 10 ans : on va moins vite et ça fait mal :)

    La première étape a été d’exécuter l’exemple de sun sans toucher au code. Premier problème : comment construire une DLL ? Et oui venant du monde Java je n’avais jamais eu besoin de générer une DLL. Je commence donc par télécharger « Microsoft Visual C++ 2008 Express Edition », mais impossible de faire fonctionner le code de SUN. Je n’arrive pas à inclure les .h de JVMTI à mon projet et encore moins à générer une DLL. En cherchant un autre outil je suis tombé sur Dev-C++ et j’ai enfin réussi. Il faut créer un projet DLL en C et modifier deux options du projet. Dans l’onglet Fichiers il faut décocher l’option « Compiler en tant que C++ » sur notre fichier C. Et dans l’onglet Répertoire il faut ajouter les deux répertoires $JDK_HOME/include et $JDK_HOME/include/win32. Après on clique sur compiler et hop ! on a une DLL !

    Dev-C++ est un peu triste au niveau IHM mais il a l’avantage d’avoir été très rapidement pris en main.

    L’agent

    Une fois la barrière de la langue franchie, il faut attaquer le vif du sujet : construire un Agent.

    Le point d’entrée c’est la méthode Agent_OnLoad. C’est dans cette méthode qu’il va falloir indiquer quels événements on souhaite écouter. Cela se passe en trois étapes.

    1.Capabilities

    En fonction des événements que vous souhaitez écouter et des méthodes dont vous avez besoin il faudra activer les bonnes capabilities. Attention, plus vous activerez de Capabilities, plus les performances en seront affectées.

    // Activation des capabilities
    (void)memset(&capa, 0, sizeof(jvmtiCapabilities));
    capa.can_get_current_contended_monitor = 1;
    capa.can_generate_monitor_events = 1;
    capa.can_get_line_numbers = 1;
    error = (*jvmti)->AddCapabilities(jvmti, &capa);

    can_get_current_contended_monitor : Pour écouter les entrées/sorties de block synchronized
    can_generate_monitor_events : Pour activer la génération d’event sur l’activité du moniteur
    can_get_line_numbers : Pour récupérer les numéros de lignes d’une méthode avec GetLineNumberTable().

    2.EventCallbacks

    Ensuite il faut abonner nos méthodes aux événements monitorés.

    // Déclaration des méthodes de callback
    (void)memset(&callbacks, 0, sizeof(callbacks));
    callbacks.VMInit = &callbackVMInit;
    callbacks.VMDeath = &callbackVMDeath;
    callbacks.MonitorContendedEnter = &callbackMonitorContendedEnter;
    callbacks.MonitorContendedEntered = &callbackMonitorContendedEntered;
    error = (*jvmti)->SetEventCallbacks(jvmti, &callbacks, (jint)sizeof(callbacks));

    Dans mon cas je vais écouter les événements de début et de fin de vie de la JVM ainsi que la mise en attente et la libération d’un Thread sur un block synchronized.

    3.EventNotificationMode

    Pour finir on active la génération d’événement

    // Déclaration des événements à écouter
    error = (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_VM_INIT, (jthread)NULL);
    error = (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_VM_DEATH, (jthread)NULL);
    error = (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_MONITOR_CONTENDED_ENTER, (jthread)NULL);
    error = (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_MONITOR_CONTENDED_ENTERED, (jthread)NULL);

    Et voilà ! Maintenant à chaque fois qu’un Thread est mis en attente ma méthode callbackMonitorContendedEnter est appelée et lorsqu’il est libéré c’est callbackMonitorContendedEntered qui est executé.

    OMG WTF memory leak !

    On peut traduire poliment ce titre par : Mon dieu que c’est dur de perdre son garbage collector !

    En effet après avoir mis en place le moniteur, la tache me paraissait simple. Il suffit de stocker dans le contexte du Thread l’heure de mise en attente et au moment où on le libère on peut en déduire le temps d’attente. Mais ça n’a pas été aussi trivial car la gestion de la mémoire m’a posé beaucoup de problèmes.

    Dans JVMTI il y a deux types de gestion de la mémoire. Soit c’est la lib qui gère l’allocation de la mémoire, comme pour la méthode GetMethodName, et dans ce cas il ne faut pas oublier de lui demander de libérer cet espace :

    char *methodName;
    err = (*jvmti)->GetMethodName(jvmti, frames[i].method, &methodName, NULL, NULL);
    (*jvmti)->Deallocate(jvmti, (unsigned)methodName);

    Soit c’est à vous de réserver l’espace mémoire avant l’appel, comme pour GetMethodDeclaringClass.

    jclass* declaring_class_ptr;
    declaring_class_ptr = malloc ( sizeof(jclass));
    err = (*jvmti)->GetMethodDeclaringClass(jvmti, frames[i].method, declaring_class_ptr);
    free(declaring_class_ptr);

    Dans un premier temps je n’avais pas vraiment fait attention à la gestion de la mémoire en pensant naïvement qu’une petite fuite sur une petite application qui n’a pas vocation à s’exécuter longtemps, n’avait pas d’importance. Grave erreur ! Même si mon code fonctionnait sur mon petit exemple à 3 Thread, c’est lorsque je l’ai lancé avec Jonas que j’ai rencontré des problèmes. Mais une fois que l’allocation et la libération de la mémoire a été bien faite, je n’ai plus eu de problème.

    Conclusion

    J’ai fait ma première DLL !! Ca a été un peu laborieux, mais j’ai fini par y arriver. C’est vrai que de devoir programmer en C m’a beaucoup ralenti, mais d’un autre coté si j’étais un développeur C je n’aurais pas besoin de faire du profiling sur une application Java.

    Une autre difficulté venait du manque de documentation. Il y a très peu d’exemple d’utilisation de cette librairie sur le net. En français c’est simple : il n’y en a pas. C’est aussi pour cette raison que je publie cette article à fin d’aider les futurs utilisateurs francophones :)

    Je ne suis pas encore totalement satisfait de mon code car pour le moment le timer fonctionne en secondes. Il ne peut donc détecter que les Thread bloqués au minimum une seconde. Ce n’est pas l’idéal mais ça permet déjà de détecter les plus gros blocages.

    Liens :

    Mon code

    Doc de l’API

    Tutorial de SUN

    Code demo

    Article tutorial

    Article de Xebia sur JVMDI