I am doing runtime instrumentation using the interpreter.
My focus now is adding a function call before any ReEntractLock lock() and unlock() function.
For reference, this is the Test.java:
class Test {
static Lock lock = new ReentrantLock();
public String name;
public Test(String name) {
this.name = name;
}
public void test() {
lock.lock();
try {
System.out.println("Hello " + name);
name = "World";
} finally {
lock.unlock();
}
}
}
by examining the bytecode, I found that on each lock/unlock, there is an invokeinterafce bytecode.
public void test();
Code:
0: getstatic #13 // Field lock:Ljava/util/concurrent/locks/Lock;
3: invokeinterface #17, 1 // InterfaceMethod java/util/concurrent/locks/Lock.lock:()V
8: ...
26: getstatic #13 // Field lock:Ljava/util/concurrent/locks/Lock;
29: invokeinterface #23, 1 // InterfaceMethod java/util/concurrent/locks/Lock.unlock:()V
34: ...
So I went into src/hotspot/cpu/x86/templateTable_x86.cpp, and added a vm leaf call.
void TemplateTable::invokeinterface(int byte_no) {
...
// rbx: Method* to call
// rcx: receiver
// Check for abstract method error
// Note: This should be done more efficiently via a throw_abstract_method_error
// interpreter entry point and a conditional jump to it in case of a null
// method.
__ testptr(rbx, rbx);
__ jcc(Assembler::zero, no_such_method);
__ profile_arguments_type(rdx, rbx, rbcp, true);
__ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::monitor_invoke), rbx); // <- the call, rbx contains the method pointer
...
}
However, this way I get every method called by invoke interface, and the only way I could think was to check the name, like this
if (strstr(name, "java.util.concurrent.locks.ReentrantLock"))...
This however, looks bad and will probably add too much additional overhead.
Is there another way I can track lock/unlock calls made by java.util.concurrent or a cheaper way to do the name comparison?
Instrumenting
invokeinterfaceis not enough. Iflockfield is declared withReentrantLocktype, thenlock.lock()will be called usinginvokevirtualrather thaninvokeinterface.The good news is that you don't need to change VM sources to trace method invocation. There is a standard API for such things called JVM Tool Interface (JVM TI). All you need is to create an agent that installs a callback for MethodEntry and MethodExit events. You many find an example of using these events in this answer.
As mentioned in the documentation, enabling MethodEntry/MethodExit events may significantly degrade performance. To intercept calls to
ReentrantLock.lock/unlockmethods in a more performant way, consider using Bytecode instrumentation. The idea is to change the implementation oflock/unlockmethods dynamically at runtime, inserting the bytecode to call your monitoring function. Such instrumented methods can benefit from JIT compilation and all JVM optimizations.