P
'
t
i
t
e
C
h
a
t
t
e
 
spacer~ I DON'T SUFFER FROM INSANITY, I ENJOY EVERY MINUTE OF IT Articles | Connexion
 
~Au coeur de la machine virtuelle avec JVMPI

Précédent  
  Java  
  Suivant
 Présentation

Les développeurs ont souvent pour habitude d'utiliser des outils de surveillance, des "profilers", pour vérifier les performances de leur travail. Java propose une solution simple et élégante pour vous permettre de réaliser votre propre outil.
 Sommaire


 Introduction

Les admirateurs de Java le répètent à l'envi, cette plate-forme de développement possède de nombreux avantages. Nous vous épargnerons la liste de leurs arguments que vous connaissez sans doute très bien. Il en est pourtant un qu'aucun évangéliste ne mettra en avant. Java possède en effet l'indéniable qualité de permettre au programmeur de blâmer aisément la machine virtuelle pour les piètres performances de sa production. Souvent infondée, cette excuse témoigne d'un orgueil démesuré voire d'une certaine fainéantise.

Dès les versions 1.1 du JDK, Sun Microsystems avait mis à notre disposition un outil qui offrait une analyser intéressante de l'exécution des applications. Il suffisait pour cela d'exécuter la JVM avec l'option -prof. Au grand dam des utilisateurs, cet outil rudimentaire souffrait de graves limitations qui le rendaient inutile pour l'analyse d'applications de taille conséquente. Java2 a apporté une solution à ce problème par l'entremise de JVMPI, la Java Virtual Machine Profiling Interface.

Présentée sous la forme d'une API native, cette technologie permet l'écriture d'agents de surveillance de la machine virtuelle. Lorsque cette dernière démarre, un agent se branche dessus et reçoit un certain nombre d'événements de sa part. Ceux-ci peuvent être ou non interceptés, laissant ainsi à l'agent la possibilité d'effectuer un certain nombre d'analyse sur les actions entreprises par la JVM et donc par le programme Java en cours d'exécution. JVMPI définit une interface bidirectionnelle car elle permet aux agents de contrôler la machine virtuelle pour, par exemple, forcer un cycle du ramasse-miettes. Relativement simple à mettre en ouvre, ce système ne prévoit rien concernant l'interface utilisateur. L'auteur de l'agent pourra ainsi préférer un simple journal texte ou encore une interface graphique Swing. Les JDK contiennent tous un exemple d'agent JVMPI appelé hprof. La commande suivante vous permet d'analyser l'exécution de l'application d'exemple que nous utiliserons pour créer notre propre agent :

java -Xrunhprof -classpath jai_codec.jar:jype.jar org.jext.jype.Jype
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Après fermeture du programme vous trouverez le fichier java.hprof.txt dans le répertoire courant, recélant de nombreuses informations pertinentes sur son activité. Comme vous pouvez le constater, un agent est branché sur la machine virtuelle par l'intermédiaire de l'option -Xrun. Le nom spécifié ensuite permet au système de déterminer le nom de la bibliothèque dynamique à charger. Sous Linux l'agent hrpof réside dans libhprof.so tandis que sous Windows le code se trouve dans la DLL hprof.dll. Ces fichiers doivent se trouver dans des répertoires accessibles par la JVM comme le répertoire d'exécution courant ou encore dans l'un des chemins définis dans LD_LIBRARY_PATH. Bien que l'on puisse facilement trouver sur le marché de nombreux agents JVMPI, gratuits, payants, libres ou non, il s'avère parfois très intéressant de créer son propre agent. Vous pourrez même en détourner l'usage de manière assez inattendue. Nous allons découvrir comment à travers l'exemple de l'agent TIDE.


 L'agent TIDE

L'agent JVMPI TIDE compte le nombre d'instances créées pour chaque classe utilisée durant l'exécution d'une application Java. Il comptabilise en outre le temps total passé dans chaque méthode invoquée. Toutes les informations ainsi réunies sont écrites dans des fichiers CSV à la fin du programme. Ce format permet au développeur d'importer les données dans un tableur, comme celui d'OpenOffice.org, afin de les interpréter plus aisément. Nous avons retenu le C++ comme langage de développement afin de profiter de la STL qui fournit certaines structures de données très pratiques pour l'archivage des informations.

Un agent JVMPI se compose de deux fonctions vitales gérant respectivement l'initialisation et les événements système. La première, propre à l'API de Sun, s'intitule JVM_OnLoad tandis que la seconde doit être définie par le programmeur. Le listing 1 présente l'interface C++ minimale de notre agent. L'inclusion de l'en-tête jni.h a son importance car JVMPI réside en grande partie sur JNI. Nous vous recommandons de vous familiariser avec cette API avant de continuer (voir Login: 111 et 112). Lors de l'initialisation, nous devons récupérer un pointeur vers l'interface JVMPI contenu dans la JVM avant d'installer notre gestionnaire d'événements ainsi que le montre le listing 2. Si vous préférez programmer en C, l'appel vm->GetEnv(.) deviendra vm->GetEnv(vm, .). Ceci est valable de manière générale pour les toutes les fonctions JNI.

#ifndef TIDE_MAIN_H_
#define TIDE_MAIN_H_
#include <jni.h>
#include <jvmpi.h>

extern "C" JNIEXPORT jint JNICALL JVM_OnLoad(JavaVM*, char*, void*);
void notifyEvent(JVMPI_Event*);

#endif
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
JNIEXPORT jint JNICALL JVM_OnLoad(JavaVM* vm, char* options, void* reserved)
{
  int res = vm->GetEnv((void **) &jvmpi_interface, JVMPI_VERSION_1);
  if (res < 0)
    return JNI_ERR;

  jvmpi_interface->NotifyEvent = notifyEvent;
  return JNI_OK;
}
       
      
JextCopier dans Jext
L'interface JVMPI à laquelle nous obtenons l'accès dépend du numéro de version spécifié lors de l'appel GetEnv(). Toutes les versions de Java2 acceptent la version 1 de cette technologie. Vous pourrez également utiliser les versions 1.1, introduite dans le JDK 1.2.2, et 1.2, présente à partir du JDK 1.4.2. Les différences ne concernent toutefois que des cas très particuliers qui justifieront rarement l'abandon de la compatibilité avec les Java2 SDK plus anciens. Vous pouvez ensuite compiler l'agent et le brancher sur la machine virtuelle :

% g++ -I. -I$(JDK_HOME)/include -I$(JDK_HOME)/include/linux -shared TIDE.cpp -o libTIDE.so
% java -XrunTIDE application
       
      
JextCopier dans Jext
Dans l'état actuel des choses l'agent ne peut rien faire car tous les événements systèmes sont bloqués par défaut. Nous devons donc les activer comme dans le listing 3 qui se préoccupe de ceux concernant l'allocation des objets, le chargement des classes et l'extinction de la JVM. Vous pourrez bien évidemment réaliser cette opération à n'importe quel moment, laissant ainsi à l'utilisateur la possibilité de contrôler le fonctionnement de l'agent depuis l'interface graphique.

jvmpi_interface->EnableEvent(JVMPI_EVENT_JVM_SHUT_DOWN, NULL);
jvmpi_interface->EnableEvent(JVMPI_EVENT_OBJECT_ALLOC, NULL);
jvmpi_interface->EnableEvent(JVMPI_EVENT_CLASS_LOAD, NULL);
       
      
JextCopier dans Jext
A chaque événement émis, notre gestionnaire notifyEvent() reçoit une structure de type JVMPI_Event contenant le type de l'événement et son environnement. Ce dernier joue un rôle crucial pour une gestion correcte des threads ainsi que nous le verrons plus tard. Le listing 4 présente la gestion des événements que nous avons activés dans JVM_OnLoad(). Quand un objet est créé, nous ne recevons que son identificateur unique et celui de la classe dont il est l'instance. Nous devons donc impérativement conserver une liste des classes chargées en mémoire pour faire correspondre chaque nouvel objet à un nom lisible par l'utilisateur.

void notifyEvent(JVMPI_Event *event)
{
  switch(event->event_type)
  {
    case JVMPI_EVENT_CLASS_LOAD:
      classLoadHandler(event->u.class_load.class_id,
        event->u.class_load.class_name);
      break;

    case JVMPI_EVENT_OBJECT_ALLOC:
      objectAllocHandler(event->u.obj_alloc.class_id);
      break;

    case JVMPI_EVENT_JVM_SHUT_DOWN:
      dumpClassTypesMap();
      break;
}
       
      
JextCopier dans Jext
Le rôle de la fonction classLoadHandler() consiste simplement à placer dans un dictionnaire à clé, une map STL, la paire (identificateur de la classe, nom complet). Ces deux valeurs sont lues dans la structure event :

event->u.class_load.class_id
event->u.class_load.class_name
       
      
JextCopier dans Jext
Celle-ci contient une union de toutes les structures relatives aux événements. Vous devrez vous reporter à la documentation officielle de JVMPI pour obtenir la liste complète de ces structures et de leurs attributs. Maintenant que nous sommes certains de posséder le nom de chacune des classes chargées par l'application surveillée, nous pouvons nous préoccuper du dénombrement des instances. Un objet est défini par son identificateur, celui de sa classe, sa taille et sa qualité de tableau ou non. Nous n'avons besoin ici que du seul identificateur de classe. Une fois de plus, nous utilisons un dictionnaire à clé pour archiver les paires (identificateur de classe, nombre d'instances). Le véritable travail de notre agent a lieu lorsque la JVM termine l'exécution du programme qui nous est signalée par l'événement JVM_SHUT_DOWN. Nous créons alors le fichier CSV dont le premier champ désigne le nom de la classe et le second son nombre total d'instances. Le listing 5 contient le code de la fonction prévue à cet effet.

void dumpClassTypesMap()
{
  fstream fout("tide-alloc.csv", ios::out);
  map<jobjectID, int>::const_iterator it;
  long total = 0;
  for (it = objectAllocations.begin(); it != objectAllocations.end(); it++)
  {
    total += it->second;
    fout << (classTypes[it->first]).c_str() << "," << it->second << endl;
  }
  fout.close();
}
       
      
JextCopier dans Jext

 Gestion des threads et suivi des appels de méthodes

Compter le nombre d'instances de chaque classe s'avère relativement simple. Nous allons maintenant implanter la seconde fonction de notre programme, le chronométrage des invocations de méthodes. Pour cela JVMPI nous propose deux événements indispensables intitulés METHOD_ENTRY2 et METHOD_EXIT. Il nous suffit donc, en théorie, de déclencher notre chronomètre lors de la réception du premier événement et de l'arrêter pour le second. Nous commettrions une belle erreur en retenant une solution si simple, aussi séduisante soit-elle. Nous devons absolument prendre en compte les threads. Imaginez le cas de deux threads T1 et T2. T1 commence par exécuter la méthode M(), notre chronomètre démarre. Le thread est alors préempté et le système donne la main à T2 qui invoque lui aussi M(). Le chronomètre redémarre alors, faussant tous nos calculs. Pour remédier à ce problème nous devons nous intéresser aux environnements des événements. La structure JVM_Event contient en effet un pointeur vers un JNIEnv qui définit de manière unique un thread de l'application. Il serait donc envisageable de maintenir une liste des méthodes en cours d'exécution en les classant par threads. JVMPI propose toutefois une solution plus simple et bien plus efficace, l'espace de stockage local.

Cet espace désigne un pointeur non identifié maintenu par la JVM pour chaque thread en cours d'exécution. Par le biais de l'interface JVMPI nous pouvons lire et écrire dans cet espace. De fait, nous pourrons conserver nos chronomètres de manière sûre. Il convient toutefois de bien récupérer les valeurs calculées lorsqu'un thread meurt. L'activation des événements THREAD_START et THREAD_END est donc nécessaire. Chaque naissance d'un thread entraînera la création d'un dictionnaire à clé contenant les paires (identificateur de méthode, structure). La structure utilisée se trouve dans le listing 6 et contient le temps total passé dans la méthode, son nombre d'invocations et son chronomètre. Dans ce même listing se trouve la variable globale methods qui recevra les données de chaque thread et que nous exploiterons pour écrire le fichier CSV.

typedef struct
{
  unsigned long time;
  int count;
  jlong start;
} invokedMethod;
typedef map<jmethodID, invokedMethod*> MethodMap;
static MethodMap methods;
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Voici ce qu'il se passe lors du démarrage d'un thread :

MethodMap* threadInvokes = new MethodMap;
jvmpi_interface->SetThreadLocalStorage(id, threadInvokes);
       
      
JextCopier dans Jext
L'écriture dans l'espace de stockage local s'effectue grâce à SetThreadLocalStorage() tandis que la lecture nécessite l'utilisation de GetThreadLocalStorage() ainsi qu'en témoigne le listing 8 qui présente la gestion de l'événement METHOD_ENTRY2. Cet extrait de code présente un cas particulier très intéressant au sujet du thread principal de l'application. Une lecture dans l'espace de stockage local renvoie la valeur NULL si rien ne s'y trouve. Or notre système garantit la présence d'une MethodMap pour chaque thread. Curieusement aucun événement THREAD_START n'est émis pour le thread principal bien qu'un événement THREAD_END soit bien reçu. Nous considérons donc que l'absence de données dans l'espace local caractérise ce thread. Nous pouvons alors utiliser de manière sûre la map globale appelée methods.

Le chronométrage des méthodes présente un aspect très intéressant de JVMPI, dont l'usage peut être détourné. L'API contient en effet une fonction GetCurrentThreadCpuTime() qui renvoie le nombre de nano-secondes écoulées en temps processeur pour le thread courant de l'application Java. Ceci nous permet de compter de manière sûre le temps exact passé dans les méthodes. Notre implantation se content pour sa part de conserver les durées en millisecondes. Cela signifie malheureusement que si une méthode invoquée 462 fois, nécessitant moins d'une milliseconde de temps processeur à chaque appel, sera comptée comme n'ayant consommé que 0ms. Ce choix se justifie aisément dans le cas d'applications destinées à tourner durant de longues périodes. Notre système compte en effet le temps total utilisé par une méthode. Autrement dit, si la méthode A() utilise 2ms pour faire des opérations puis invoque la méthode B() qui occupe le processeur 10ms, nous considérerons que la méthode A() a consommé 12ms. Nous chronométrons donc es piles d'appels. Outre son aspect indispensable pour la gestion des threads, GetCurrentThreadCpuTime() se révèle d'une redoutable précision, particulièrement si on la compare à celle du System.currentTimeMillis() de l'API Java. Sachez que vous pouvez parfaitement créer un agent JVMPI contenant des fonctions JNI destinées à être exécutées depuis le code Java. Vous disposez alors d'un chronomètre plus précis pour vos applications Java.

Si nous savons maintenant chronométrer gérer nos threads et méthodes, nous ne savons toutefois pas leur associer un nom, ainsi que nous l'avons fait pour les objets. Nous devons pour cela revenir à l'événement CLASS_LOAD étudié précédemment et ajouter l'appel suivant :

methodsLoadHandler(
  event->u.class_load.class_name,
  event->u.class_load.num_methods,
  event->u.class_load.methods);
       
      
JextCopier dans Jext
L'événement de chargement d'une classe contient en effet le nombre de méthodes exposées par cette classe ainsi que leurs définitions (num_methods et methods). La fonction methodsLoadHandler() associe donc dans un dictionnaire à clé l'identification des méthodes et leur nom complet, c'est-à-dire leur nom précédé de celui de la classe. Le listing 9 présente le parcours de la liste des méthodes pour une classe donnée. Cet exemple ne vous présente que quelques-uns des événements et fonctions indispensables de JVMPI. Cette API vous réserve encore bien d'autres surprises que nous vous invitons à découvrir dans le manuel de référence à l'adresse http://java.sun.com/j2se/1.4.2/docs/guide/jvmpi/.


 Listings

void methodEntryHandler(JNIEnv* threadId, jmethodID id)
{
  MethodMap* threadInvokes =
    (MethodMap*) jvmpi_interface->GetThreadLocalStorage(threadId);
  if (threadInvokes == NULL)
  {
    threadInvokes = &methods;
  }
  // compter le nombre d'instances
  // lancer le chronomètre
}
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
void methodsLoadHandler(const char* name, jint nb, JVMPI_Method* methods)
{
  for (jint i = 0; i < nb; i++)
  {
    methodNames[methods[i].method_id] = string(name) + '.' +
      string(methods[i].method_name) + string("()");
  }
}
       
      
JextCopier dans Jext

 Téléchargement

Nous vous conseillons de télécharger le code source de TIDE pour Win32 et Linux.



par Romain Guy
romain.guy@jext.org
http://www.jext.org
Dernière mise à jour : 14/10/2006


Précédent  
  Java  
  Suivant

 
#ProgX©2005 Mathieu GINOD - Romain GUY - Erik LOUISE