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:
Innanzitutto il metodo getThreadStat è preceduto dalla parola chiave
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
...
}
}
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.
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:
Questo è il prototipo della funzione che implementerà il metodo getThjreadStat.
/*
* Class: res_JvmMonitor
* Method: getThreadStat
* Signature: ()[Lres/ThreadStat;
*/
JNIEXPORT jobjectArray JNICALL Java_res_JvmMonitor_getThreadStat
(JNIEnv *, jobject);
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.
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.
//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;
}
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.