GC part 7

part 7

UseConcMarkSweepGC下的内存申请流程分析

-XX:+UseConcMarkSweepGC俗称CMS,是一种减少GC停顿时间的堆管理方案,使用的堆管理器是GenCollectedHeap,新生代堆类型是ParNew,老年代是ConcurrentMarkSweepGeneration,新生代使用多线程版本的copy算法来进行垃圾收集,将新生代分为Eden + From + To三个空间区域;老年代使用CMS来进行周期性的垃圾收集,可以通过设置CMSInitiatingOccupancyFraction来让CMS检测是否需要进行一次CMS GC,CMSInitiatingOccupancyFraction的默认值为92%,也就是如果老年代的空间使用占了92%,那么就会进行一次CMS GC,这个默认值是计算出来的:

void ConcurrentMarkSweepGeneration::init_initiating_occupancy(intx io, uintx tr) {
  assert(io <= 100 && tr <= 100, "Check the arguments");
  if (io >= 0) {
    _initiating_occupancy = (double)io / 100.0;
  } else {
    _initiating_occupancy = ((100 - MinHeapFreeRatio) +
                             (double)(tr * MinHeapFreeRatio) / 100.0)
                            / 100.0;
  }
}

参数io是CMSInitiatingOccupancyFraction,trCMSTriggerRatio;是如果设置了CMSInitiatingOccupancyFraction,那么_initiating_occupancy就是(double)io / 100.0,否则通过else分支中的计算分支来计算,假设没有设置CMSTriggerRatio,默认就是80,MinHeapFreeRatio是40;那么计算结果就是0.92;CMS的GC分为background gc和foreground gc,前者是CMS线程进行不但检测是否需要进行CMS GC来实现垃圾回收的,属于后台任务;而后者是被"Allocation Fail"或者“Promotion Fail”触发的,是一种主动的GC,而主动GC是要全程STW的,在实现上使用了SerialOld的策略,使用标记-清除-整理算法来进行整个堆空间的垃圾回收;关于CMS GC的详细细节另论,本文的重点在于UseConcMarkSweepGC下的对象内存分配策略探索。

在UseConcMarkSweepGC下对象依然首先在Eden中进行内存申请,UseConcMarkSweepGC新生代使用的是ParNew,是DefNew的子类,ParNew上的GC是DefNew上GC的多线程版本,在ParNew上进行空间分配应该也和DefNew差不多,下面来看看UseConcMarkSweepGC下内存分配的全流程。

因为前面的文章已经分析过对象在UseSerialGC下的内存申请流程,所以对于CMS的内存申请直接从CollectedHeap::common_mem_allocate_noinit函数开始看起,在UseSerialGC的时候也说过该函数,这个函数首先allocate_from_tlab函数来试图从TLAB申请空间,如果无法满足,那么就重新申请一块TLAB,申请一块TLAB和为对象申请空间的流程对于堆来说都是内存申请,所以后续的流程是一致的;如果通过TLAB无法申请到内存,那么就通过Universe::heap()->mem_allocate来直接在堆中申请内存,这个时候就要加锁了,因为堆面向的是所有线程,不像TLAB是线程私有的,所以会存在多线程竞争的问题,所以但愿TLAB可以有效;GenCollectorPolicy::mem_allocate_work将完成再堆中内存申请的流程,下面就主要来分析一下这个函数的具体实现。

    // First allocation attempt is lock-free.
    Generation *young = gch->young_gen();
    assert(young->supports_inline_contig_alloc(),
      "Otherwise, must do alloc within heap lock");
    if (young->should_allocate(size, is_tlab)) {
      result = young->par_allocate(size, is_tlab);
      if (result != NULL) {
        assert(gch->is_in_reserved(result), "result not in heap");
        return result;
      }
    }

young->should_allocate用于判断是否应该在新生代进行空间申请,大对象应该直接在老年代进行分配,如果不是大对象,那么就会通过young->par_allocate来进行空间申请,young->par_allocate使用的是DefNew的实现,ParNew继承了DefNew的young->par_allocate实现;

HeapWord* DefNewGeneration::par_allocate(size_t word_size,
                                         bool is_tlab) {
  HeapWord* res = eden()->par_allocate(word_size);
  if (CMSEdenChunksRecordAlways && _old_gen != NULL) {
    _old_gen->sample_eden_chunk();
  }
  return res;
}

可以看到是向Eden空间申请内存,具体实现时通过ContiguousSpace::par_allocate_impl来进行的,关于这块的内容前面的文章已经分析过,不再赘述,因为使用copying算法来进行垃圾回收,不会存在内存碎片问题,所以可以使用指针碰撞算法来进行空间分配,所谓指针碰撞就是使用一个top指针,来标记当前空闲内存的起始地址,分配一块size大小的内存空间的实现就是将top指针向前移动size即可实现;如果无法从Eden空间分配到内存,那么就要试图从From区域分配内存了,gch->attempt_allocation将实现先尝试从Eden区域申请内存,如果无法成功,那么尝试从From区域分配,如果还不可以,那么就从Old区域分配的逻辑,具体实现如下:

HeapWord* GenCollectedHeap::attempt_allocation(size_t size,
                                               bool is_tlab,
                                               bool first_only) {
  HeapWord* res = NULL;
  if (_young_gen->should_allocate(size, is_tlab)) {
    res = _young_gen->allocate(size, is_tlab);
    if (res != NULL || first_only) {
      return res;
    }
  }
  if (_old_gen->should_allocate(size, is_tlab)) {
    res = _old_gen->allocate(size, is_tlab);
  }
  return res;
}

_young_gen->allocate将会从From区域尝试申请内存:

HeapWord* DefNewGeneration::allocate(size_t word_size, bool is_tlab) {
  // This is the slow-path allocation for the DefNewGeneration.
  // Most allocations are fast-path in compiled code.
  // We try to allocate from the eden.  If that works, we are happy.
  // Note that since DefNewGeneration supports lock-free allocation, we
  // have to use it here, as well.
  HeapWord* result = eden()->par_allocate(word_size);
  if (result != NULL) {
    if (CMSEdenChunksRecordAlways && _old_gen != NULL) {
      _old_gen->sample_eden_chunk();
    }
  } else {
    // If the eden is full and the last collection bailed out, we are running
    // out of heap space, and we try to allocate the from-space, too.
    // allocate_from_space can't be inlined because that would introduce a
    // circular dependency at compile time.
    result = allocate_from_space(word_size);
  }
  return result;
}

eden()->par_allocate将从Eden区域申请内存,如果无法满足,那么就通过allocate_from_space从From区域进行内存分配;

// The last collection bailed out, we are running out of heap space,
// so we try to allocate the from-space, too.
HeapWord* DefNewGeneration::allocate_from_space(size_t size) {
  bool should_try_alloc = should_allocate_from_space() || GCLocker::is_active_and_needs_gc();

  // If the Heap_lock is not locked by this thread, this will be called
  // again later with the Heap_lock held.
  bool do_alloc = should_try_alloc && (Heap_lock->owned_by_self()
                                       || (SafepointSynchronize::is_at_safepoint()
                                           && Thread::current()->is_VM_thread()));
  HeapWord* result = NULL;
  if (do_alloc) {
    result = from()->allocate(size);
  }
  return result;
}

当然,需要判断是否允许在From区域进行内存分配,如果不允许,那么还是无法在From区域进行分配;should_allocate_from_space将完成这个判断,当然,如果当前有线程在进行GC,那么是运行从From区域进行内存分配的,下面看看should_allocate_from_space函数的具体判断逻辑:

  bool should_allocate_from_space() const {
    return _should_allocate_from_space;
  }

  void clear_should_allocate_from_space() {
    _should_allocate_from_space = false;
  }
  void set_should_allocate_from_space() {
    _should_allocate_from_space = true;
  }

很简单,直接返回_should_allocate_from_space的值,所以来看看在什么时候设置了该值即可找到判断逻辑:

image

判断条件还是比较严格的,首先collection_attempt_is_safe是true,并且Eden已经满了,collection_attempt_is_safe函数的实现如下:

bool DefNewGeneration::collection_attempt_is_safe() {
  if (!to()->is_empty()) {
    log_trace(gc)(":: to is not empty ::");
    return false;
  }
  if (_old_gen == NULL) {
    GenCollectedHeap* gch = GenCollectedHeap::heap();
    _old_gen = gch->old_gen();
  }
  return _old_gen->promotion_attempt_is_safe(used());
}

如果To区域不为空,那么就直接不可以在From区域进行分配,To区域不为空就说明发生了“Promotion Fail”,如果没有发生过“Promotion Fail”,那么判断晋升是否是安全的,通过_old_gen->promotion_attempt_is_safe函数来实现:

bool ConcurrentMarkSweepGeneration::promotion_attempt_is_safe(size_t max_promotion_in_bytes) const {
  size_t available = max_available();
  size_t av_promo  = (size_t)gc_stats()->avg_promoted()->padded_average();
  bool   res = (available >= av_promo) || (available >= max_promotion_in_bytes);
  return res;
}

available是老年代可用内存大小,av_promo是新生代评价晋升对象大小,max_promotion_in_bytes是新生代的使用量(Eden + From),所以,如果老年代的可用空间大于新生代评价晋升对象大小,或者大于新生代的使用量,那么就说明年轻代晋升是安全的,否则就是不安全的;

总结一下,如果当前有线程在进行GC,或者Eden区域已经满了,或者老年代判断晋升是安全的,那么就运行在From区域进行分配,否则只能到老年代去分配了;
如果新生代(包括从Eden和From区域)无法申请到内存的话,那么就要去老年代试试了,_old_gen->should_allocate首先判断是否可以在老年代进行内存申请,如果允许,那么就通过_old_gen->allocate函数来申请内存,下面先来看看_old_gen->should_allocate的实现;

  // Returns "true" iff this generation should be used to allocate an
  // object of the given size.  Young generations might
  // wish to exclude very large objects, for example, since, if allocated
  // often, they would greatly increase the frequency of young-gen
  // collection.
  virtual bool should_allocate(size_t word_size, bool is_tlab) {
    bool result = false;
    size_t overflow_limit = (size_t)1 << (BitsPerSize_t - LogHeapWordSize);
    if (!is_tlab || supports_tlab_allocation()) {
      result = (word_size > 0) && (word_size < overflow_limit);
    }
    return result;
  }

如果上述函数判断是true,那么就通过_old_gen->allocate来从老年代申请内存:

HeapWord* ConcurrentMarkSweepGeneration::allocate(size_t size, bool tlab) {
  CMSSynchronousYieldRequest yr;
  MutexLockerEx x(freelistLock(), Mutex::_no_safepoint_check_flag);
  return have_lock_and_allocate(size, tlab);
}

HeapWord* ConcurrentMarkSweepGeneration::have_lock_and_allocate(size_t size,
                                                                bool   tlab /* ignored */) {
  assert_lock_strong(freelistLock());
  size_t adjustedSize = CompactibleFreeListSpace::adjustObjectSize(size);
  HeapWord* res = cmsSpace()->allocate(adjustedSize);
  // Allocate the object live (grey) if the background collector has
  // started marking. This is necessary because the marker may
  // have passed this address and consequently this object will
  // not otherwise be greyed and would be incorrectly swept up.
  // Note that if this object contains references, the writing
  // of those references will dirty the card containing this object
  // allowing the object to be blackened (and its references scanned)
  // either during a preclean phase or at the final checkpoint.
  if (res != NULL) {
    // We may block here with an uninitialized object with
    // its mark-bit or P-bits not yet set. Such objects need
    // to be safely navigable by block_start().
    assert(oop(res)->klass_or_null() == NULL, "Object should be uninitialized here.");
    assert(!((FreeChunk*)res)->is_free(), "Error, block will look free but show wrong size");
    collector()->direct_allocated(res, adjustedSize);
    _direct_allocated_words += adjustedSize;
    // allocation counters
    NOT_PRODUCT(
      _numObjectsAllocated++;
      _numWordsAllocated += (int)adjustedSize;
    )
  }
  return res;
}

CompactibleFreeListSpace将会负责CMS老年代的内存分配工作,这里需要说一下的是,CMS老年代和DefNew或者ParNew都不一样,CMS老年代堆可能会产生内存碎片,所以无法使用指针碰撞算法来进行内存分配,CMS老年代使用了称为空闲列表(Free-List)的算法来管理老年代的内存,下面来看看CompactibleFreeListSpace的allocate函数的实现:

HeapWord* CompactibleFreeListSpace::allocate(size_t size) {
  HeapWord* res = NULL;
  res = allocate_adaptive_freelists(size);
  if (res != NULL) {
    FreeChunk* fc = (FreeChunk*)res;
    fc->markNotFree();
    // Verify that the block offset table shows this to
    // be a single block, but not one which is unallocated.
    _bt.verify_single_block(res, size);
    _bt.verify_not_unallocated(res, size);
  }
  return res;
}

allocate_adaptive_freelists函数将尽最大努力来找到一块合适的内存,这里面的流程也是非常复杂的,但是这里的实现像极了C++ STL中内存池的实现,所以如果有条件的话还是希望去分析一下C++ STL内存池的相关实现。下面来看看allocate_adaptive_freelists函数的具体实现。

image

这里顺便说一下,如果从Old区域中也无法满足申请要求,那么就得去通过expand_heap_and_allocate扩展堆再来allocate了,如果还不行,那么就执行进行GC了,VM_GenCollectForAllocation将会被放在VMThread中等待执行,具体执行Minor GC还是FullGC需要具体判断,这部分内容在前面的文章中分析过,就不再赘述,下面将详细分析CMS Free-List内存分配的实现细节,也就是allocate_adaptive_freelists函数的具体实现细节。

HeapWord* CompactibleFreeListSpace::allocate_adaptive_freelists(size_t size) {
  assert_lock_strong(freelistLock());
  HeapWord* res = NULL;
  assert(size == adjustObjectSize(size),
         "use adjustObjectSize() before calling into allocate()");

  // Strategy
  //   if small
  //     exact size from small object indexed list if small
  //     small or large linear allocation block (linAB) as appropriate
  //     take from lists of greater sized chunks
  //   else
  //     dictionary
  //     small or large linear allocation block if it has the space
  // Try allocating exact size from indexTable first
  if (size < IndexSetSize) {
    res = (HeapWord*) getChunkFromIndexedFreeList(size);
    if(res != NULL) {
      assert(res != (HeapWord*)_indexedFreeList[size].head(),
        "Not removed from free list");
      // no block offset table adjustment is necessary on blocks in
      // the indexed lists.

    // Try allocating from the small LinAB
    } else if (size < _smallLinearAllocBlock._allocation_size_limit &&
        (res = getChunkFromSmallLinearAllocBlock(size)) != NULL) {
        // if successful, the above also adjusts block offset table
        // Note that this call will refill the LinAB to
        // satisfy the request.  This is different that
        // evm.
        // Don't record chunk off a LinAB?  smallSplitBirth(size);
    } else {
      // Raid the exact free lists larger than size, even if they are not
      // overpopulated.
      res = (HeapWord*) getChunkFromGreater(size);
    }
  } else {
    // Big objects get allocated directly from the dictionary.
    res = (HeapWord*) getChunkFromDictionaryExact(size);
    if (res == NULL) {
      // Try hard not to fail since an allocation failure will likely
      // trigger a synchronous GC.  Try to get the space from the
      // allocation blocks.
      res = getChunkFromSmallLinearAllocBlockRemainder(size);
    }
  }

  return res;
}

CMS使用的Free-List分配算法策略复杂,当然复杂带来的好处是高效的内存分配速率;这一块内容日后再来整理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • part 7 UseConcMarkSweepGC下的内存申请流程分析 -XX:+UseConcMarkSweep...
    一字马胡阅读 256评论 0 0
  • part 4 JVM可以帮我们管理内存,这是一件非常有意义的事情,我们再也不用担心allocate出来的内存没有在...
    一字马胡阅读 297评论 0 2
  • part 5 本comment希望能系统的探索一下GC发生的时机,以及各个GC的具体工作内容(流程),GC包括Mi...
    一字马胡阅读 353评论 0 1
  • part 2 这个comment希望能分析一下GenCollectedHeap::do_collection这个函...
    一字马胡阅读 502评论 0 0
  • part 4 JVM可以帮我们管理内存,这是一件非常有意义的事情,我们再也不用担心allocate出来的内存没有在...
    一字马胡阅读 222评论 0 0