OpenJDK Tracking ReentrantLock lock and unlock

90 views Asked by At

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?

2

There are 2 answers

0
apangin On

Instrumenting invokeinterface is not enough. If lock field is declared with ReentrantLock type, then lock.lock() will be called using invokevirtual rather than invokeinterface.

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/unlock methods in a more performant way, consider using Bytecode instrumentation. The idea is to change the implementation of lock/unlock methods dynamically at runtime, inserting the bytecode to call your monitoring function. Such instrumented methods can benefit from JIT compilation and all JVM optimizations.

1
alex01011 On

What I ended up doing, was editing the ReentrantLock.java file.

I added a native function in Runtime.java which then called the VM and handled the lock/unlock operation.

Inside src/java.base/share/native/libjava/Runtime.c, I added the following function:

JNIEXPORT void JNICALL
Java_java_lang_Runtime_handleLock(JNIEnv *env, jobject this)
{
    JVM_handleLock(env);
}

Then inside src/java.base/share/classes/java/lang/Runtime.java:

public native void handleLock();

Finally, inside src/hotspot/share/prims/jvm.cpp:

JVM_ENTRY(void, JVM_handleLock(JNIEnv* env))
    if (thread && thread->is_Java_thread()) {
      JavaThread *jt = (JavaThread *) thread;
      frame fr = jt->last_frame();
      // To get the actual sender frame, we need to skip Runtime and java.util.concurrent frames
      RegisterMap map(jt,
                  RegisterMap::UpdateMap::skip,
                  RegisterMap::ProcessFrames::skip,
                  RegisterMap::WalkContinuation::skip);
      fr = fr.sender(&map); // is there a better way?
      fr = fr.sender(&map);

      if (fr.is_interpreted_frame()) {
        Method *m      = fr.interpreter_frame_method();
        address bcp    = fr.interpreter_frame_bcp();

        ...
      }
    }
JVM_END

I added the functions inside, make/data/hotspot-symbols/symbols-unix:

JVM_handleLock

Then to actually handle the lock/unlock functions, I only needed to add a Runtime call inside the corresponding function.

E.g for src/java.base/share/classes/java/util/concurrent/locks/ReentrantLock.java:

    public void lock() {
        sync.lock();
        Runtime.getRuntime().handleLock();
    }