JVM源碼分析之不要被GC日志的表面現(xiàn)象迷惑

簡(jiǎn)書(shū) 占小狼
轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處,謝謝!

數(shù)組動(dòng)態(tài)擴(kuò)容導(dǎo)致頻繁FGC

關(guān)于數(shù)組動(dòng)態(tài)擴(kuò)容導(dǎo)致頻繁GC的問(wèn)題,笨神又寫了一篇文章分析,當(dāng)時(shí)因?yàn)闆](méi)有仔細(xì)看,導(dǎo)致還有一些疑惑,于是把垃圾回收算法的實(shí)現(xiàn)重新看了一遍,不過(guò)每次看都會(huì)有不小的收獲,所以源碼不是讀一遍就可以了,隔三差五的回頭看看,說(shuō)不定有些不懂的地方,在下一次的時(shí)候就豁朗了。

關(guān)于數(shù)組動(dòng)態(tài)擴(kuò)容導(dǎo)致頻繁FGC,有興趣的同學(xué)可以按下面的步驟動(dòng)手實(shí)踐一下,Java代碼如下:

// jdk1.7  -Xmx500M -Xms500M -Xmn200M -XX:+UseConcMarkSweepGC
// -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=90
public class CrossReference {
       
    private static int unit = 20 * 1024;

    public static void main(String[] args) throws Exception{
        Thread.sleep(5000);
        System.out.println("allocate start************");
        allocate();
        Thread.sleep(1000);
        System.out.println("allocate end************");
        System.in.read();
    }

    private static void allocate() throws Exception{
        int size = 1024 * 1024 * 400; // 400M
        int len = size / unit;
        List<BigObject> list = new ArrayList<>();

        for(int i = 1; i <= len; i++){
            BigObject bigObject = new BigObject();
            list.add(bigObject);
            Thread.sleep(1);
            System.out.println(i);
        }
    }

    private static class BigObject{
        private byte[] foo;
        BigObject(){
            foo = new byte[unit];
        }
    }
}

通過(guò)jstat -gcutil <pid> 1000命令,可以展示堆內(nèi)存的使用情況和GC觸發(fā)情況。

1、JVM參數(shù)中沒(méi)有-XX:+CMSScavengeBeforeRemark,jstat 結(jié)果如下:

圖中每個(gè)分代的數(shù)值代表內(nèi)存使用率,可以看出發(fā)生FGC的時(shí)候,并沒(méi)有觸發(fā)YGC,所以老年代的對(duì)象一直回收不了,因?yàn)槔夏甏M(jìn)行GC時(shí),會(huì)把新生代的對(duì)象作為GC root,在本場(chǎng)景中,新生代的對(duì)象不回收,老年代的對(duì)象也無(wú)法回收,而且老年代的內(nèi)存使用率已經(jīng)超過(guò)設(shè)置的閾值90%,所以會(huì)不斷的進(jìn)行FGC。

2、JVM參數(shù)中添加-XX:+CMSScavengeBeforeRemark,結(jié)果如下:

從上面的數(shù)據(jù)可以發(fā)現(xiàn),發(fā)生FGC時(shí),確實(shí)也觸發(fā)了YGC,但是這次YGC并沒(méi)有回收對(duì)象,導(dǎo)致了又不斷的FGC,通過(guò)查看GC日志,可以發(fā)現(xiàn)也是在不斷的打印GC信息,而且每次打印的數(shù)據(jù)(GC前的內(nèi)存使用量和GC后的內(nèi)存使用量)都是一樣的。

現(xiàn)象描述如下:
allocate() 方法執(zhí)行完成后
1.頻繁CMS GC,但是old區(qū)仍然占用大,基本未回收空間
2.添加-XX:+CMSScavengeBeforeRemark參數(shù),remark之前的一次ygc也未回收空間,old區(qū)情況同1
3.在Jprofiler中手動(dòng)Run Full GC,young區(qū),old區(qū)都被正?;厥?br> 4.若Arraylist初始化容量為它需要add的數(shù)量,則不存在上述現(xiàn)象

問(wèn)題如下:
1.allocate() 方法執(zhí)行完成后,堆中存在young-》old, old-》young的循環(huán)引用嗎
2.添加-XX:+CMSScavengeBeforeRemark參數(shù),為何仍然有問(wèn)題,方法執(zhí)行完后在Jprofiler中查看存活對(duì)象Arraylist已經(jīng)沒(méi)有,但是BigObject,byte[]仍然存在,占用約400M內(nèi)存
3.手動(dòng)Run Full GC為何就能正?;厥眨?/p>

為了解答這個(gè)問(wèn)題,就得從Hotspot的實(shí)現(xiàn)源碼入手,看看GC動(dòng)作到底是什么樣的過(guò)程,我們不妨可以假設(shè)一下,確實(shí)觸發(fā)了YGC動(dòng)作,然后記錄了GC信息,但是因?yàn)槟承┰虿](méi)有真正的執(zhí)行YGC過(guò)程。

YGC實(shí)現(xiàn)過(guò)程

新生代內(nèi)存分配失敗觸發(fā)YGC

如果是因?yàn)樾律鷥?nèi)存分配失敗觸發(fā)YGC時(shí),JVM內(nèi)部對(duì)應(yīng)會(huì)生成一個(gè)VM_GenCollectForAllocation對(duì)象,提交到一個(gè)執(zhí)行隊(duì)列中,最終會(huì)由VM Thread執(zhí)行它的doit()方法,可以看一下doit方法

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

  GenCollectedHeap* gch = GenCollectedHeap::heap();
  GCCauseSetter gccs(gch, _gc_cause);
  _res = gch->satisfy_failed_allocation(_size, _tlab);
  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();
  }
}

這里需要關(guān)注的是satisfy_failed_allocation()方法,定義在GenCollectedHeap


HeapWord* GenCollectedHeap::satisfy_failed_allocation(size_t size, bool is_tlab) {
  return collector_policy()->satisfy_failed_allocation(size, is_tlab);
}

由垃圾回收策略決定執(zhí)行satisfy_failed_allocation方法,實(shí)現(xiàn)如下


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()) {
    // GC locker is active; instead of a collection we will attempt
    // to expand the heap, if there's room for expansion.
    if (!gch->is_maximal_no_gc()) {
      result = expand_heap_and_allocate(size, is_tlab);
    }
    return result;   // could be null if we are out of space
  } else if (!gch->incremental_collection_will_fail(false /* don't consult_young */)) {
    // Do an incremental collection.
    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 :: ");
    }
    // Try a full collection; see delta for bug id 6266275
    // for the original code and why this has been simplified
    // with from-space allocation criteria modified and
    // such allocation moved out of the safepoint path.
    gch->do_collection(true             /* full */,
                       false            /* clear_all_soft_refs */,
                       size             /* size */,
                       is_tlab          /* is_tlab */,
                       number_of_generations() - 1 /* max_level */);
  }

  ...

1、如果GC_locker正在起作用,說(shuō)明有線程正在通過(guò)JNI操作臨界內(nèi)存,那么就放棄GC動(dòng)作,因?yàn)镴NI操作完之后會(huì)可能會(huì)觸發(fā)一次GC
2、如果上一次YGC是失敗的,至于為什么上一次是失敗的,可能是因?yàn)槔夏甏鷽](méi)有足夠的空間容納新生代的對(duì)象,那么就不執(zhí)行本次的YGC,直接進(jìn)行FGC

不管是YGC還是FGC,都是通過(guò)執(zhí)行GenCollectedHeap::do_collection()方法實(shí)現(xiàn)的,在該方法中會(huì)記錄執(zhí)行GC的日志,實(shí)現(xiàn)如下:

bool complete = full && (max_level == (n_gens()-1));
const char* gc_cause_prefix = complete ? "Full GC" : "GC";
TraceCPUTime tcpu(PrintGCDetails, true, gclog_or_tty);
GCTraceTime t(GCCauseString(gc_cause_prefix, gc_cause()), PrintGCDetails, false, NULL);

其中GCTraceTime實(shí)現(xiàn)中,會(huì)打印GC日志信息,而且這個(gè)時(shí)刻并未開(kāi)始GC動(dòng)作。

CTraceTime::GCTraceTime(const char* title, bool doit, bool print_cr, GCTimer* timer) :
    _title(title), _doit(doit), _print_cr(print_cr), _timer(timer) {
  if (_doit || _timer != NULL) {
    _start_counter = os::elapsed_counter();
  }

  if (_timer != NULL) {
    assert(SafepointSynchronize::is_at_safepoint(), "Tracing currently only supported at safepoints");
    assert(Thread::current()->is_VM_thread(), "Tracing currently only supported from the VM thread");

    _timer->register_gc_phase_start(title, _start_counter);
  }

  if (_doit) {
    gclog_or_tty->date_stamp(PrintGCDateStamps);
    gclog_or_tty->stamp(PrintGCTimeStamps);
    gclog_or_tty->print("[%s", title);
    gclog_or_tty->flush();
  }
}

執(zhí)行YGC的真正邏輯在parNewGeneration::collect()方法中,在方法中有這么一個(gè)判斷邏輯:

  // If the next generation is too full to accommodate worst-case promotion
  // from this generation, pass on collection; let the next generation
  // do it.
  if (!collection_attempt_is_safe()) {
    gch->set_incremental_collection_failed();  // slight lie, in that we did not even attempt one
    return;
  }

如果不滿足collection_attempt_is_safe(),就直接返回,說(shuō)明本次YGC是不安全的,不會(huì)真正執(zhí)行本次的垃圾回收,那什么情況算是不安全的呢?collection_attempt_is_safe()實(shí)現(xiàn)如下:

bool DefNewGeneration::collection_attempt_is_safe() {
  if (!to()->is_empty()) {
    if (Verbose && PrintGCDetails) {
      gclog_or_tty->print(" :: to is not empty :: ");
    }
    return false;
  }
  if (_next_gen == NULL) {
    GenCollectedHeap* gch = GenCollectedHeap::heap();
    _next_gen = gch->next_gen(this);
    assert(_next_gen != NULL,
           "This must be the youngest gen, and not the only gen");
  }
  return _next_gen->promotion_attempt_is_safe(used());
}

1、to空間不為空
2、沒(méi)有下一個(gè)內(nèi)存代,即沒(méi)有老年代
3、其中情況1和2幾乎不會(huì)發(fā)生,主要還是看這種情況,主要看老年代是否有足夠的空間來(lái)容納新生代的對(duì)象,老年代的promotion_attempt_is_safe()的實(shí)現(xiàn)如下:

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);
  if (Verbose && PrintGCDetails) {
    gclog_or_tty->print_cr(
      "CMS: promo attempt is%s safe: available("SIZE_FORMAT") %s av_promo("SIZE_FORMAT"),"
      "max_promo("SIZE_FORMAT")",
      res? "":" not", available, res? ">=":"<",
      av_promo, max_promotion_in_bytes);
  }
  return res;
}

如果老年代中的可用空間大于gc_stats統(tǒng)計(jì)的新生代每次平均晉升的對(duì)象大小,或者可以容納目前新生代所有的對(duì)象,表明可以執(zhí)行正常的YGC動(dòng)作,如果都不滿足,就直接放棄本次YGC。

由FGC觸發(fā)的YGC

在JVM參數(shù)中添加-XX:+CMSScavengeBeforeRemark,執(zhí)行FGC之前會(huì)觸發(fā)一次YGC,這個(gè)參數(shù)的好處是如果YGC比較有效果的話是能有效降低remark的時(shí)間長(zhǎng)度,可以簡(jiǎn)單理解為如果大部分新生代的對(duì)象被回收了,那么GC root變少了,從而提高了remark的效率。

因?yàn)镃MS是多線程執(zhí)行的,主要的執(zhí)行入口定義在ConcurrentMarkSweepThread::run()方法,ConcurrentMarkSweepThread相當(dāng)于繼承了Java中的Thread的一個(gè)類,在run方法執(zhí)行實(shí)現(xiàn)具體邏輯,下面是CMS執(zhí)行過(guò)程的簡(jiǎn)要分析。

1、run方法中調(diào)用CMSCollector::collect_in_background方法,在該方法中,會(huì)根據(jù)當(dāng)前CMS的執(zhí)行狀態(tài),初始化對(duì)應(yīng)的 VM_CMS_Operation,本文是分析CMSScavengeBeforeRemark,該變量所用到的邏輯中對(duì)用的狀態(tài)為 VM_CMS_Final_Remark,該狀態(tài)是CMS中再次標(biāo)記階段。

2、初始化 VM_CMS_Final_Remark,并提交到VM Thread的執(zhí)行隊(duì)列中,等待被執(zhí)行,最終由VM Thread執(zhí)行它的doit方法,這方式和執(zhí)行YGC時(shí)類似

3、在VM_CMS_Final_Remark::doit()方法中調(diào)用_collector->do_CMS_operation();,最終調(diào)用CMSCollector::checkpointRootsFinal()方法,其中和CMSScavengeBeforeRemark相關(guān)的代碼實(shí)現(xiàn)如下:

if (CMSScavengeBeforeRemark) {
      GenCollectedHeap* gch = GenCollectedHeap::heap();
      // Temporarily set flag to false, GCH->do_collection will
      // expect it to be false and set to true
      FlagSetting fl(gch->_is_gc_active, false);
      NOT_PRODUCT(GCTraceTime t("Scavenge-Before-Remark",
        PrintGCDetails && Verbose, true, _gc_timer_cm);)
      int level = _cmsGen->level() - 1;
      if (level >= 0) {
        gch->do_collection(true,        // full (i.e. force, see below)
                           false,       // !clear_all_soft_refs
                           0,           // size
                           false,       // is_tlab
                           level        // max_level
                          );
      }
    }

這里又回到了GenCollectedHeap::do_collection()方法,具體過(guò)程和新生代內(nèi)存分配失敗觸發(fā)YGC的情況一樣。

另外手動(dòng)執(zhí)行Run Full GC和執(zhí)行jmap -histo:live <pid> 命令,可以強(qiáng)制虛擬機(jī)執(zhí)行垃圾回收。

參考資料:

假笨說(shuō)-關(guān)于數(shù)組動(dòng)態(tài)擴(kuò)容導(dǎo)致頻繁GC的問(wèn)題,我還有話說(shuō)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容