jvm源码阅读笔记[5]:内存分配失败触发的GC究竟对内存做了什么?

    从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨
    个人源码地址:https://github.com/FlashLightNing/openjdk-notes
    还有一个openjdk6,7,8,9的地址:https://github.com/dmlloyd/openjdk
    jvm源码阅读笔记[1]:如何触发一次CMS回收
    jvm源码阅读笔记[2]:你不知道的晋升阈值TenuringThreshold详解
    jvm源码阅读笔记[3]:从内存分配到触发GC的细节
    jvm源码阅读笔记[4]:从GC说到vm operation
    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的具体流程。
    

    从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨
    个人源码地址:https://github.com/FlashLightNing/openjdk-notes
    还有一个openjdk6,7,8,9的地址:https://github.com/dmlloyd/openjdk
    jvm源码阅读笔记[1]:如何触发一次CMS回收
    jvm源码阅读笔记[2]:你不知道的晋升阈值TenuringThreshold详解
    jvm源码阅读笔记[3]:从内存分配到触发GC的细节
    jvm源码阅读笔记[4]:从GC说到vm operation
    jvm源码阅读笔记[5]:内存分配失败触发的GC究竟对内存做了什么?

你可能感兴趣的:(jvm,垃圾回收)