性能文章>G1-垃圾回收简述(三)>

G1-垃圾回收简述(三)原创

1年前
555616

Mixed GC回收流程介绍

前面3篇文章,分解了G1的分区(Region),RSet,以及新生代的回收也就是G1-YGC。而G1的混合回收,则是既回收新生代分区也回收部分老年代分区。回收新生代分区的我就不再提了,着重关注老年代的并发标记,并发清理。针对Mixed GC而言没主要分为标记清理和回收俩个阶段,我在这里逐步分析。

标记清理

  • 初始标记

    初始标记需要STW。和新生代的处理一样,也是从Java根或者JVM根出发进行标记,而且由于混合Mixed GC发生一定会触发一次YGC,所以这里的初始标记直接借用YGC收集的结果,也就是survivor的作为根。

  • 并发标记

    并发标记依赖于Survivor区和老年代的Rset进行并发标记,触发的时机在于YGC之后堆空间使用率达到InitiatingHeapOccupancyPercent==45的阈值,此时并发标记线程和应用线程同时进行,这时候就会可能标记的不准确,由此引出了G1的三色标记。G1使用快照的模式来保证宁可不杀,也不错杀的原则,等最后重标记阶段去兜底处理。即便是出现了B.x=null;也不直接处理,而是使用write barrier来处理,就是发生引用删除的时候,在执行出插入一块写屏障,把这个对象放在STAB当中,标记为活对象。这一块儿的处理呢,就会存在浮动垃圾.

  • 重标记

    同样需要一个STW,用来最终确认整个标记的活跃对象。在三色标记中,G1保存了对象的初始快照,所以需要在STW的时候进行处理,

  • 清理阶段

    这个阶段特直观,G1只会回收全是垃圾的分区。重置老年代RSet

回收阶段

经过标记清理阶段之后,就是要把那些不全是活跃对象的空间进行复制,将其腾挪到空闲列表中,回收不全为空的分区。这个阶段和YGC一样,从分区中选取N个分区当作CSet。然后把这些分区中的活跃对象拷贝到空闲的分区之内,复制完毕之后,再把回收的分区驾到空闲分区当中。只不过YGC有特殊的是会有一步晋升的处理。

三色标记

三色标记算法用于在并发标记阶段防止漏标和错标的算法。定义了三种颜色,黑色,灰色,白色,

  • 黑色

    表示当前对象已近被标记到且当前对象的所有field所引用的对象都已经被标记

  • 灰色

    表示自己已经被标记,但是其Field还没有被处理

  • 白色

    表示自己还没有被处理。

从GCRoot出发标记对象,当前Mutator线程也是在工作状态,并行于并发标记线程,所以容易出现漏标。例如:Mutator线程把一个黑色对象的关联到一个白色对象,此时需要更新黑色对象,不更新就会导致白色对象会被漏标,还有就是Mutator线程把一个灰色对象到白色对象的引用删除了,由于灰色对象这是标记自身了,并没有标记到field,如果直接此时删除了这层关系,那么对应的引用关系就会被漏标。
解决漏标问题,有增量更新和STAB的方式来处理

  • 增量更新
    在CMS中使用,当对象引用发生改变是,把黑色对象重新标记为灰色,进行重新标记。
    1606117559842.gif
  • STAB
    G1则使用STAB缓存来解决,即在对象赋值之前,把对象开始的引用状态保存下来,处理STAB缓存。
    1606118347592.gif

上述两图中分别模拟了增量更新和STAB对待漏标的处理流程。从图中的GcRoot触发,标记到俩个对象(A,B),其中(黑色)A对象自身的其所有的field引用都以被标记过。(灰色)B对象自身已被标记完毕,其field的引用在标记队列中进行标记过程中,没有全部标记完毕。(白色)C对象还没有被GcRoot关联到。注意此时的Mutator和并发标记线程是在同时进行的。当应用线程就改了A和B的引用关系。如去掉了A->B的引用,增加了A->C的引用,去掉了B->C,不处理就是发生漏标。

细心的你可能会发现有疑问的点,为什么STAB的方式,是把C对象标记为灰色。具体操作呢则是在应用线程发生删除应用关系的同时,先把B对象的引用的C对象放到标记队列当中,其实就相当于C对象标记为灰色。

写屏障

这里的写屏障不是并发访问中的内存屏障,STAB中的写屏障是主要用于保证并发标记过程的准确,在Rset中也有写屏障用于处理引用关系。
在对象field赋值阶段,JVM会进行如下方法调用oop.cpp#oop_store

template <class T> inline void oop_store(T* p, oop v) {
  if (always_do_update_barrier) {
    oop_store((volatile T*)p, v);
  } else {
    update_barrier_set_pre(p, v);
    oopDesc::encode_store_heap_oop(p, v);
    // always_do_update_barrier == false =>
    // Either we are at a safepoint (in GC) or CMS is not used. In both
    // cases it's unnecessary to mark the card as dirty with release sematics.
    update_barrier_set((void*)p, v, false /* release */);  // cast away type
  }
}

在调用oop_store方法的时候,前置处理就是把目标对象放在队列中,实际上执行的是入队方法,如下
G1SATBCardTableModRefBS.cpp#enqueue

void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
  // Nulls should have been already filtered.
  assert(pre_val->is_oop(true), "Error");

  if (!JavaThread::satb_mark_queue_set().is_active()) return;
  Thread* thr = Thread::current();
  if (thr->is_Java_thread()) {
    JavaThread* jt = (JavaThread*)thr;
    jt->satb_mark_queue().enqueue(pre_val);
  } else {
    MutexLockerEx x(Shared_SATB_Q_lock, Mutex::_no_safepoint_check_flag);
    JavaThread::satb_mark_queue_set().shared_satb_queue()->enqueue(pre_val);
  }
}

这个STAB队列在并发标记线程中自旋消费。
在调用oop_store方法的时候,后置处理就是把目标对象放在DCQ当中,表示该对象有引用关系,供RSet处理。

Mixed GC源码分析

混合GC的开始,基于G1 YGC的完成,并发标记过程由并发标记线程从Survivor区或者老年代的RSet处记录的的活跃对象作为根开始标记,并发标记线程的执行逻辑主要在concurrentMarkThread.cpp#run方法中.

void ConcurrentMarkThread::run() {
  initialize_in_thread();
  _vtime_start = os::elapsedVTime();
  wait_for_universe_init();

  G1CollectedHeap* g1h = G1CollectedHeap::heap();
  G1CollectorPolicy* g1_policy = g1h->g1_policy();
  G1MMUTracker *mmu_tracker = g1_policy->mmu_tracker();
  Thread *current_thread = Thread::current();

  while (!_should_terminate) {
    sleepBeforeNextCycle();
    if (_should_terminate) {
      break;
    }

    {
      ResourceMark rm;
      HandleMark   hm;
      double cycle_start = os::elapsedVTime();


      double scan_start = os::elapsedTime();
      if (!cm()->has_aborted()) {
        if (G1Log::fine()) {
          gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
          gclog_or_tty->print_cr("[GC concurrent-root-region-scan-start]");
        }

        _cm->scanRootRegions();

        double scan_end = os::elapsedTime();
        if (G1Log::fine()) {
          gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
          gclog_or_tty->print_cr("[GC concurrent-root-region-scan-end, %1.7lf secs]",
                                 scan_end - scan_start);
        }
      }

      double mark_start_sec = os::elapsedTime();
      if (G1Log::fine()) {
        gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
        gclog_or_tty->print_cr("[GC concurrent-mark-start]");
      }

      int iter = 0;
      do {
        iter++;
        if (!cm()->has_aborted()) {
          _cm->markFromRoots();
        }

        double mark_end_time = os::elapsedVTime();
        double mark_end_sec = os::elapsedTime();
        _vtime_mark_accum += (mark_end_time - cycle_start);
        if (!cm()->has_aborted()) {
          if (g1_policy->adaptive_young_list_length()) {
            double now = os::elapsedTime();
            double remark_prediction_ms = g1_policy->predict_remark_time_ms();
            jlong sleep_time_ms = mmu_tracker->when_ms(now, remark_prediction_ms);
            os::sleep(current_thread, sleep_time_ms, false);
          }

          if (G1Log::fine()) {
            gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
            gclog_or_tty->print_cr("[GC concurrent-mark-end, %1.7lf secs]",
                                      mark_end_sec - mark_start_sec);
          }

          CMCheckpointRootsFinalClosure final_cl(_cm);
          VM_CGC_Operation op(&final_cl, "GC remark", true /* needs_pll */);
          VMThread::execute(&op);
        }
        if (cm()->restart_for_overflow()) {
          if (G1TraceMarkStackOverflow) {
            gclog_or_tty->print_cr("Restarting conc marking because of MS overflow "
                                   "in remark (restart #%d).", iter);
          }
          if (G1Log::fine()) {
            gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
            gclog_or_tty->print_cr("[GC concurrent-mark-restart-for-overflow]");
          }
        }
      } while (cm()->restart_for_overflow());

      double end_time = os::elapsedVTime();
      _vtime_accum = (end_time - _vtime_start);

      if (!cm()->has_aborted()) {
        if (g1_policy->adaptive_young_list_length()) {
          double now = os::elapsedTime();
          double cleanup_prediction_ms = g1_policy->predict_cleanup_time_ms();
          jlong sleep_time_ms = mmu_tracker->when_ms(now, cleanup_prediction_ms);
          os::sleep(current_thread, sleep_time_ms, false);
        }

        CMCleanUp cl_cl(_cm);
        VM_CGC_Operation op(&cl_cl, "GC cleanup", false /* needs_pll */);
        VMThread::execute(&op);
      } else {
        SuspendibleThreadSetJoiner sts;
        g1h->set_marking_complete();
      }

      if (g1h->free_regions_coming()) {

        double cleanup_start_sec = os::elapsedTime();
        if (G1Log::fine()) {
          gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
          gclog_or_tty->print_cr("[GC concurrent-cleanup-start]");
        }

        _cm->completeCleanup();

        g1h->reset_free_regions_coming();

        double cleanup_end_sec = os::elapsedTime();
        if (G1Log::fine()) {
          gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
          gclog_or_tty->print_cr("[GC concurrent-cleanup-end, %1.7lf secs]",
                                 cleanup_end_sec - cleanup_start_sec);
        }
      }
      guarantee(cm()->cleanup_list_is_empty(),
                "at this point there should be no regions on the cleanup list");

      {
        SuspendibleThreadSetJoiner sts;
        if (!cm()->has_aborted()) {
          g1_policy->record_concurrent_mark_cleanup_completed();
        }
      }

      if (cm()->has_aborted()) {
        if (G1Log::fine()) {
          gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
          gclog_or_tty->print_cr("[GC concurrent-mark-abort]");
        }
      }

      if (!cm()->has_aborted()) {
        SuspendibleThreadSetJoiner sts;
        _cm->clearNextBitmap();
      } else {
        assert(!G1VerifyBitmaps || _cm->nextMarkBitmapIsClear(), "Next mark bitmap must be clear");
      }
    }

    {
      SuspendibleThreadSetJoiner sts;
      g1h->increment_old_marking_cycles_completed(true /* concurrent */);
      g1h->register_concurrent_cycle_end();
    }
  }
  assert(_should_terminate, "just checking");

  terminate();
}

并发标记线程的任务在创建后也不是直接启动,并发标记的启动依赖于YGC,在YGC的最后阶段会嗲用doConcurrentMark方法来启动。判断启动的条件在于当老年代使用的内存加上本次即将分配的内存占到总内存的45%,就表明需要启动混合GC,然后就要启动并发线程。
观察并发标记线程的执行方法。

  1. 扫描根进行标记参见scanRootRegions方法
  2. 并发标记参见markFromRoots方法
  3. 最终标价阶段处理参见CMCheckpointRootsFinalClosure闭包
  4. 循环处理针对栈空间溢出
  5. 清理阶段,回收

后序具体源码细节 后序补上,🐶困得不行了

分类:
标签:
请先登录,再评论

老哥继续更新啊,”并发标记过程由并发标记线程从Survivor区或者老年代的RSet处记录的的活跃对象作为根开始标记“这句话在详细说说,按照我的理解在初始标记阶段应该是扫描的survivor的Heap Region,在并发标记阶段会扫描老年代的RSet等等。。。但是没看到代码在哪

8月前

为你推荐

不起眼,但是足以让你有收获的JVM内存分析案例
分析 这个问题说白了,就是说有些int[]对象不知道是哪里来的,于是我拿他的例子跑了跑,好像还真有这么回事。点该 dump 文件详情,查看相关的 int[] 数组,点该对象的“被引用对象”,发现所
从一起GC血案谈到反射原理
前言 首先回答一下提问者的问题。这主要是由于存在大量反射而产生的临时类加载器和 ASM 临时生成的类,这些类会被保留在 Metaspace,一旦 Metaspace 即将满的时候,就会触发 Fu
关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
协助美团kafka团队定位到的一个JVM Crash问题
概述 有挺长一段时间没写技术文章了,正好这两天美团kafka团队有位小伙伴加了我微信,然后咨询了一个JVM crash的问题,大家对crash的问题都比较无奈,因为没有现场,信息量不多,碰到这类问题我
又发现一个导致JVM物理内存消耗大的Bug(已提交Patch)
概述 最近我们公司在帮一个客户查一个JVM的问题(JDK1.8.0_191-b12),发现一个系统老是被OS Kill掉,是内存泄露导致的。在查的过程中,阴差阳错地发现了JVM另外的一个Bug。这个B
JVM实战:优化我的IDEA GC
IDEA是个好东西,可以说是地球上最好的Java开发工具,但是偶尔也会卡顿,仔细想想IDEA也是Java开发的,会不会和GC有关,于是就有了接下来对IDEA的GC进行调优 IDEA默认JVM参数: -
不起眼,但是足以让你收获的JVM内存案例
今天的这个案例我觉得应该会让你涨姿势吧,不管你对JVM有多熟悉,看到这篇文章,应该还是会有点小惊讶的,不过我觉得这个案例我分享出来,是想表达不管多么奇怪的现象请一定要追究下去,会让你慢慢变得强大起来,
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得