第42篇-JNI引用的管理(1)

  • 2021 年 12 月 3 日
  • 筆記

在本地函數中會使用Java服務,這些服務都可以通過調用JNIEnv中封裝的函數獲取。我們在本地函數中可以訪問所傳入的引用類型參數,也可以通過JNI函數創建新的 Java 對象。這些 Java 對象顯然也會受到GC的影響。所以我們需要通過JNI 的局部引用(Local Reference)和全局引用(Global Reference)來保證不讓GC回收這些本地函數中可能引用到的 Java 對象。

無論是局部引用還是全局引用,其實都是通過句柄進行引用。其中,局部引用所對應的句柄有兩種存儲方式,一是在本地方法棧幀中,主要用於存放 C 函數所接收的來自 Java 層面的引用類型參數;另一種則是線程私有的句柄塊,主要用於存放本地函數運行過程中創建的局部引用(實際是通過JNI函數來完成來這些操作)。無論是傳入的引用類型參數,還是通過JNI函數(除NewGlobalRefNewWeakGlobalRef之外)返回的引用類型對象,都屬於局部引用。

關於句柄我們不應該陌生,在《深入剖析Java虛擬機:源碼剖析與實例詳解(基礎卷)》一書中詳細介紹過,Java棧在引用Java堆中的對象時會通過句柄的方式來引用,句柄指的是內存中 Java 對象的指針的指針。同時也介紹了HandleMark、HandleArea與Chunk這幾個類的用法,它是為解決JVM內部的本地代碼引用情況。當發生垃圾回收時,如果 Java 對象被移動了,那麼句柄指向的指針值也將發生變動,但句柄本身保持不變。

HotSpot VM的JNI句柄是放在若干不同的區域里的,但不會放在GC堆中。傳遞參數用的句柄直接在棧上;局部句柄放在每個Java線程中的JNIHandleBlock里;全局句柄放在HotSpot VM全局的JNIHandleBlock里。

JNIHandles類的定義如下:

源代碼位置:openjdk/hotspot/src/share/vm/runtime/jniHandles.hpp
 
class JNIHandles : AllStatic {
 private:
  // 保存全局引用的JNIHandleBlock鏈表的頭元素
  static JNIHandleBlock*  _global_handles;    
  // 保存全局弱引用的JNIHandleBlock鏈表的頭元素
  static JNIHandleBlock*  _weak_global_handles;
  static oop              _deleted_handle;                    
  ...
}
 

調用JNIHandles類的initialize()函數初始化如上的屬性,如下:

void JNIHandles::initialize() {
  _global_handles      = JNIHandleBlock::allocate_block();
  _weak_global_handles = JNIHandleBlock::allocate_block();
  // 宏擴展為如下的形式:
  // Thread*        __the_thread__ = 0;
  // ExceptionMark  __em(__the_thread__);
  EXCEPTION_MARK;

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

HotSpot VM會在啟動時調用init_globals()函數初始化全局模塊,init_globals()函數會間接調用到JNIHandles::initialize()函數,在這個函數中對全局的變量分配對應的JNIHandleBlock塊。所以說,全局對象的句柄存儲在JNIHandleBlock中。

JNIHandle分為兩種,全局和局部對象引用,大部分的對象引用屬於局部對象引用,最終還是調用了JNIHandleBlock來管理,因為JNIHandle沒有設計一個JNIHandleMark的機制,所以在創建局部對象引用時需要明確調用JNIHandles::mark_local()函數,在回收時也需要明確調用JNIHandles::destroy_local()函數。

在線程中定義的、與局部引用對象相關的變量如下:

// 保存活躍的JNIHandleBlock塊,塊中存儲的句柄對象也是活躍的
JNIHandleBlock* _active_handles;
 
// 保存空閑JNIHandleBlock塊,在必要時進行重用
JNIHandleBlock* _free_handle_block;
 
HandleMark* _last_handle_mark;

無論是全局還是局部對象引用,其句柄都存儲在JNIHandleBlock塊中。當需要分配一個新的塊時,調用JNIHandleBlock::allocate_block()函數;當不需要塊時,調用JNIHandleBlock::release_block()來釋放JNIHandleBlock塊。其中分配新塊和釋放塊的操作的最典型應用就是在JavaCallWrapper類的構造函數和析構函數中,這個JavaCallWrapper我們在之前接觸過,就是在介紹HotSpot VM調用Java主類的main()方法時,會調用到JavaCalls::call_helper()函數,這個函數中有如下調用:

{
    // 使用JavaCallWrapper保存相關信息
    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)

這個link會從C/C++函數調用到Java方法時,存儲到棧上,如下圖所示。

其中的call wrapper就是保存的link值。

其實任何從C/C++調用到Java方法時都會在C/C++的棧幀中保存call wrapper,其中保存的信息非常重要,因為寄生在C/C++棧中的C/C++函數和Java方法對應的棧幀混合在一起,我們有時候要遍歷C/C++棧幀,有時候需要遍歷Java棧幀,當C/C++函數或Java函數執行完成後,還要能正確恢復調用者的棧幀信息並執行,這裡我們不對這些內容做過多介紹,我們只關心C/C++函數使用的局部變量句柄即可。 

如上圖所示,在第1個C/C++棧幀(非當前執行的函數對應的C/C++棧幀)中可通過call wrapper找到JavaCallWrapper,然後通過JavaCallWrapper::_handles找到之前使用的JNIHandleBlock單鏈表,這樣就能遍歷到之前的C/C++棧幀中引用的堆中對象了。在第2個C/C++棧幀(當前正在執行的函數)中,通過Thread::_active_handles就能找到當前使用的JNIHandleBlock單鏈表,這樣就能遍歷引用的堆中對象了。對於Java棧引用的堆中對象來說,在《深入剖析Java虛擬機:源碼剖析與實例詳解(基礎卷)》一書中介紹過,可通過HandleMark、HandleArea與Chunk等進行管理。

如果發生GC,那麼需要遍歷線程中的所有C/C++棧找到所有使用的JNIHandleBlock塊,這樣才能不產生漏標現象。

在JavaCallWrapper類中有如下屬性定義:

JNIHandleBlock*  _handles; // 實際保存JNI引用的內存塊的指針

在JavaCallWrapper構造函數中有如下實現: 

JavaCallWrapper::JavaCallWrapper(
 methodHandle  callee_method, 
 Handle        receiver, 
 JavaValue*    result, 
 TRAPS
) {
  JavaThread* thread = (JavaThread *)THREAD;
  // ...
  
  // 分配一個新的JNIHandleBlock
  JNIHandleBlock* new_handles = JNIHandleBlock::allocate_block(thread);
  
  // ...
  
  _thread       = (JavaThread *)thread;
  // 保存當前線程的active_handles
  _handles      = _thread->active_handles();   
  
  
  // 將新分配的JNIHandleBlock作為線程的active_handles
  _thread->set_active_handles(new_handles);     
}

無論是全局變量還是局部變量,都需要分配調用JNIHandleBlock::allocate_block()函數分配JNIHandleBlock。JNIHandleBlock類的定義如下:

class JNIHandleBlock : public CHeapObj<mtInternal> {
 private:
  enum SomeConstants {
    // 每個JNIHandleBlock中只能分配出32個句柄,所以只能存儲32個oop
     block_size_in_oops  = 32                    
  };
 
  // 句柄中保存的是oop,本地函數只能通過句柄來操作oop
  oop               _handles[block_size_in_oops];
  // 下一個沒有使用的_handles中的slot,可以在這個slot上存儲oop,
  // 然後返回此slot的地址給本地函數進行操作
  int               _top;                        
  // 通過_next字段將所有的JNIHandleBlock連接成單鏈表
  JNIHandleBlock*   _next;                        
 
  // 指向JNIHandleBlock鏈表中的最後一個塊,這個塊中的_handles正在負責為當前線程分配句柄區域
  JNIHandleBlock*   _last;                 
  JNIHandleBlock*   _pop_frame_link;              
 
  // 將空閑的句柄區域通過列表連接起來 
  oop*              _free_list; 
            
  // 將空閑的JNIHandleBlock通過如下字段連接成單鏈表,注意這是
  // 一個靜態變量,所以這個列表保存的JNIHandleBlock塊可被任何線程重用
  static JNIHandleBlock* _block_free_list;      
  // ...
}

其中各個屬性的說明如下圖所示。

注意,在線程中分配局部變量的句柄時,會從_last指向的JNIHandleBlock塊的_handles數組中分配,如果top已經指向了_handles數組的下一個位置,則表示此數組已經無法分配出額外的句柄空間,需要調用JNIHandleBlock::allocate_block()函數分配一個新的JNIHandleBlock並連接到單鏈表中。

在JavaCallWrapper::JavaCallWrapper()構造函數中調用的JNIHandleBlock類的allocate_block()函數的實現如下: 

JNIHandleBlock* JNIHandleBlock::allocate_block(Thread* thread)  {
  JNIHandleBlock* block;
 
  // 如果當前線程的Thread::_free_handle_block中有空閑
  // 的JNIHandleBlock,則從空閑的列表中獲取即可
  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) {
       // 如果空閑列表中沒有空閑的JNIHandleBlock,則分配一個新的JNIHandleBlock
// JNIHandleBlock的內存是通過調用os::malloc()函數進行分配的 block = new JNIHandleBlock(); _blocks_allocated++; if (ZapJNIHandleArea) block->zap(); } else { // 從JNIHandleBlock::_block_free_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; }

如上函數會在線程啟動時調用,如在VMThread::run()、WatcherThread::run()和JavaThread::run()函數中調用,因為這幾個函數都可能會執行native方法。當從線程的_free_handle_block和JNIHandleBlock::__block_free_list列表中都無法分配出空閑的JNIHandleBlock塊時,就需要通過new關鍵字創建新的JNIHandleBlock了,JNIHandleBlock繼承自CHeapObj<mtInternal>,所以會通過調用os::malloc()函數從本地內存中分配塊的內存。

JavaCallWrapper::~JavaCallWrapper()析構函數的實現如下:

JavaCallWrapper::~JavaCallWrapper() {
  // 校驗執行析構的是同一個Java線程
  assert(_thread == JavaThread::current(), "must still be the same thread");
  
  // 獲取當前線程的active_handles
  JNIHandleBlock *_old_handles = _thread->active_handles();
  // 恢復方法調用前的active_handles
  _thread->set_active_handles(_handles);
  
  // ...
  
  // 釋放方法調用中新分配的JNIHandleBlock
  JNIHandleBlock::release_block(_old_handles, _thread);
}

析構函數在Java方法返回到C/C++函數時調用,調用JNIHandleBlock::release_block()函數就相當於在釋放本地函數棧幀中的句柄。所以我們也能看到,一旦從本地函數中返回到Java 方法中,那麼局部引用將失效。也就是說,垃圾回收器在標記垃圾時不再考慮這些局部引用。這就意味着,我們不能緩存局部引用,以供另一個線程或下一次 native 方法調用時使用。對於這種應用場景,我們需要藉助 JNI 函數NewGlobalRef,將該局部引用轉換為全局引用,以確保其指向的 Java 對象不會被垃圾回收。相應的,我們還可以通過 JNI 函數DeleteGlobalRef來消除全局引用,以便回收被全局引用指向的 Java 對象。

調用的release_block()函數的實現如下:

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);
 
    // 將新的空閑塊添加到列表頭部,其它的空閑塊添加到列表尾部
    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();
      //  如果函數傳入的參數thread為NULL,那麼會將block連接到靜態變量
      // _block_free_list列表中
      JNIHandleBlock* next = block->_next;
      block->_next = _block_free_list;
      _block_free_list = block;
      block = next;
    }
  }
  // ...
} 

當線程不為NULL時,將空閑的JNIHandleBlock連接到Thread::_free_handle_block上,否則連接到JNIHandleBlock::_block_free_list上。一般來說,線程使用的JNIHandleBlock如果空閑了,都會連接到Thread::_free_handle_block上,但是當線程退出或者ClassLoaderData::_handles(用來對已經連接的對象的引用,之前介紹過)卸載時會歸還給JNIHandleBlock::_block_free_list,這樣其它的線程也能使用這些空閑的JNIHandleBlock,不像Thread::_free_handle_block一樣,只能在本線程內重用。 

 公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源代碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流