第42篇-JNI引用的管理(1)
- 2021 年 12 月 3 日
- 筆記
在本地函數中會使用Java服務,這些服務都可以通過調用JNIEnv中封裝的函數獲取。我們在本地函數中可以訪問所傳入的引用類型參數,也可以通過JNI函數創建新的 Java 對象。這些 Java 對象顯然也會受到GC的影響。所以我們需要通過JNI 的局部引用(Local Reference)和全局引用(Global Reference)來保證不讓GC回收這些本地函數中可能引用到的 Java 對象。
無論是局部引用還是全局引用,其實都是通過句柄進行引用。其中,局部引用所對應的句柄有兩種存儲方式,一是在本地方法棧幀中,主要用於存放 C 函數所接收的來自 Java 層面的引用類型參數;另一種則是線程私有的句柄塊,主要用於存放本地函數運行過程中創建的局部引用(實際是通過JNI函數來完成來這些操作)。無論是傳入的引用類型參數,還是通過JNI函數(除NewGlobalRef
及NewWeakGlobalRef
之外)返回的引用類型對象,都屬於局部引用。
關於句柄我們不應該陌生,在《深入剖析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,拉你入虛擬機群交流