Debugging can be quite tedious, complex and cumbersome. Detailed within this blog we will explore how to make Java debugging a bit easier by leveraging the JVM’s embedded debugging tools. Native C/C++ debuggers utilize operating system hooks to debug an application. The Java Virtual Machine (JVM) includes it’s own unique and powerful toolkit for debugging; the JVM Tooling Interface (JVMTI).
JVM Tooling Interface
The JVMTI is one of the most powerful components of the JVM. It contains a series of Application Programming Interfaces (APIs) that cover a wide breadth of operations within the JVM. These APIs are written in C/C++, and developers can leverage them by creating libraries which can be loaded dynamically by the JVM on startup. The tooling interface allows us to monitor the execution of applications running on the virtual machine. This enables us to extend development to other languages where just Java is not enough. Examples of its use is not limited to: metrics & thread analysis, profiling, monitoring, and in the focus of this blog debugging.
Watched Fields
One of the capabilities of JVMTI is the ability to be notified when a field is being accessed or modified. Similar to native debuggers like GDB, the JVMTI receives callback events from the JVM. The callback events that we’re interested in this case would be to be notified when a field is being accessed or modified. The callback event is generated each time until it is cleared by the JVMTI library. Unlike GDB, these APIs open up a world of opportunities we’re now able to implement very much sophisticated debugging than ever before. We’re able to create debugging libraries that not only notify us when we want them to, and we’re able to code much more specific debugging conditions. Most importantly this helps us save time and resources debugging.
Getting Started
Let’s dive right into making a Watched Fields JVMTI library. The JVMTI API headers are located in jvmti.h which is shipped with the JDK. If you’ve downloaded OpenJDK8+OpenJ9 from adoptOpenJDK this will be located inside include/jvmti.h . Starting off, we need to establish our “main” method. Creating a JVMTI library in C/C++ requires defining a function with the following signature:
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
The JavaVM parameter allows us to communicate with the VM through jvmtiEnv. This can be obtained via:
jvmtiEnv *globalEnv;
vm->GetEnv((void **) &globalEnv, JVMTI_VERSION_1_2);
Next, we need to set what capabilities we are to use. Capabilities allow us to select in advance what functionality is to be used within the JVMTI library. This includes setting to which callback functions are to be called, what events are to be generated and any respective functionality. A full list of capabilities can be found here.
There are two main sets of functionalities with Watched Fields. We’re implementing the functionalities to both be notified when the Java application accesses the watched field, and when the application writes to it. Therefore, we need to address two different callbacks from the VM for these events. These capabilities are listed as can_generate_field_access_events for access events, and can_generate_field_modification_events for modification events. The code we need looks something like this:
// Set Capabilities
jvmtiCapabilities capabilities;
memset(&capabilities, 0, sizeof(capabilities));
capabilities.can_generate_field_access_events = 1;
capabilities.can_generate_field_modification_events = 1;
globalEnv->AddCapabilities(&capabilities);
Each of these capabilities will need a separate call back routine, since we’re going to be listening for two separate events; a field access, and a field modification. The two call back routines will be called fieldAccess, and fieldModification. The full list of callbacks can be found here. The code to register the callbacks looks like this:
// Set CallBacks
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
// Receive callback when a field is read
callbacks.FieldAccess = fieldAccess;
// Receive a callback when a field is written to
callbacks.FieldModification = fieldModification;
// Set Callback Events
globalEnv->SetEventCallbacks(&callbacks, sizeof(callbacks));
// Set Notifcation for both Events
globalEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_FIELD_ACCESS, NULL);
globalEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_FIELD_MODIFICATION, NULL);
Moving to creating the callback functions, we will need one each for field accesses, and field modifications. These function signatures for these callbacks are documented here. During the modification callback, we read the value by using GetStaticIntField, but other methods exist to read values of other types; check them out here. This callback uses JNI to read old value, so we will see the fieldAccess callback to also trigger. Overall, the implementation looks something like this:
// Receive a call back when the field is accessed
void JNICALL
fieldAccess(jvmtiEnv *jvmti_env,
JNIEnv* jni_env,
jthread thread,
jmethodID method,
jlocation location,
jclass field_klass,
jobject object,
jfieldID field)
{
printf("field access method %p location %d\n", method,(jint)location);
}
// Receive a call back when field is modified
void JNICALL
fieldModification(jvmtiEnv *jvmti_env,
JNIEnv* jni_env,
jthread thread,
jmethodID method,
jlocation location,
jclass field_klass,
jobject object,
jfieldID field,
char signature_type,
jvalue new_value)
{
int val = jni_env->GetStaticIntField(field_klass, field);
printf("Field Modification current value: %d new value %d\n",
val, new_value.i);
}
The fieldAccess function simply prints the method address and field location. The fieldModification prints the value to be replaced, and what it is replaced with. The callback occurs before the value is fully written, so we can still access the older value at the time of the callback.
Finally, we need a function to add the watched fields. This is done by creating a Java Native Interface Call (JNICALL). JNI is used to integrate any C/C++ library with a Java program, and is used when programs cannot be written in purely java. Let’s call this function setWatch.
// Add Watched Fields
JNIEXPORT void JNICALL
Java_Test_setWatch(JNIEnv *env, jobject classObject, jobject fieldObject, jchar type) {
// classObject is the class object we're watching
// fieldObject is refereed to reflect the field object passed in
// type is the type of the field object passed in
jclass rcv = env->GetObjectClass(classObject);
jfieldID fid = env->FromReflectedField(fieldObject);
char fieldType = (char)type;
if (fid != NULL) {
jvmtiError err = globalEnv->SetFieldModificationWatch(rcv, fid);
if (err != JVMTI_ERROR_NONE)
printf("Failed to add modification watch %d\n", err);
err = globalEnv->SetFieldAccessWatch(rcv, fid);
if (err != JVMTI_ERROR_NONE)
printf("Failed to add access watch %d\n", err);
}
}
The JNI method naming convention is Java_<Name of the Java Class>_<Name of the function>. In this case our Java Class is named Test, and we’ve chosen to call our function setWatch. Here we need an accompanying call within the Java application. Calling setWatch passes a reference to the field being watched, and a character which represents the type of that field.
Once you’ve completed your library, we can compile it. We link both the path to the jvmti header file, and the source code of your library. We will name our library libtest.so. Here is an example how this would look like compiling on linux using a build from adoptOpenJDK.
g++ -I/sdk/include -I/sdk/include/linux -fPIC -c test.cpp
g++ -shared -fPIC -o libtest.so test.o -lc
Start Watching Fields
Let’s move our attention to Java. To start watching fields on Java we need to call the Java native method setWatch(). This Java method is mapped to the function Java_Test_setWatch that we defined earlier in the JVMTI library. The type is specified according to the value type table found here.
import java.lang.reflect.*;
public class Test {
public static int MyField;
public static native void setWatch(Field theField, char type);
public static void main(String args[]) throws Throwable {
// Start watching fields
Field field = Test.class.getField("MyField");
setWatch(field, 'i');
// Will trigger a modification event
MyField = 3;
// Will trigger an access event
int local = MyField;
}
}
Finally, let’s run our program. The agentpath is determined by the path to the native library libtest.so we created earlier.
/sdk/bin/javac Test.java
/sdk/jre/bin/java -agentpath:/workspace/libtest.so -Djava.library.path=/workspace
We can see the call backs occur. For example:
Field modification current value 0 new value 3
Field access method 0x7ffff04e3d68 location 7
Note: There are other techniques to watch fields, including not needing to modify the Java code. Adding a class load event will allow an event to generated when a class is loaded. Here you can filter by classes, and add fields you would want to monitor as you wish. This blog won’t elaborate further, but check the official docs if interested.
Performance
Unfortunately, if you’re using OpenJDK+Hotspot the performance of methods with watched fields could be less than desired. Let’s take a look at a simple benchmark.
Let’s use a simple benchmark that generates an array of random integers, sorts them and stores the median value. We will monitor the median value, which belongs to the Test class. Using the setWatch method defined above accordingly we can enable call backs when this field is accessed and modified.
import java.lang.RuntimeException;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Random;
public class Test {
public static int median = 0;
public static native void setWatch(Field theField, char type);
private static int NUM_LOOPS=123456;
private static int ARRAY_SIZE = 51;
public static void main(String args[]) throws Throwable {
// Adding Watches
Field field = Test.class.getField("median");
setWatch(field, 'i');
long startTime = System.nanoTime();
for (int i = 0; i < NUM_LOOPS; i++) {
Random rand = new Random();
int [] items = new int [ARRAY_SIZE];
for(int j = 0; j < ARRAY_SIZE; j++) {
items[j] = rand.nextInt(100);
}
// Sort
Arrays.sort(items);
// Store median
if (items.length%2==0)
median = (items[(items.length-1/2)] + items[(items.length-1/2 + 1)])/2;
else
median = items[(items.length - 1)/2];
}
long endTime = System.nanoTime();
long timeElapsed = endTime - startTime;
System.out.println("Program took:" + timeElapsed/1000000 + " milliseconds");
}
}
Next, running the test.
java -agentpath:<path to native libraries>/libtest.so -Djava.library.path=<path to native libraries> WatchTest
Running this on a Linux PPC64LE Power 8 machine yields:
OpenJDK + OpenJ9 on Java 8:
Program took:2046 milliseconds
OpenJDK + Hotspot on Java 8:
Program took:24654 milliseconds
That’s over a 1200% performance improvement.
What gives OpenJ9 the edge is that OpenJDK+OpenJ9 (R0.16+ running PPC64, X86, and S390) ships with JIT support to compile methods with watched fields rather than interpret them. If you’re interested to take a test drive you can grab a build over at adoptOpenJDK. You can learn more, and even contribute by visiting the OpenJ9 Eclipse project.
Associated Files