JVMTI – audit des performances Java

L’API JVMTI (Java Virtual Machine Tool Interface) fournit un moyen de créer des agents logiciels. Elle a été intégrée à Java à partir de la version Java 5 et remplace l’API JVMPI (Java Virtual Machine Profiling Interface). L’agent natif (développé par exemple en C) fonctionne dans le même processus et le même espace mémoire que la JVM. C’est pourquoi il est recommandé d’être très vigilant sur le développement de ce type de composant.

Ces agents peuvent être remarquablement intéressants et légers. Ils peuvent notamment vous apporter des éclairages précieux sur des contentions applicatives. Ils peuvent être notamment utilisés lors de tests de charge ce qui n’est pas le cas des ‘profilers’ classiques et peuvent se substituer (dans une certaine mesure) aux solutions APM (Application Performance Management) lorsque aucune solution n’est disponible dans votre environnement de tests.

 Fonctionnement de l’agent JVMTI

Après que la JVM ait chargé la librairie de l’agent, la méthode Agent_OnLoad de l’agent est invoquée par la JVM. Celle-ci permet de configurer les fonctionnalités de l’agent avant que la VM ne soit initialisée. Si l’agent veut être alerté lors de la survenance d’un évènement particulier, il doit configurer la VM pour rendre disponible certaines fonctionnalités (appelées ‘capabilities’). La fonction Agent_onLoad doit donc enregistrer les évènements pour lesquels l’agent veut être notifié, et définir des méthodes « callback » associées à chaque type d’évènement.

 Ci-dessous la liste des indicateurs utilisables pour activer des fonctionnalités.

Par exemple l’utilisation de l’indicateur ‘can_generate_method_entry_events’ permet d’activer la génération d’un évènement au début de l’exécution d’une méthode. L’activation des évènements liés aux contentions s’effectue en positionnant l’attribut can_generate_monitor_events, et l’analyse de la durée d’acquisition d’un moniteur est possible en déclarant des fonctions ‘callback’ pour les évènements MonitorContendedEnter, MonitorContendedEntered qui indiquent respectivement qu’un thread est mis en attente d’un moniteur, et qu’un thread récupère un moniteur après avoir été mis en attente.

can_tag_objects Can set and get tags, as described in the Heap category. 
can_generate_field_modification_events Can set watchpoints on field modification – SetFieldModificationWatch  
can_generate_field_access_events Can set watchpoints on field access – SetFieldAccessWatch  
can_get_bytecodes Can get bytecodes of a method GetBytecodes  
can_get_synthetic_attribute Can test if a field or method is synthetic – IsFieldSynthetic and IsMethodSynthetic  
can_get_owned_monitor_info Can get information about ownership of monitors – GetOwnedMonitorInfo  
can_get_current_contended_monitor Can GetCurrentContendedMonitor  
can_get_monitor_info Can GetObjectMonitorUsage  
can_pop_frame Can pop frames off the stack – PopFrame  
can_redefine_classes Can redefine classes with RedefineClasses. Bytecodes of the original and redefined methods can be different. The constant pool and attributes can also be different.  
can_signal_thread Can send stop or interrupt to threads  
can_get_source_file_name Can get the source file name of a class  
can_get_line_numbers Can get the line number table of a method  
can_get_source_debug_extension Can get the source debug extension of a class  
can_access_local_variables Can set and get local variables  
can_maintain_original_method_order Can return methods in the order they occur in the class file  
can_generate_single_step_events Can get single step events  
can_generate_exception_events Can get exception thrown and exception catch events  
can_generate_frame_pop_events Can set and thus get FramePop events  
can_generate_breakpoint_events Can set and thus get Breakpoint events  
can_suspend Can suspend and resume threads  
can_redefine_any_class RedefineClasses can be called on any class (can_redefine_classes must also be set)  
can_get_current_thread_cpu_time Can get current thread CPU time  
can_get_thread_cpu_time Can get thread CPU time  
can_generate_method_entry_events Can generate method entry events on entering a method  
can_generate_method_exit_events Can generate method exit events on leaving a method  
can_generate_all_class_hook_events Can generate ClassFileLoadHook events for every loaded class.  
can_generate_compiled_method_load_events Can generate events when a method is compiled or unloaded  
can_generate_monitor_events Can generate events on monitor activity  
can_generate_vm_object_alloc_events Can generate events on VM allocation of an object  
can_generate_native_method_bind_events Can generate events when a native method is bound to its implementation  
can_generate_garbage_collection_events Can generate events when garbage collection begins or ends  
can_generate_object_free_events Can generate events when the garbage collector frees an object

La mise en œuvre de cette mécanique n’est pas complètement triviale mais elle est documentée au travers d’exemples et de documentations de référence qui reste un passage incontournable avant de pouvoir se lancer dans le développement d’un agent.

Ci-dessous un exemple de code simplifié de la méthode Agent_OnLoad extrait de l’article The JVM Tool Interface (JVM TI): How VM Agents Work (http://java.sun.com/developer/technicalArticles/J2SE/jvm_ti/). Vous pouvez également trouver des informations très détaillées sur l’api JVMTI à partir de la documentation de référence accessible à l’adresse suivante http://docs.oracle.com/javase/1.5.0/docs/guide/jvmti/jvmti.html

JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
    jvmtiEnv              *jvmti;
    jvmtiError             error;
    jint                   res;
    jvmtiCapabilities      capabilities;
    jvmtiEventCallbacks    callbacks;

    // Create the JVM TI environment (jvmti).
    res = (*vm)->GetEnv(vm, (void **)&jvmti, JVMTI_VERSION_1);
    // If res!=JNI_OK generate an error.
   // Parse the options supplied to this agent on the command line.  
  // If options don’t parse, do you want this to be an error?
  // Clear the capabilities structure and set the ones you need.
    (void)memset(&capabilities,0, sizeof(capabilities));
    capabilities.can_generate_all_class_hook_events  = 1;
    capabilities.can_tag_objects                     = 1;
    capabilities.can_generate_object_free_events     = 1;
    capabilities.can_get_source_file_name            = 1;
    capabilities.can_get_line_numbers                = 1;
    capabilities.can_generate_vm_object_alloc_events = 1; 
    // Request these capabilities for this JVM TI environment.
    error = (*jvmti)->AddCapabilities(jvmti, &capabilities);
    // If error!=JVMTI_ERROR_NONE, your agent may be in trouble. 
    // Clear the callbacks structure and set the ones you want.
    (void)memset(&callbacks,0, sizeof(callbacks));
    callbacks.VMStart           = &cbVMStart;
    callbacks.VMInit            = &cbVMInit;
    callbacks.VMDeath           = &cbVMDeath;
    callbacks.ObjectFree        = &cbObjectFree;
    callbacks.VMObjectAlloc     = &cbVMObjectAlloc;
    callbacks.ClassFileLoadHook = &cbClassFileLoadHook;
    error = (*jvmti)->SetEventCallbacks(jvmti, &callbacks,
                      (jint)sizeof(callbacks));
    //  If error!=JVMTI_ERROR_NONE, the callbacks were not accepted. 
    // For each of the above callbacks, enable this event.
    error = (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
                      JVMTI_EVENT_VM_START, (jthread)NULL);
    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_OBJECT_FREE, (jthread)NULL);
    error = (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
                      JVMTI_EVENT_VM_OBJECT_ALLOC, (jthread)NULL);
    error = (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
                      JVMTI_EVENT_CLASS_FILE_LOAD_HOOK,
                      (jthread)NULL);
    // In all the above calls, check errors. 
    // Create a raw monitor in the agent for critical sections.
    error = (*jvmti)->CreateRawMonitor(jvmti, « agent data »,
                      &(agent_lock));
    // If error!=JVMTI_ERROR_NONE, then you haven’t got a lock!
    return JNI_OK; // Indicates to the VM that the agent loaded OK.

J’attire votre vigilance sur cet exemple car il est fortement conseillé de gérer les codes erreurs renvoyés par les appels de fonctions JVMTI.

Dès que la fonction Agent_onLoad est exécutée, la JVM commence à invoquer les fonctions qui ont été initialisées lors de l’appel de SetEventCallbacks. Par convention dans cet exemple les fonctions ont été préfixées par « cb » mais il peut s’agir de n’importe quel autre nom de fonction défini dans l’agent. La fonction cbClassFileLoadHook sera donc invoquée à chaque fois qu’une classe sera chargée par la JVM. C’est donc un endroit privilégié lors que l’on veut instrumenter le code java à la volée.

Notons que la notification des évènements (fonction SetEventNotificationMode) peut être activée globalement ou pour un thread en particulier.

A l’issue de cette section de code, nous avons donc déclaré les fonctionnalités que nous souhaitons utiliser (« capabilities »), nous avons enregistré des fonctions ‘callback’ appelées par la VM lors de la survenance des évènements, et nous avons défini la portée de la notification (ici tous les threads).

Notez que la gestion de la mémoire peut s’avérer un peu complexe par moment. En effet, certaines fonctions JVMTI allouent de la mémoire qu’il faut désallouer dans le code de l’agent. Par exemple la fonction  GetMethodName [dont la signature est jvmtiError

GetMethodName(jvmtiEnv* env, jmethodID method, char** name_ptr, char** signature_ptr, char** generic_ptr)] permet de récupérer le nom et la signature de la méthode correspondant à l’identifiant transmis en paramètre. Cette fonction alloue des zones de mémoire pointées par les pointeurs name_ptr, signature_ptr, generic_ptr. Ces allocations mémoires doivent être libérées en utilisant la fonction Deallocate [dont la signature est jvmtiError Deallocate(jvmtiEnv* env, unsigned char* mem)].

La fonction JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) est invoquée par la VM lorsque l’agent est déchargé. En général cette fonction est utilisée pour libérer les ressources allouées dans par la fonction Agent_onLoad. 

Gestion de la portée des données

L’API JVMTI fournit également des fonctions permettant de gérer une structure de données données dont la portée est spécifique à un thread. Le prototype de ces fonctions est donné ci-dessous  pour lire et écriture une structure de données représentée par le pointeur data_ptr.

jvmtiError GetThreadLocalStorage(jvmtiEnv* env,jthread thread, void** data_ptr)jvmtiError SetThreadLocalStorage(jvmtiEnv* env, jthread thread,const void* data_ptr)

 Gestion des sections critiques

Les sections critiques du code de l’agent doivent être protégées par un moniteur. La création du moniteur doit être réalisée soit dans la fonction Agent_onLoad soit entre l’émission des évènements VMInit et VMDeath correspondant respectivement à une phase d’initialisation et de fin de fonctionnement de la VM. 

Exemple de création d’un moniteur nommé « agent data ».
jrawMonitorID lock ; // déclaration du moniteur
(*jvmti)->CreateRawMonitor(jvmti, « agent data », &( lock)); 

La fonction RawMonitorEnter() permet d’acquérir un verrou exclusif. Tandis que la fonction RawMonitorExit() permet de libérer ce verrou. Armé de ces deux fonctions il est possible de mettre en place un accès sécurisé aux données globales. 

Déploiement de l’agent

Pour le déploiement de l’agent, il existe deux façons de déclarer un agent dans une ligne de commande java.

-agentlib:<agent-lib-name>=<options>
agent-lib-name représente le nom de l’agent à charger. La recherche de la librairie, ainsi que le nom complet de la librairie sont spécifiques à chaque système. Par exemple si l’option -agentlib:foo=opt1,opt2 est utilisée sur un système windows, la VM essaye de charger la librairie foo.dll à partir des chemins déclarés dans la variable PATH. Par contre sur un système Linux, la VM essaye de charger une librairie libfoo.so à partir des chemins déclarés dans la variable LD_LIBRARY_PATH.

-agentpath:<path-to-agent>=<options>
Dans ce cas le nom complet de la librairie est fourni. Par exemple si l’option -agentpath:c:\myLibs\foo.dll=opt1,opt2 est utilisée, la VM essaye de charger la librairie c:\myLibs\foo.dll 

Les options sont transmises à la fonction Agent_onLoad sous la forme d’une chaîne de caractères à parser. 

Cet article ne couvre pas la mise en place d’une procédure de compilation du code natif.

Sous windows, il est possible d’utiliser par exemple MinGW et de créer un fichier de configuration ‘makefile’. 

Nous reviendrons plus en détail sur la création d’agent JVMTI dans un prochain article. 

A lire également

L’excellent article sur le blog Xebia : http://blog.xebia.fr/2007/11/29/chroniques-de-la-performance-a-propos-de-contentions/

The JVM Tool Interface (JVM TI): How VM Agents Work http://java.sun.com/developer/technicalArticles/J2SE/jvm_ti/

Documentation de référence JVMTI http://docs.oracle.com/javase/1.5.0/docs/guide/jvmti/jvmti.html

http://www.jperf.com

 

Publicités

A propos jlerbsc

founder of JavaPerf Consulting Http://www.jperf.com
Cet article a été publié dans JVMTI, performance. Ajoutez ce permalien à vos favoris.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s