JVM源碼分析之GC locker深度分析

簡書 占小狼
轉載請注明原創出處,謝謝!

GC locker是什么?

概念太抽象,一兩句話說不清...

先看看它被使用的場景,jni.cpp類中有一對函數,分別是jni_GetStringCriticaljni_ReleaseStringCritical,看名字也知道一個Get,一個Release,有點像鎖的感覺。

當使用本地方法JNI函數訪問JVM中的字符串或數組數據時,就需要用到上面的兩個函數,如下是JNI讀取JVM中的字符串實現:

實現步驟:
1、從JVM返回字符串指針
2、通過MALLOC_MIN4分配內存
3、如果內存不足,拋出OOM異常
4、拷貝字符串的值
5、執行ReleaseStringCritical

這里GetStringCriticalReleaseStringCritical有何用?

這兩個函數之間的代碼必須在"critical region"(臨界區)執行,即在調用GetStringCritical函數開始,到數據復制完成期間,必須保證原始數據不被修改,那應該如何防止其它線程的操作、或發生GC回收改字符串對象?

GetStringCritical函數實現:

可以發現,在返回字符串指針之前執行了GC_locker::lock_critical(thread);對臨界區進行了加鎖。

GC_locker::lock_critical(thread)方法實現:

每個線程都有一個參數_jni_active_critical,記錄當前進入的臨界區個數,如果_jni_active_critical > 0,說明該線程已經在臨界區。

如果該線程還沒進入臨界區,且_need_gc標識為true,則執行jni_lock方法。_need_gc默認為false,只有在特殊情況下才會被設置為true。

如果現在有線程在臨界區中,且_need_gc標識為true,則通過JNICritical_lock鎖阻塞線程等待,實現如下:

假設現在線程A執行了enter_critical(),第一次進入臨界區,這時不爭氣的JVM發生了YGC,而臨界區的代碼還沒有執行完,怎么辦?

我們先來看一段代碼,觸發年輕代的垃圾回收之前,會進行如下判斷:

if (GC_locker::check_active_before_gc()) {
    return; // GC is disabled (e.g. JNI GetXXXCritical operation)
}

check_active_before_gc()的實現:

如果發現有線程進入了臨界區,本次的GC需要被disabled(丟棄),并設置_need_gc為true,這正好和前面的對應起來。

技巧:通過添加-XX+PrintJNIGCStalls可以打印出進入臨界區的線程信息。

因為有線程進入了臨界區,導致本次觸發的YGC被丟棄了,那內存分配時空間不足的問題怎么解決?

線程執行完臨界區的代碼之后,執行ReleaseStringCritical方法,離開臨界區:

GC_locker::unlock_critical的方法實現:

如果這個臨界區是該線程最后一個臨界區,且_need_gc為true,就是上次YGC丟棄時設置的true,則執行jni_unlock方法,實現如下:

實現有點復雜,總之就是把進入臨界區時對一些字段進行+1操作,現在執行-1操作,如果發現已經沒有任何線程在臨界區了,說明該線程是最后一個離開臨界區的線程。

如果當前沒有其它地方持有CG locker的lock鎖,則觸發一次原因為GCCause::_gc_locker的GC,最后通過JNICritical_lock->notify_all()喚醒被阻塞在臨界區外面的線程。

collect(GCCause::Cause cause)的方法實現:

先通過should_do_concurrent_full_gc判斷是否可以使用并發GC,判斷邏輯如下:

bool GenCollectedHeap::should_do_concurrent_full_gc(GCCause::Cause cause) {
  return UseConcMarkSweepGC &&
         ((cause == GCCause::_gc_locker && GCLockerInvokesConcurrent) ||
          (cause == GCCause::_java_lang_system_gc && ExplicitGCInvokesConcurrent));
}

前提是使用CMS垃圾算法
1、GCCause::_gc_locker 對應 -XX+GCLockerInvokesConcurrent
2、GCCause::_java_lang_system_gc 對應 -XX+ExplicitGCInvokesConcurrent

如果開啟了-XX+GCLockerInvokesConcurrent,并發的實現如下:

VM_GenCollectFullConcurrent::doit()實現,這是GC開始的入口:

大吃一驚,居然有3情況,然后傻傻分不清楚:
1、只收集年輕代,如果你真的以為只收集年輕代,那就太年輕了,實現如下:

注意:如果_incremental_collection_failed標識為false,那么會執行一次真正意義的full gc,真的很慘!

2、觸發一次CMS,如何觸發?

設置_full_gc_requested參數為true,在周期性觸發CMS的條件中,包含了對該標識的判斷,如果為true,則會執行CMS

3、啥都不做

如果沒有開啟-XX+GCLockerInvokesConcurrent,則執行collect(cause, n_gens() - 1)

VM_GenCollectFull::doit()實現如下:

do_full_collection的邏輯和并發情況下只收集年輕代的邏輯差不多,正常情況下,也只會對年輕代收集,如果incremental_collection_will_fail返回true的話,就對年輕代和老年代都收集了。

這么分析下來,GC locker導致的GC問題,真是飄忽不定,頭有點暈...我前面都在說什么?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容