Part 42 - management of JNI references

Java services will be used in local functions, which can be obtained by calling the functions encapsulated in JNIEnv. We can access the passed reference type parameters in the local function, or create a new Java object through the JNI function. These Java objects are obviously also affected by GC. Therefore, we need to ensure that the GC does not recycle Java objects that may be referenced in these local functions through JNI's Local Reference and Global Reference.

Whether it is a local reference or a global reference, it is actually referenced through a handle. There are two ways to store the handle corresponding to the local reference. One is to store the reference type parameters from the Java level received by the C function in the local method stack frame; The other is a thread private handle block, which is mainly used to store local references created during the operation of local functions (actually these operations are completed through JNI functions). Both the reference type parameters passed in and the reference type objects returned through JNI functions (except NewGlobalRef and NewWeakGlobalRef) belong to local references.

We should not be unfamiliar with handles. As described in detail in the book in-depth analysis of Java virtual machine: source code analysis and detailed explanation of examples (basic volume), when the Java stack references the objects in the Java heap, it will be referenced through handles. Handles refer to the pointers of Java objects in memory. At the same time, it also introduces the usage of HandleMark, HandleArea and Chunk, which is used to solve the local code reference within the JVM. When garbage collection occurs, if the Java object is moved, the pointer value pointed to by the handle will also change, but the handle itself remains unchanged.

The JNI handle of the HotSpot VM is placed in several different areas, but not in the GC heap. The handle used to pass parameters is directly on the stack; The local handle is placed in the JNIHandleBlock in each Java thread; The global handle is placed in the JNIHandleBlock of the HotSpot VM.

The JNIHandles class is defined as follows:

Source code location: openjdk/hotspot/src/share/vm/runtime/jniHandles.hpp
 
class JNIHandles : AllStatic {
 private:
  // The header element of the JNIHandleBlock linked list that holds the global reference
  static JNIHandleBlock*  _global_handles;    
  // The header element of the JNIHandleBlock linked list that holds the global weak reference
  static JNIHandleBlock*  _weak_global_handles;
  static oop              _deleted_handle;                    
  ...
}
 

Call the initialize() function of JNIHandles class to initialize the above properties, as follows:

void JNIHandles::initialize() {
  _global_handles      = JNIHandleBlock::allocate_block();
  _weak_global_handles = JNIHandleBlock::allocate_block();
  // The macro is extended as follows:
  // Thread*        __the_thread__ = 0;
  // ExceptionMark  __em(__the_thread__);
  EXCEPTION_MARK;

  Klass* k      = SystemDictionary::Object_klass();
  _deleted_handle = InstanceKlass::cast(k)->allocate_instance(CATCH);
}

The HotSpot VM calls init at startup_ The globals() function initializes the global module, init_ The globals() function will indirectly call the JNIHandles::initialize() function, in which the corresponding JNIHandleBlock block is allocated to the global variables. Therefore, the handle of the global object is stored in JNIHandleBlock.

Jnihandles are divided into two types: global and local object references. Most object references belong to local object references. Finally, JNIHandleBlock is called to manage them. Because JNIHandle does not design a mechanism of JNIHandleMark, it is necessary to explicitly call jnihandles:: mark when creating local object references_ The local() function also needs to explicitly call jnihandles:: destroy when recycling_ Local() function.

Variables related to local reference objects defined in the thread are as follows:

// Holds an active JNIHandleBlock block, and the handle object stored in the block is also active
JNIHandleBlock* _active_handles;
 
// Save the free JNIHandleBlock block and reuse it if necessary
JNIHandleBlock* _free_handle_block;
 
HandleMark* _last_handle_mark;

Whether it is a global or local object reference, its handle is stored in the JNIHandleBlock block. When a new block needs to be allocated, JNIHandleBlock:: allocate is called_ Block() function; When no block is needed, JNIHandleBlock:: release is called_ Block() to release the JNIHandleBlock block. Among them, the most typical application of allocating new blocks and releasing blocks is in the constructor and destructor of JavaCallWrapper class. We have contacted this JavaCallWrapper before, that is, when HotSpot VM calls the main() method of Java main class, it will call JavaCalls::call_helper() function, which is called as follows:

{
    // Use JavaCallWrapper to save relevant information
    JavaCallWrapper link(method, receiver, result, CHECK);
    {
      HandleMark hm(thread);  
      StubRoutines::call_stub()(
         (address)&link,
         result_val_address,              
         result_type,
         method(),
         entry_point,
         args->parameters(),
         args->size_of_parameters(),
         CHECK
      );
  
      result = link.result(); 

      if (oop_result_flag) {
        thread->set_vm_result((oop) result->get_jobject());
      }
    }
} // Exit JavaCallWrapper (can block - potential return oop must be preserved)

This link will be stored on the stack when calling Java methods from C/C + + functions, as shown in the following figure.

The call wrapper is the saved link value.

In fact, any call from C/C + + to Java method will save the call wrapper in the stack frame of C/C + +. The saved information is very important because the C/C + + functions parasitic in the C/C + + stack are mixed with the stack frame corresponding to the Java method. Sometimes we need to traverse the C/C + + stack frame, and sometimes we need to traverse the Java stack frame. When the C/C + + function or Java function is executed, We will not introduce these contents too much here. We only care about the local variable handle used by C/C + + functions.  

As shown in the above figure, in the first C/C + + stack frame (not the C/C + + stack frame corresponding to the currently executed function), you can find the JavaCallWrapper through the call wrapper, and then through the JavaCallWrapper::_handles finds the previously used JNIHandleBlock single linked list, so that you can traverse the objects in the heap referenced in the previous C/C + + stack frame. In the second C/C + + stack frame (currently executing function), through Thread::_active_handles can find the currently used JNIHandleBlock single linked list, so that you can traverse the objects in the referenced heap. As for the heap objects referenced by the Java stack, it was introduced in the book "in depth analysis of Java virtual machine: source code analysis and example explanation (basic volume)", which can be managed through HandleMark, HandleArea and Chunk.

If GC occurs, you need to traverse all C/C + + stacks in the thread to find all the used JNIHandleBlock blocks, so as not to miss the mark.

The JavaCallWrapper class has the following attribute definitions:

JNIHandleBlock*  _handles; // Pointer to the memory block that actually holds the JNI reference

The JavaCallWrapper constructor has the following implementation:  

JavaCallWrapper::JavaCallWrapper(
 methodHandle  callee_method, 
 Handle        receiver, 
 JavaValue*    result, 
 TRAPS
) {
  JavaThread* thread = (JavaThread *)THREAD;
  // ...
  
  // Assign a new JNIHandleBlock
  JNIHandleBlock* new_handles = JNIHandleBlock::allocate_block(thread);
  
  // ...
  
  _thread       = (JavaThread *)thread;
  // Saves the active of the current thread_ handles
  _handles      = _thread->active_handles();   
  
  
  // Take the newly allocated JNIHandleBlock as the active of the thread_ handles
  _thread->set_active_handles(new_handles);     
}

JNIHandleBlock:: allocate needs to be called for both global and local variables_ The block() function allocates JNIHandleBlock. The JNIHandleBlock class is defined as follows:

class JNIHandleBlock : public CHeapObj<mtInternal> {
 private:
  enum SomeConstants {
    // Only 32 handles can be allocated in each JNIHandleBlock, so only 32 oop can be stored
     block_size_in_oops  = 32                    
  };
 
  // oop is stored in the handle. Local functions can only operate oop through the handle
  oop               _handles[block_size_in_oops];
  // Next unused_ The slot in handles can store oop on this slot,
  // Then return the address of this slot to the local function for operation
  int               _top;                        
  // Pass_ The next field connects all jnihandleblocks into a single linked list
  JNIHandleBlock*   _next;                        
 
  // Points to the last block in the JNIHandleBlock linked list, and the_ handles is responsible for allocating handle regions for the current thread
  JNIHandleBlock*   _last;                 
  JNIHandleBlock*   _pop_frame_link;              
 
  // Connect free handle areas through a list 
  oop*              _free_list; 
            
  // Connect the idle JNIHandleBlock into a single linked list through the following fields. Note that this is
  // A static variable, so the JNIHandleBlock block saved in this list can be reused by any thread
  static JNIHandleBlock* _block_free_list;      
  // ...
}

The description of each attribute is shown in the following figure.

Note that when allocating a handle to a local variable in a thread, the_ Of the JNIHandleBlock block pointed to by last_ Allocated in the handles array, if the top has pointed to_ The next position of the handles array indicates that the array has been unable to allocate additional handle space, and JNIHandleBlock:: allocate needs to be called_ The block () function allocates a new JNIHandleBlock and connects it to the single linked list.

Allocate_ in the JNIHandleBlock class called in JavaCallWrapper::JavaCallWrapper() constructor. The implementation of the block() function is as follows:  

JNIHandleBlock* JNIHandleBlock::allocate_block(Thread* thread)  {
  JNIHandleBlock* block;
 
  // If the thread of the current thread:_ free_ handle_ Free in block
  // JNIHandleBlock of, you can get it from the free list
  if (thread != NULL && thread->free_handle_block() != NULL) {
    block = thread->free_handle_block();
    thread->set_free_handle_block(block->_next);
  }
  else {
    MutexLockerEx  ml(JNIHandleBlockFreeList_lock,Mutex::_no_safepoint_check_flag);
    if (_block_free_list == NULL) {
       // If there is no free JNIHandleBlock in the free list, a new JNIHandleBlock is assigned
// The memory of JNIHandleBlock is allocated by calling os::malloc() function block = new JNIHandleBlock(); _blocks_allocated++; if (ZapJNIHandleArea) block->zap(); } else { // From jnihandleblock::_ block_ free_ Get free block from list block = _block_free_list; _block_free_list = _block_free_list->_next; } } block->_top = 0; block->_next = NULL; block->_pop_frame_link = NULL; return block; }

The function above is called when the thread starts, as in VMThread::run(), WatcherThread::run() and JavaThread::run() function, because these functions may execute the native method. When from thread_ free_handle_block and JNIHandleBlock:__ block_ free_ When no free JNIHandleBlock block can be allocated in the list, you need to create a new JNIHandleBlock through the new keyword. JNIHandleBlock inherits from cheapobj < mtinternal >, so the memory of the block will be allocated from the local memory by calling os::malloc() function.

Javacallwrapper:: ~ the implementation of javacallwrapper() destructor is as follows:

JavaCallWrapper::~JavaCallWrapper() {
  // Verify that the same Java thread performs destruct
  assert(_thread == JavaThread::current(), "must still be the same thread");
  
  // Gets the active of the current thread_ handles
  JNIHandleBlock *_old_handles = _thread->active_handles();
  // Restore active before method call_ handles
  _thread->set_active_handles(_handles);
  
  // ...
  
  // Release the newly allocated JNIHandleBlock in the method call
  JNIHandleBlock::release_block(_old_handles, _thread);
}

The destructor is called when the Java method returns to the C/C + + function, calling jnihandleblock:: release_ The block () function is equivalent to releasing the handle in the local function stack frame. Therefore, we can also see that once returned from the local function to the Java method, the local reference will be invalidated. That is, the garbage collector no longer considers these local references when marking garbage. This means that we cannot cache local references for use by another thread or the next native method call. For this application scenario, we need to use the JNI function NewGlobalRef to convert the local reference into a global reference to ensure that the Java object it points to will not be garbage collected. Accordingly, we can also eliminate the global reference through the JNI function DeleteGlobalRef to recycle the Java object pointed to by the global reference.

Called release_ The implementation of the block() function is as follows:

void JNIHandleBlock::release_block(JNIHandleBlock* block, Thread* thread) {
  JNIHandleBlock* pop_frame_link = block->pop_frame_link();
 
  if (thread != NULL ) {
    if (ZapJNIHandleArea) 
       block->zap();
    JNIHandleBlock* freelist = thread->free_handle_block();
    block->_pop_frame_link = NULL;
    thread->set_free_handle_block(block);
 
    // Add new free blocks to the head of the list and other free blocks to the end of the list
    if ( freelist != NULL ) {
      while ( block->_next != NULL ) 
          block = block->_next;
      block->_next = freelist;
    }
    block = NULL;
  }

  if (block != NULL) {
    MutexLockerEx ml(JNIHandleBlockFreeList_lock,Mutex::_no_safepoint_check_flag);
    while (block != NULL) {
      if (ZapJNIHandleArea) 
          block->zap();
      //  If the parameter thread passed in by the function is NULL, the block is connected to the static variable
      // _ block_free_list
      JNIHandleBlock* next = block->_next;
      block->_next = _block_free_list;
      _block_free_list = block;
      block = next;
    }
  }
  // ...
} 

When the thread is not NULL, connect the idle JNIHandleBlock to the thread:_ free_ handle_ Block, otherwise connect to JNIHandleBlock::_ block_ free_ On the list. Generally speaking, if the JNIHandleBlock used by the thread is idle, it will be connected to the thread:_ free_ handle_ Block, but when the thread exits or ClassLoaderData::_handles (used to reference connected objects, as described earlier) will be returned to JNIHandleBlock when uninstalling:_ block_ free_ List, so that other threads can also use these idle jnihandleblocks, unlike thread::_ free_ handle_ Like block, it can only be reused in this thread.  

  official account   In depth analysis of Java virtual machine HotSpot   The articles related to source code analysis of virtual machines have been updated to 60 +. You are welcome to pay attention. If you have any questions, you can add the author wechat mazhimazh to pull you into the virtual machine cluster for communication
 

  

 

 

 

 

 

  

 

Posted on Fri, 03 Dec 2021 06:12:24 -0500 by jamal dakak