1. 程式人生 > >jvm原始碼閱讀筆記[5]:記憶體分配失敗觸發的GC究竟對記憶體做了什麼?

jvm原始碼閱讀筆記[5]:記憶體分配失敗觸發的GC究竟對記憶體做了什麼?

    
    在第3篇文章中,我們總結到,當分配記憶體失敗時,會通過VM觸發一次由分配失敗觸發的一次GC,也就是我們經常能在GC日誌裡面看到的“allocation failure”

VM_GenCollectForAllocation op(size, is_tlab, gc_count_before);//VM操作
    VMThread::execute(&op);

    同時,在我們第4篇中也簡單介紹了VMThread和VMOperation的原理和作用,寫到每個VM操作的具體實現邏輯都是在它的doit()方法上。那麼今天我們就來看看VM_GenCollectForAllocation的具體GC的過程和步驟。來看看vmGCOperations.cpp:

void VM_GenCollectForAllocation::doit() {
  SvcGCMarker sgcm(SvcGCMarker::MINOR);

  GenCollectedHeap* gch = GenCollectedHeap::heap();
  GCCauseSetter gccs(gch, _gc_cause);
  //通知記憶體堆管理器處理一次記憶體分配失敗  
  _res = gch->satisfy_failed_allocation(_size, _tlab);//res=分配的結果
  assert(gch->is_in_reserved_or_null(_res), "result not in heap"
); if (_res == NULL && GC_locker::is_active_and_needs_gc()) { set_gc_locked(); } }

    
    方法很短,精華內容應該就是在gch->satisfy_failed_allocation(_size, _tlab)一行了。
    


HeapWord* GenCollectorPolicy::satisfy_failed_allocation(size_t size,
                                                        bool   is_tlab) {
  GenCollectedHeap *gch = GenCollectedHeap::heap();
  GCCauseSetter x(gch, GCCause::_allocation_failure);
  HeapWord* result = NULL;

  assert
(size != 0, "Precondition violated"); if (GC_locker::is_active_and_needs_gc()) {//表示有jni在操作記憶體,此時不能進行GC避免改變物件在記憶體的位置。詳見第3篇文章。 if (!gch->is_maximal_no_gc()) {//儘量不gc result = expand_heap_and_allocate(size, is_tlab);//擴堆 } return result; } else if (!gch->incremental_collection_will_fail(false /* don't consult_young */)) { gch->do_collection(false /* full */, false /* clear_all_soft_refs */, size /* size */, is_tlab /* is_tlab */, number_of_generations() - 1 /* max_level */); } else { if (Verbose && PrintGCDetails) { gclog_or_tty->print(" :: Trying full because partial may fail :: "); } // 做一次full gc gch->do_collection(true /* full */, false /* clear_all_soft_refs */, size /* size */, is_tlab /* is_tlab */, number_of_generations() - 1 /* max_level */); } //GC完再嘗試分配 result = gch->attempt_allocation(size, is_tlab, false /*first_only*/); if (result != NULL) { assert(gch->is_in_reserved(result), "result not in heap"); return result; } //如果GC完還分配失敗,看看能否進行擴容和分配 result = expand_heap_and_allocate(size, is_tlab); if (result != NULL) { return result; } { UIntFlagSetting flag_change(MarkSweepAlwaysCompactCount, 1); // Make sure the heap is fully compacted /* 最後再進行一次full gc,同時清除軟引用 */ gch->do_collection(true /* full */, true /* clear_all_soft_refs */, size /* size */, is_tlab /* is_tlab */, number_of_generations() - 1 /* max_level */); } /* 要是再分配不了,只能報OOM了。。。 */ result = gch->attempt_allocation(size, is_tlab, false /* first_only */); if (result != NULL) { assert(gch->is_in_reserved(result), "result not in heap"); return result; } assert(!should_clear_all_soft_refs(), "Flag should have been handled and cleared prior to this point"); return NULL; }

    總結起來,大致記憶體分配失敗觸發的GC分為以下幾個過程

  1. .判斷此時是否有JNI在操作,若有,則只能通過擴堆然後分配解決,不能進行GC(原因詳見第3篇文章)。
  2. 若無jni操作,則判斷!gch->incremental_collection_will_fail條件。若true,則只是進行一次young gc。若false,則會觸發一次fullgc(該次不會清除軟引用)。
  3. 嘗試進行一次記憶體分配。若成功則返回,若失敗,則試著看看能否擴容並分配
  4. 若仍失敗,最後進行一次full gc,此時會清除軟引用。GC完後進行分配。
  5. 若還是失敗…只能OOM了。

    
    再來解釋一下gch->incremental_collection_will_fail是幹嘛的?
    

bool incremental_collection_will_fail(bool consult_young) {
     assert(heap()->collector_policy()->is_two_generation_policy(),
           "the following definition may not be suitable for an n(>2)-generation system");
    return incremental_collection_failed() ||
           (consult_young && !get_gen(0)->collection_attempt_is_safe());
  }

    
    consult_young=true的時候,表示呼叫該方法時,判斷此時晉升是否的安全的。若=false,表示只取上次young gc時設定的引數,此次不再進行額外的判斷

    簡單來講,就是

在young gc時可能存在晉升失敗的風險。老年代最大的連續可用的空間>之前的平均晉升大小,或者>年輕代使用的空間大小,則被認為是安全的。反之則是不安全

    若在執行第2步的ygc時(也就是之前的ygc沒有設定晉升失敗的標記),young gen在進行gc時判斷了晉升情況,認為不安全,就會快速返回,從而讓jvm執行第4步中的full gc。這樣就達到了在晉升可能失敗的情況下,由fullgc 來接替young gc的目的。當進行fullgc或者一次background式的GC後(詳見第1篇文章),incremental_collection_will_fail標誌就會清除。
    
    打個比方:當系統剛開始執行後,分配了許多物件後,記憶體空間不夠了,第一次觸發一次GC。假設此時沒有jni,則判斷上次ygc是否設定了晉升不安全的標記。因為是此時是第一次觸發,所以標記位是預設值(安全的)。那麼jvm就試著開始一次ygc了。在ygc的過程中,young gen發現此時老年代放不下年輕代晉升的物件,就直接return,不進行下一步的操作了。方法return之後,走到第3步分配。分配失敗然後看看能否擴堆。發現都不行,再執行第4步進行一次full gc(同時清除了晉升不安全的標記),完了之後再分配。 這樣就走完了一次完整的分配失敗觸發GC的過程。
    以下是對此部分流程的圖解。圖片比較長,分成2部分….
    


    
    下一篇文章將寫一下圖中的進行young gc和進行full gc的具體流程。