之前我們已經介紹了JNIHandleBlock,但是沒有具體介紹JNIHandleBlock中存儲的句柄,這一篇我們將詳細介紹對這些句柄的操作。
JNI句柄分為兩種,全局和局部對象引用:
(1)大部分對象的引用屬於局部對象引用,最終還是調用了JNIHandleBlock來管理,因為JNIHandle沒有設計一個JNIHandleMark的機制,所以在創建時需要明確調用JNIHandles::make_local()函數,在回收時也需要明確調用JNIHandles::destory_local()函數;
(2)對於全局對象的引用,比如在編譯任務compilerTask中會訪問Method實例,這時候就需要把這些實例設置為全局的(否則在GC時可能會被回收)。
下面我們就來詳細介紹一下局部對象的引用和全局對象的引用。
1、局部對象引用
當我們編寫本地函數時,可能會調用JNI函數獲取父類,獲取父類的JNI函數jni_GetSuperclass()的實現如下:
JNI_ENTRY(jclass, jni_GetSuperclass(JNIEnv *env, jclass sub))
JNIWrapper("GetSuperclass");
jclass obj = NULL;
// ...
obj = (super == NULL) ? NULL : (jclass) JNIHandles::make_local(super->java_mirror());
return obj;
JNI_END
也就是獲取sub類的父類時,如果查詢到的父類super不為null,則在返回時需要返回句柄,此時就會調用JNIHandles::make_local()函數。調用的JNIHandles::make_local()函數的實現如下:
jobject JNIHandles::make_local(Thread* thread, oop obj) {
if (obj == NULL) {
return NULL;
} else {
assert(Universe::heap()->is_in_reserved(obj), "sanity check");
return thread->active_handles()->allocate_handle(obj);
}
}
jobject JNIHandles::make_local(JNIEnv* env, oop obj) {
if (obj == NULL) {
return NULL;
} else {
JavaThread* thread = JavaThread::thread_from_jni_environment(env);
// 確保obj是堆上的對象,因為只有堆上的對象才會移動,才會被GC回收
assert(Universe::heap()->is_in_reserved(obj), "sanity check");
return thread->active_handles()->allocate_handle(obj);
}
}
obj是堆中的對象,現在本地函數要通過句柄訪問這個對象,此時會調用JNIHandles::make_local()函數創建出對應的句柄,這個句柄的內存空間是從JNIHandleBlock中分配出來的。
調用的JavaThread::thread_from_jni_environment()函數的實現如下 :
static JavaThread* thread_from_jni_environment(JNIEnv* env) {
// 直接通過地址偏移得到JavaThread*,這是因為
// JavaThread類中定義了JNIEnv類型的字段
JavaThread *thread_from_jni_env = (JavaThread*)(
(intptr_t)env - in_bytes(jni_environment_offset())
);
// 最后檢查線程是否已經終止狀態,沒有終止才返回該線程對象
if (thread_from_jni_env->is_terminated()) {
thread_from_jni_env->block_if_vm_exited();
return NULL;
} else {
return thread_from_jni_env;
}
}
在JNIHandles::make_local()函數中調用的JNIHandleBlock::allocate_handle()函數進行句柄分配。
Java線程使用一個對象句柄存儲塊JNIHandleBlock來為其在本地函數中申請的臨時對象創建對應的句柄。具體就是調用JNIHandleBlock::allocate_handle()函數分配句柄,此函數的實現如下:
jobject JNIHandleBlock::allocate_handle(oop obj) {
assert(Universe::heap()->is_in_reserved(obj), "sanity check");
if (_top == 0) {
for (JNIHandleBlock* current = _next;
current != NULL;
current = current->_next
){
assert(current->_last == NULL, "only first block should have _last set");
assert(current->_free_list == NULL,"only first block should have _free_list set");
current->_top = 0;
if (ZapJNIHandleArea)
current->zap();
}
// 重轉相關變量的值
_free_list = NULL;
_allocate_before_rebuild = 0;
_last = this;
if (ZapJNIHandleArea)
zap();
}
// 當前的JNIHandleBlock::_handles中能夠有空閑的slot分配句柄則分配后直接返回
if (_last->_top < block_size_in_oops) {
oop* handle = &(_last->_handles)[_last->_top++];
*handle = obj;
return (jobject) handle;
}
// 如果有空閑的句柄slot,則從列表中第1個空閑的句柄slot中分配句柄並返回
if (_free_list != NULL) {
oop* handle = _free_list;
_free_list = (oop*) *_free_list;
*handle = obj;
return (jobject) handle;
}
// 如果_last后有空閑的JNIHandleBlock,則從此JNIHandleBlock中分配
if (_last->_next != NULL) {
_last = _last->_next;
return allocate_handle(obj); // 遞歸調用
}
// 沒有空閑的JNIHandleBlock,並且當前的JNIHandleBlock也無法分配句柄需要的內容,則調用rebuild_free_list
if (_allocate_before_rebuild == 0) {
rebuild_free_list();
} else {
Thread* thread = Thread::current();
Handle obj_handle(thread, obj);
// 關於allocate_block()函數在之前已經介紹過
_last->_next = JNIHandleBlock::allocate_block(thread);
_last = _last->_next;
_allocate_before_rebuild--;
obj = obj_handle();
}
// 再次嘗試從當前的JNIHandleBlock中分配句柄需要的內存
return allocate_handle(obj); // 遞歸調用
}
當_allocate_before_rebuild的值為0時,會調用rebuild_free_list()函數重建_free_list列表,_free_list列表中的各個slot都是分布在各個已經使用了的JNIHandleBlock中的空閑slot,所以重建就意味着我們要掃描所有已經使用了的slot,然后找到空閑的slot連接到這個單鏈表中,最大限度的重用slot。
調用的rebuild_free_list()函數的實現如下:
void JNIHandleBlock::rebuild_free_list() {
assert(_allocate_before_rebuild == 0 && _free_list == NULL, "just checking");
int free = 0;
int blocks = 0;
for (JNIHandleBlock* current = this; current != NULL; current = current->_next) {
for (int index = 0; index < current->_top; index++) {
oop* handle = &(current->_handles)[index];
if (*handle == JNIHandles::deleted_handle()) {
// 找到了空閑的slot,連接到單鏈表上
*handle = (oop) _free_list;
_free_list = handle;
free++;
}
}
blocks++;
}
// 當已經使用過的slot中有超過一半的slot都是空閑的,那么我們其實偏向於不再重新分配JNIHandleBlock,
// 而是重用這些空閑slot;當空閑的slot少時,會計算出_allocate_before_rebuild值,這樣就會偏向於
// 從新的JNIHandleBlock中分配句柄
int total = blocks * block_size_in_oops;
int extra = total - 2*free;
if (extra > 0) {
// Not as many free handles as we would like - compute number of new blocks to append
_allocate_before_rebuild = (extra + block_size_in_oops - 1) / block_size_in_oops;
}
}
我們除了查找所有的空閑slot並連接到_free_list上之外,還要計算_allocate_before_rebuild值。如果計算出的_allocate_before_rebuild值大於0,那么我們查看JNIHandleBlock::allocate_handle()這個函數的邏輯,可以看到當無法分配句柄時,偏向於分配新的JNIHandleBlock,然后從JNIHandleBlock::_handles數組中分配句柄,否則從_free_list中重用空閑句柄,這就很好的避免了過度分配太多的JNIHandleBlock,如果過度分配太多的JNIHandleBlock,不但會加重GC掃描過程中的時間,也會占用更多的內存空間。
釋放句柄的函數如下:
inline void JNIHandles::destroy_local(jobject handle) {
if (handle != NULL) {
// 使用JNIHandles::_deleted_handle來初始化,這個值在
// JNIHandles::initialize()函數中會初始化為Object對象,用來
// 表示這個slot為空,沒有存儲對象
*((oop*)handle) = deleted_handle();
}
}
代碼實現非常簡單,這里不再詳細介紹。
2、全局對象引用
在分配全局變量時,調用如下函數:
jobject JNIHandles::make_global(Handle obj) {
jobject res = NULL;
if (!obj.is_null()) {
MutexLocker ml(JNIGlobalHandle_lock);
assert(Universe::heap()->is_in_reserved(obj()), "sanity check");
res = _global_handles->allocate_handle(obj()); // 同樣調用allocate_handle()函數處理
}
return res;
}
在分配全局對象引用時,同樣會調用allocate_handle()句柄,實現相對局部對象引用來說比較簡單,通過一個全局的_global_handles來保存一個JNIHandleBlock的單鏈表,這個單鏈表中的JNIHandleBlock節點只增不減,所以如果某個時間點,全局對象引用足夠多時會為GC帶來不小負擔,尤其是忘記釋放全局對象引用會引起內存泄漏。
調用JNIHandles::destroy_global()函數釋放全局對象引用:
void JNIHandles::destroy_global(jobject handle) {
if (handle != NULL) {
*((oop*)handle) = deleted_handle();
}
}
函數的實現非常簡單,這里不再介紹。
公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源代碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流
