In profondità

Leggere i dati

Cosa succede lanciando il comando java -Xrunjvmmonitor UnaVostraClasse? Succede che il Profiler inizia a registrare tutti gli eventi della vostra applicazione (rallentandola un pochino), senza peraltro interferire con essa. La cosa è perfettamente inutile perchè nessuno è in grado di leggere i dati del Profiler, che rimangono quindi inutilizzati.

La filosofia di questo studio è di rendere disponibili tali dati direttamente a un monitor Java, in modo che eventuali azioni correttive possano essere intraprese direttamente dall'applicazione stessa. Il tramite di questo meccanismo è la classe res.JvmMonitor, già presentata in precedenza.
Vediamo alcuni punti della sua dichiarazione:

  public class JvmMonitor{
  //ritorna le statistiche di tutti i thread attivi
  public native ThreadStat[] getThreadStat();

  static
    {
    //carica la DLL
    try{System.loadLibrary("jvmmonitor");}   //carica dll
    ...
    }  
  }
Innanzitutto il metodo getThreadStat è preceduto dalla parola chiave native, ed è senza corpo. Ciò significa che la JVM non eseguirà del bytecode bensì del codice già compilato contenuto in una DLL, secondo il protocollo descritto in JNI. L'istruzione System.loadLibrary serve appunto per caricare la libreria.
La strana sezione static non deve stupire: serve semplicemente per eseguire le istruzioni al suo interno una volta sola, all'atto dell'inizializzazione della classe.

Dal punto di vista della chiamata, un metodo nativo è esattamente uguale ad uno "normale", dunque non vi sono inghippi nel passaggio dei parametri o dei valori restituiti. L'unico inconveniente è che il metodo è portabile se e solo se è stata compilata una DLL idonea per la piattaforma dove si sta eseguendo il programma. Dunque per assicurare la completa portabilità basterà avere una DLL per Windows, una per Linux, e così via.

Un'ultima puntualizzazione. Che differenza c'è tra caricare la DLL con l'opzione -Xrun e il metodo System.loadLibrary? E perchè si sta caricando due volte la stessa DLL jvmmonitor - una non è ridondante?
Innanzitutto ci sono delle differenze temporali: -Xrun carica la DLL subito, all'inizializzazione della JVM; System.loadLibrary invece carica la DLL all'inizializzazione della classe, cioè non appena si istanzia il primo oggetto di tipo JvmMonitor.
-Xrun è opzionale, ma sembra lo strumento ideale per un Profiler. In effetti le funzionalità di JVMPI potrebbero essere sfruttate anche successivamente, ma sembra più naturale creare subito un Profiler. System.loadLibrary invece è di supporto a JNI, ed è obbligatorio nella definizione di ogni classe che presenti dei metodi nativi.

Nel caso specifico, aver utilizzato la stessa DLL per il Profiler e per i metodi nativi della classe JvmMonitor è stato, per così dire, un caso, poichè concettualmente le due funzioni sono distinte. Il vantaggio evidente e che le strutture dati coinvolte sono le medesime, e dunque debbono essere visibili in due fasi: in un caso il Profiler scrive le statistiche nel database, nell'altro un metodo nativo le legge.

Scrivere un metodo nativo

Cosa occorre, a questo punto, per scrivere il metodo nativo in C o C++? E' sufficiente sapere il prototipo della funzione e come maneggiare i tipi Java. Nel jdk è incluso un comando, javah, che genera automaticamente l'header file coi prototipi delle funzioni, partendo da un file .class contenente dichiarazioni di metodi nativi. Ad esempio, lanciando javah res.JvmMonitor vedrete comparire il file res_JvmMonitor.h, contenente, tra il resto:

  /*
   * Class:     res_JvmMonitor
   * Method:    getThreadStat
   * Signature: ()[Lres/ThreadStat;
   */
  JNIEXPORT jobjectArray JNICALL Java_res_JvmMonitor_getThreadStat
    (JNIEnv *, jobject);
Questo è il prototipo della funzione che implementerà il metodo getThjreadStat.
I tag JNIEXPORT e JNICALL servono al preprocessore e non hanno significato. Gli argomenti in ingresso e il valore restituito sono gli equivalenti C dei tipi Java, e sono dichiarati in jni.h. Usare questi tipi è garanzia di completa compatibilità tra tipi C e tipi Java. In verità, in ingresso sono sempre presenti due argomenti in più rispetto a quelli che ci si aspetta. Il primo, che chiameremo env, è un puntatore a JNIEnv (la stessa struttura vista negli eventi di JVMPI), il secondo, che chiameremo thiz, è di tipo jobject. Come alcuni avranno già capito, env identifica il thread chiamante, e permette di avere accesso alle potenzialità di JNI utilizzando le chiamate env->NomeFunzione(argomenti). thiz invece identifica l'oggetto Java chiamante (analogamente al this del C++).
Vediamo di verificare quanto spiegato direttamente con l'esempio.
  //file jvmmon.cpp
  #include < jni.h >

  JNIEXPORT jobjectArray JNICALL
  Java_res_JvmMonitor_getThreadStat(JNIEnv *env, jobject thiz)
    {
    jclass clazz;
    jobject obj;
    jobjectArray array;
    jfieldID fid[14];
    jint i;
    Stat *stat;

    //Devo restituire un array di oggetti ThreadStat
    //contenenti le statistiche sui thread; dunque:
    //identifico la classe
    clazz=env->FindClass("res/ThreadStat");
    //identifico il campo thread di tipo Thread
    fid[0]=env->GetFieldID(clazz,"thread","Ljava/lang/Thread;");
    //identifico il campo method_num di tipo int
    fid[1]=env->GetFieldID(clazz,"method_num","I");
    //e così via
    ...

    lock();
    //per ogni entry della tabella thread_table
    for(i=0;i < thread_table.size();i++)
      {
      stat=thread_table.getAt(i);
      //costruisco un oggetto ThreadStat
      obj=env->AllocObject(clazz);
      //imposto i campi, prima identificati,
      //coi valori presi dalla thread_table
      env->SetObjectField(obj,fid[0],stat->thread);
      env->SetIntField(obj,fid[1],stat->method_num);
      //la prima volta costruisco l'array
      //con la giusta dimensione
      if(i==0)
        array=env->NewObjectArray(thread_table.size(),clazz,obj);
      //le volte successive imposto l'i-esimo
      //elemento dell'array
      else
        env->SetObjectArrayElement(array,i,obj);
      }
    unlock();
    //et voilà
    return array;
    }
Si è utilizzato l'argomento env per chiamare una serie di funzioni che interagiscono con la JVM. Si è anche visto come i riferimenti a oggetti Java siano di tipo generico jobject, e vadano meglio caratterizzati identificandone la classe di appartenenza. Questa pubblicazione non ha la pretesa di essere esaustiva in merito, per cui si consiglia di sfogliare una guida a JNI. Per i più arguti basterà esaminare il solo file jni.h.

Con questo si è finalmente conclusa la trattazione degli aspetti fondamentali del monitor; la pagina che segue è un addendum per quanto riguarda la misurazione del consumo di CPU sotto Windows NT, un contatore non certo marginale ma per ora disponibile solo su questa piattaforma.

precedente|continua