software, programming, functional
home mail me! RSS (2.0) feed

JVMTI: real-time analysis of Java applications

JVMTI is an API built into both JDK 5.0 and 6.0, enabling an aspect-oriented approach to dynamic analysis - such as performance and coverage analysis - without the overhead of ordinary AOP approaches.

We all have experienced the need to find that performance bottleneck or that last crucial bug making our most vocal client crazy.

IDEs allow us to break at certain instructions and inspect the state of the application, more or less manually. Not only is this approach tedious but it changes the dynamics quite significantly, making it useless for performance or synchronization issues.

Say that we want to find the number of invocations of the method doThis in the following application:

JAVA:
  1. class JvmtiDemo {
  2.     public static void main(String argv[])
  3.     {
  4.     JvmtiDemo demo = new JvmtiDemo();
  5.     for (int i = 0; i <10; ++i)
  6.         demo.doThis();
  7.     }
  8.     public void doThis() {
  9.     System.out.println("Doing something");
  10.     }
  11. }

The immediate approach is to manually insert analytical logic, such as in

JAVA:
  1. class JvmtiDemo {
  2.     private static int invocationCount_ = 0;
  3.     public static void main(String argv[])
  4.     {
  5.     JvmtiDemo demo = new JvmtiDemo();
  6.     for (int i = 0; i <10; ++i)
  7.         demo.doThis();
  8.     System.out.println("'doThis' was invoked " +
  9.                invocationCount_ + " times");
  10.     }
  11.     public void doThis() {
  12.     System.out.println("Doing something");
  13.     ++invocationCount_;
  14.     }
  15. }

And then we just insert analytical statements for any analysis we want to perform, and add if-statements to enable/disable these statements where needed. Easy, right? NO!

We could always use an Aspect-Oriented framework, such as AspectJ or the AOP server from JBoss, but they often require you to recompile the code and always changes the dynamics of the code within the JVM, and come with significant tax on the runtime performance.

Fortunately, Sun has had various APIs for real-time "aspects" built into their JVMs, such as JVMDI (JVM Debugger Interface) or JVMPI (JVM Performance Interface) since JDK 1.2 - I think. With Java 5.0, these two APIs became deprecated as a new API was introduced: JVMTI (JVM Tool Interface), and in Java 6.0, the two older APIs are no longer implemented.

So, how do we use JVMTI?

It is a native interface, so we need to use it from C or C++.

Make sure you have a proper JDK installed - such as Java 5.0 or 6.0 from Sun, although other JVM vendors implement JVMTI quite extensively - and that you add the Java header directory to your include path. This header directory is found at $JDK_HOME/include.

The JVMTI "aspect" must contain a function called Agent_OnLoad, so the simplest aspect - here called JvmtiAspect - is

C++:
  1. #include <iostream>
  2. #include <jvmti.h>
  3.  
  4. JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
  5. {
  6.     std::cout <<"Loading aspect..." <<std::endl;
  7.     return JNI_OK;
  8. }

Compile it. NOTE: if you use OS X, you need to rename the generated dynamic library to JvmtiAspect.jnilib; the rationale behind that decision is beyond my cognitive capabilities.

So, let us inject this aspect. First of all, copy the dynamic library so it accesible by the JVM; such as the directory of the bytecode for JvmtiDemo. Second, specify that you want the JVM to load this aspect. This can be done from the command line or by setting an environment variable. We use the command line option here:


java -agentlib:JvmtiAspect JvmtiDemo

Yep, the aspect was loaded properly!

So what about actually performing some analysis?

JVMTI uses events and associated callbacks. One usually has to do three things for a specific event:

  • Enable capability
  • Enable event
  • Register the actual callback function

The following revised version performs these three steps for the event METHOD_ENTRY:

C++:
  1. #include <iostream>
  2. #include <jvmti.h>
  3.  
  4. void JNICALL cb_method_entry(jvmtiEnv *jvmti_env,
  5.             JNIEnv* jni_env,
  6.             jthread thread,
  7.             jmethodID method)
  8. {
  9.     std::cout <<"Entering a method..." <<std::endl;
  10. }
  11.  
  12. void init_jvmti_callbacks(JavaVM* vm)
  13. {   
  14.     std::cout <<"Initing JVMTI callbacks" <<std::endl;
  15.     jvmtiEnv* env;
  16.     vm->GetEnv(reinterpret_cast<void**>(&env), JVMTI_VERSION);
  17.        
  18.     jvmtiCapabilities capabilities = { 1 };
  19.     jvmtiEventCallbacks callbacks = { 0 };
  20.  
  21.     capabilities.can_generate_method_entry_events = 1;
  22.     env->AddCapabilities(&capabilities);
  23.     env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, NULL);   
  24.     callbacks.MethodEntry = &cb_method_entry;
  25.     env->SetEventCallbacks(&callbacks, sizeof(callbacks));
  26. }
  27.  
  28. JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
  29. {
  30.     std::cout <<"Loading aspect..." <<std::endl;   
  31.     init_jvmti_callbacks(vm);
  32.     return JNI_OK;
  33. }

Try to run the Java code again. You will se a lot of method entries. This is because all methods are analyzed, including those of the Java core library.

Let us restrict the methods of interest to doThis. Luckily, given a local identifier for the Java method entered, we can get the name, using the JVMTI function GetMethodName. There is some cleaning up needed for the returned strings as well. Look at the following version:

C++:
  1. #include <iostream>
  2. #include <string>
  3. #include <jvmti.h>
  4.  
  5. void JNICALL cb_method_entry(jvmtiEnv *env,
  6.             JNIEnv* jni_env,
  7.             jthread thread,
  8.             jmethodID method)
  9. {
  10.     // Get name of method
  11.  
  12.     std::string theName;
  13.     char* name;
  14.     char* signature;
  15.     char* gen;
  16.     env->GetMethodName(method, &name, &signature, &gen);
  17.     if (name) {
  18.         theName.assign(name);
  19.         env->Deallocate(reinterpret_cast<unsigned char*>(name));
  20.     }
  21.     if (signature) {
  22.         env->Deallocate(reinterpret_cast<unsigned char*>(signature));
  23.     }
  24.     if (gen) {
  25.         env->Deallocate(reinterpret_cast<unsigned char*>(gen));
  26.     }   
  27.  
  28.     // Are we interested?
  29.  
  30.     if (theName == "doThis")
  31.         std::cout <<"Entering method " <<theName <<"..." <<std::endl;
  32. }
  33.  
  34. void init_jvmti_callbacks(JavaVM* vm)
  35. {   
  36.     std::cout <<"Initing JVMTI callbacks" <<std::endl;
  37.     jvmtiEnv* env;
  38.     vm->GetEnv(reinterpret_cast<void**>(&env), JVMTI_VERSION);
  39.        
  40.     jvmtiCapabilities capabilities = { 1 };
  41.     jvmtiEventCallbacks callbacks = { 0 };
  42.  
  43.     capabilities.can_generate_method_entry_events = 1;
  44.     env->AddCapabilities(&capabilities);
  45.     env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, NULL);   
  46.     callbacks.MethodEntry = &cb_method_entry;
  47.     env->SetEventCallbacks(&callbacks, sizeof(callbacks));
  48. }
  49.  
  50. JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
  51. {
  52.     std::cout <<"Loading aspect..." <<std::endl;   
  53.     init_jvmti_callbacks(vm);
  54.     return JNI_OK;
  55. }

Now you will get the log output only when entering our method, doThis. So, finally, let us remove this analysis logic from the Java code and add it to the aspect, utilizing a callback function called Agent_Unload, which is invoked when the aspect is unloaded. This usually coincides with the application exiting. See the final aspect and the original Java code:

C++:
  1. #include <iostream>
  2. #include <string>
  3. #include <jvmti.h>
  4.  
  5. static int g_entry_count = 0;
  6.  
  7. void JNICALL cb_method_entry(jvmtiEnv *env,
  8.             JNIEnv* jni_env,
  9.             jthread thread,
  10.             jmethodID method)
  11. {
  12.     // Get name of method
  13.  
  14.     std::string theName;
  15.     char* name;
  16.     char* signature;
  17.     char* gen;
  18.     env->GetMethodName(method, &name, &signature, &gen);
  19.     if (name) {
  20.         theName.assign(name);
  21.         env->Deallocate(reinterpret_cast<unsigned char*>(name));
  22.     }
  23.     if (signature) {
  24.         env->Deallocate(reinterpret_cast<unsigned char*>(signature));
  25.     }
  26.     if (gen) {
  27.         env->Deallocate(reinterpret_cast<unsigned char*>(gen));
  28.     }   
  29.  
  30.     // Are we interested? If so, increment our counter.
  31.  
  32.     if (theName == "doThis")
  33.         ++g_entry_count;
  34. }
  35.  
  36. void init_jvmti_callbacks(JavaVM* vm)
  37. {   
  38.     std::cout <<"Initing JVMTI callbacks" <<std::endl;
  39.     jvmtiEnv* env;
  40.     vm->GetEnv(reinterpret_cast<void**>(&env), JVMTI_VERSION);
  41.        
  42.     jvmtiCapabilities capabilities = { 1 };
  43.     jvmtiEventCallbacks callbacks = { 0 };
  44.  
  45.     capabilities.can_generate_method_entry_events = 1;
  46.     env->AddCapabilities(&capabilities);
  47.     env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, NULL);   
  48.     callbacks.MethodEntry = &cb_method_entry;
  49.     env->SetEventCallbacks(&callbacks, sizeof(callbacks));
  50. }
  51.  
  52. JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
  53. {
  54.     std::cout <<"Loading aspect..." <<std::endl;   
  55.     init_jvmti_callbacks(vm);
  56.     return JNI_OK;
  57. }
  58.  
  59. JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm)
  60. {
  61.     std::cout <<"Unloading aspect, with " <<
  62.         g_entry_count <<" entries of our favorite method" <<std::endl;
  63. }

JAVA:
  1. class JvmtiDemo {
  2.     public static void main(String argv[])
  3.     {
  4.     JvmtiDemo demo = new JvmtiDemo();
  5.     for (int i = 0; i <10; ++i)
  6.         demo.doThis();
  7.     }
  8.     public void doThis() {
  9.     System.out.println("Doing something");
  10.     }
  11. }

I hope this shed some light into the powers of JVMTI.

Happy analysis!

Leave a Comment

You must be logged in to post a comment.