YNZH's Blog

G1 and CMS

CMS垃圾回收流程:

  1. CMS young GC 使用的是复制清除算法。

  2. The CMS collector performs the following phases on the old generation of the heap:

Phase Description
(1) Initial Mark (Stop the World Event) Objects in old generation are “marked” as reachable including those objects which may be reachable from young generation. Pause times are typically short in duration relative to minor collection pause times.
(2) Concurrent Marking Traverse the tenured generation object graph for reachable objects concurrently while Java application threads are executing. Starts scanning from marked objects and transitively marks all objects reachable from the roots. The mutators are executing during the concurrent phases 2, 3, and 5 and any objects allocated in the CMS generation during these phases (including promoted objects) are immediately marked as live.
(3) Remark (Stop the World Event) Finds objects that were missed by the concurrent mark phase due to updates by Java application threads to objects after the concurrent collector had finished tracing that object.
(4) Concurrent Sweep Collects the objects identified as unreachable during marking phases. The collection of a dead object adds the space for the object to a free list for later allocation. Coalescing of dead objects may occur at this point. Note that live objects are not moved.
(5) Resetting Prepare for next concurrent collection by clearing data structures.

存在问题:

CMS是针对于老年代进行垃圾回收,会有碎片问题产生

The G1 Garbage Collector Step By Step:

为啥叫G1:

G1垃圾收集器将 heap划分为大小为1~32M等大小的不同区域。在垃圾回收的过程中:扫描各个region的时候可以判断出哪些region回收后剩余的内存空间最大,会优先选择这些region回收(所以叫G1),可以根据 参数最大延迟时间,根据预测模型,针对性的选择一定数量的region进行回收,从而达到低延时或延时可控的目的。

  1. Young GC

    将新生代regions的垃圾移除,同时copy剩余新生代到survivor region中,加入对象的年龄达到阈值则移入old generation regions中。这个过程是Stop The World的。

    In summary, the following can be said about the young generation in G1:

    • The heap is a single memory space split into regions.

    • Young generation memory is composed of a set of non-contiguous regions. This makes it easy to resize when needed.

    • Young generation garbage collections, or young GCs, are stop the world events. All application threads are stopped for the operation.

    • The young GC is done in parallel using multiple threads.

    • Live objects are copied to new survivor or old generation regions.

  1. Old gengeration GC:

    The G1 collector performs the following phases on the old generation of the heap. Note that some phases are part of a young generation collection.

Phase Description
(1) Initial Mark (*Stop the World Event)*** This is a stop the world event. With G1, it is piggybacked on a normal young GC. Mark survivor regions (root regions) which may have references to objects in old generation.
(2) Root Region Scanning Scan survivor regions for references into the old generation. This happens while the application continues to run. The phase must be completed before a young GC can occur.
(3) Concurrent Marking Find live objects over the entire heap. This happens while the application is running. This phase can be interrupted by young generation garbage collections.
(4) Remark (Stop the World Event) Completes the marking of live object in the heap. Uses an algorithm called snapshot-at-the-beginning (SATB) which is much faster than what was used in the CMS collector.
(5) Cleanup (Stop the World Event and Concurrent) Performs accounting on live objects and completely free regions. (Stop the world)Scrubs the Remembered Sets. (Stop the world)Reset the empty regions and return them to the free list. (Concurrent)
() Copying (Stop the World Event)*** These are the stop the world pauses to evacuate or copy live objects to new unused regions. This can be done with young generation regions which are logged as [GC pause (young)]. Or both young and old generation regions which are logged as [GC Pause (mixed)].
  1. Summary of Old Generation GC

  2. In summary, there are a few key points we can make about the G1 garbage collection on the old generation.

    • Concurrent Marking Phase

      • Liveness information is calculated concurrently while the application is running.
      • This liveness information identifies which regions will be best to reclaim during an evacuation pause.
      • There is no sweeping phase like in CMS.
    • Remark Phase

      • Uses the Snapshot-at-the-Beginning (SATB) algorithm which is much faster then what was used with CMS.
      • Completely empty regions are reclaimed.
    • Copying/Cleanup Phase

      • Young generation and old generation are reclaimed at the same time.
      • Old generation regions are selected based on their liveness.

G1垃圾回收的模式:

    • 1、young GC(或者叫minor GC):只收集young gen里的所有region,也就是eden和survivor。控制young GC开销的手段是动态改变young region的个数;
      2、mixed GC:收集young gen里的所有region,外加若干选定的old gen region。控制mixed GC开销的手段是选多少个、哪几个old gen region。
      3、其实没有3了。G1 GC的控制范围内没有full GC。如果mixed GC无法跟上mutator分配的速度,导致没有足够的空region来完成mixed GC,那么就会使用serial old GC( mark-compact)来对整堆收集一次。

一篇有趣的帖子

参考之前我在另一帖的回复:http://hllvm.group.iteye.com/group/topic/21468#post-272070
使用card table的remembered set,只要card的粒度大于一个word,那它都是不准确的。这种不准确性跟保守式GC的不准确性类似,虽然不影响GC的正确性(活的对象在GC后仍然会式活的),但却会带来一定程度的内存开销(少量死的对象在某次GC后可能还没被回收)。这种死了的对象叫做floating garbage。使用card marking的取舍就是要尽量让mutator快,而collector付出更多代价,消耗内存也增多。

===========================================

至于G1的算法⋯大体概念其实还挺直观的?到底是哪里没明白?

从最高层看,G1的collector一侧其实就是两个大部分:
* 全局并发标记(global concurrent marking)
* 拷贝存活对象(evacuation)
而这两部分可以相对独立的执行。

Global concurrent marking基于SATB形式的并发标记。它具体分为下面几个阶段:
1、初始标记(initial marking):暂停阶段。扫描根集合,标记所有从根集合可直接到达的对象并将它们的字段压入扫描栈(marking stack)中等到后续扫描。G1使用外部的bitmap来记录mark信息,而不使用对象头的mark word里的mark bit。在分代式G1模式中,初始标记阶段借用young GC的暂停,因而没有额外的、单独的暂停阶段。
2、并发标记(concurrent marking):并发阶段。不断从扫描栈取出引用递归扫描整个堆里的对象图。每扫描到一个对象就会对其标记,并将其字段压入扫描栈。重复扫描过程直到扫描栈清空。过程中还会扫描SATB write barrier所记录下的引用。
3、最终标记(final marking,在实现中也叫remarking):暂停阶段。在完成并发标记后,每个Java线程还会有一些剩下的SATB write barrier记录的引用尚未处理。这个阶段就负责把剩下的引用处理完。同时这个阶段也进行弱引用处理(reference processing)。
注意这个暂停与CMS的remark有一个本质上的区别,那就是这个暂停只需要扫描SATB buffer,而CMS的remark需要重新扫描mod-union table里的dirty card外加整个根集合,而此时整个young gen(不管对象死活)都会被当作根集合的一部分,因而CMS remark有可能会非常慢。
4、清理(cleanup):暂停阶段。清点和重置标记状态。这个阶段有点像mark-sweep中的sweep阶段,不过不是在堆上sweep实际对象,而是在marking bitmap里统计每个region被标记为活的对象有多少。这个阶段如果发现完全没有活对象的region就会将其整体回收到可分配region列表中。

Evacuation阶段是全暂停的。它负责把一部分region里的活对象拷贝到空region里去,然后回收原本的region的空间。
Evacuation阶段可以自由选择任意多个region来独立收集构成收集集合(collection set,简称CSet),靠per-region remembered set(简称RSet)实现。这是regional garbage collector的特征。
在选定CSet后,evacuation其实就跟ParallelScavenge的young GC的算法类似,采用并行copying(或者叫scavenging)算法把CSet里每个region里的活对象拷贝到新的region里,整个过程完全暂停。从这个意义上说,G1的evacuation跟传统的mark-compact算法的compaction完全不同:前者会自己从根集合遍历对象图来判定对象的生死,不需要依赖global concurrent marking的结果,有就用,没有拉倒;而后者则依赖于之前的mark阶段对对象生死的判定。

论文里提到的纯G1模式下,CSet的选定完全靠统计模型找处收益最高、开销不超过用户指定的上限的若干region。由于每个region都有RSet覆盖,要单独evacuate任意一个或多个region都没问题。

分代式G1模式下有两种选定CSet的子模式,分别对应young GC与mixed GC:
* Young GC:选定所有young gen里的region。通过控制young gen的region个数来控制young GC的开销。
* Mixed GC:选定所有young gen里的region,外加根据global concurrent marking统计得出收集收益高的若干old gen region。在用户指定的开销目标范围内尽可能选择收益高的old gen region。
可以看到young gen region总是在CSet内。因此分代式G1不维护从young gen region出发的引用涉及的RSet更新。

分代式G1的正常工作流程就是在young GC与mixed GC之间视情况切换,背后定期做做全局并发标记。Initial marking默认搭在young GC上执行;当全局并发标记正在工作时,G1不会选择做mixed GC,反之如果有mixed GC正在进行中G1也不会启动initial marking。
在正常工作流程中没有full GC的概念,old gen的收集全靠mixed GC来完成。

如果mixed GC实在无法跟上程序分配内存的速度,导致old gen填满无法继续进行mixed GC,就会切换到G1之外的serial old GC来收集整个GC heap(注意,包括young、old、perm)。这才是真正的full GC。Full GC之所以叫full就是要收集整个堆,只选择old gen的部分region算不上full GC。进入这种状态的G1就跟-XX:+UseSerialGC的full GC一样(背后的核心代码是两者共用的)。
顺带一提,G1 GC的System.gc()默认还是full GC,也就是serial old GC。只有加上 -XX:+ExplicitGCInvokesConcurrent 时G1才会用自身的并发GC来执行System.gc()——此时System.gc()的作用是强行启动一次global concurrent marking;一般情况下暂停中只会做initial marking然后就返回了,接下来的concurrent marking还是照常并发执行。

然后G1在mutator一侧需要使用write barrier来实现:
* SATB snapshot的完整性
* 跨region的引用记录到RSet里。
这两个动作都使用了logging barrier,其处理有一部分由collector一侧并发执行。

可以看到在这么多步骤里,G1只有两件事是并发执行的:(1) 全局并发标记;(2) logging write barrier的部分处理。而“拷贝对象”(evacuation)这个很耗时的动作却不是并发而是完全暂停的。那G1为何还可以叫做低延迟的GC实现呢?
重点就在于G1虽然会mark整个堆,但并不evacuate所有有活对象的region;通过只选择收益高的少量region来evacuate,这种暂停的开销就可以(在一定范围内)可控。每次evacuate的暂停时间应该跟一般GC的young GC类似。所以G1把自己标榜为“软实时”(soft real-time)的GC。

但是毕竟要暂停来拷贝对象,这个暂停时间再怎么低也有限。G1的evacuation pause在几十到一百甚至两百毫秒都很正常。所以切记不要把 -XX:MaxGCPauseMillis 设得太低,不然G1跟不上目标就容易导致垃圾堆积,反而更容易引发full GC而降低性能。通常设到100ms、250ms之类的都可能是合理的。设到50ms就不太靠谱,G1可能一开始还跟得上,跑的时间一长就开始乱来了。
这也提醒大家:如果您的程序要长时间运行,那么在技术选型评估GC性能的时候要让测试程序跑足够长时间才能看清状况。多久才够长取决于实际应用要连续运行多久。不然一个要运行一个月才重启一次的程序,如果测试的时候只测了两个小时就觉得没问题,实际上线跑起来可能正好两个半小时的时候来了一次几分钟的full GC暂停,那就纱布了⋯

G1需要暂停来拷贝对象,而CMS在暂停中只需要扫描(mark)对象,那算法上G1的暂停时间会比CMS短么?
其实CMS在较小的堆、合适的workload的条件下暂停时间可以很轻松的短于G1。在2011年的时候Ramki告诉我堆大小的分水岭大概在10GB~15GB左右:以下的-Xmx更适合CMS,以上的才适合试用G1。现在到了2014年,G1的实现经过一定调优,大概在6GB~8GB也可以跟CMS有一比,我之前见过有在-Xmx4g的环境里G1比CMS的暂停时间更短的案例。
合适的workload:CMS最严重的暂停通常发生在remark阶段,因为它要扫描整个根集合,其中包括整个young gen。如果在CMS的并发标记阶段,mutator仍然在高速分配内存使得young gen里有很多对象的话,那remark阶段就可能会有很长时间的暂停。Young gen越大,CMS remark暂停时间就有可能越长。所以这是不适合CMS的workload。相反,如果mutator的分配速率比较温和,然后给足时间让并发的precleaning做好remark的前期工作,这样CMS就只需要较短的remark暂停,这种条件下G1的暂停时间很难低于CMS。

要在拷贝对象的前提下实现真正的低延迟就需要做并发拷贝(concurrent compaction)。但是现在已知的实现concurrent compaction的GC算法无一例外需要使用某种形式的read barrier,例如Azul的C4和Red Hat的Shenendoah。不用read barrier的话,没办法安全的实现一边移动对象一边修正指向这些对象的引用,因为mutator也可以会并发的访问到这些引用。
而G1则坚持只用write barrier不用read barrier,所以无法实现concurrent compaction。

大体概念其实就这样。有许多细节是挺麻烦的,例如如何提高并发减少瓶颈,如何处理收集到一半需要提前终止(abort,例如evacuation failure)的情况,等等。这样在读源码的时候会很头疼,但只是理解大体概念不需要关心到那种细节。

其实影响G1实际性能的许多地方都在细节里,而不在基本算法上。例如整个开销-收益模型,收集时机的预测模型,选取CSet的策略等等。影响用户对G1做性能调优的也是在这些地方。可惜现在的G1在这些细节上做得仍然不算很好,所以预测得不够准确,性能潜力还无法完全发挥。我每天听同事吐槽感到甚欢乐orz

IBM的Balanced GC,也叫incremental generational GC,其核心概念和基本算法都跟G1 GC非常相似。它也是一个regional garbage collector,使用多层的points-into remembered set,也有相对独立的并行增量式/并发式global marking和partial GC(与G1的evacuation/mixed GC对应)。只是一些实现细节和调优策略有差别而已,例如Balanced GC的global marking用的是incremental update式的write barrier而不是SATB;另外它支持arraylet和in-place compaction(可选不同region间的copy-forward,或同region内的mark-compact)。
关于Balanced GC,可以参考:
* http://www.ibm.com/developerworks/websphere/techjournal/1108_sciampacone/1108_sciampacone.html
* http://www-01.ibm.com/support/knowledgecenter/SSYKE2_7.0.0/com.ibm.java.lnx.70.doc/diag/understanding/mm_gc_balanced.html

===========================================

我猜楼主的困惑的几个主要来源是:
1、对SATB的要求不熟悉
2、对logging write-barrier不熟悉
3、对多层的”points-into” remembered set不熟悉

光读G1的原始论文确实会比较头疼。写得太简单抽象,来龙去脉介绍得不够清楚。
嗯⋯但是我要咋讲解好呢?人家nari3都写了一本完整的书来讲解G1的算法,写得还不错;Monica从使用调优方面写的Garbage First Garbage Collector Tuning也总结得很好。我要在一帖里把问题彻底说清楚感觉挺麻烦。
另外,理解G1所需的基本知识其实在The Garbage Collection Handbook里都说得很清楚,买本来耐心读读就啥都解决了。

就上面的3点来说说吧:

1、SATB,snapshot-at-the-beginning,是维持并发GC的正确性的一个手段。G1 GC的并发理论基础就是SATB,而CMS则是“incremental update”。如果你读到有文章说CMS是SATB的话它肯定说错了。嗯Christine大概太久没关注CMS了⋯

SATB抽象的说就是在一次GC开始的时候是活的对象就被认为是活的,此时的对象图形成一个逻辑“快照”(snapshot);然后在GC过程中新分配的对象都当作是活的。其它不可到达的对象就是死的了。

很容易知道哪些对象是一次GC开始之后新分配的:每个region记录着两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象就是新分配的,因而被视为隐式marked。

但是在并发GC里,collector一边动mutator也一边动,如果collector并发mark的过程中mutator覆盖了某些引用字段的值而collector还没mark到那里,那collector不就得不到完整的snapshot了么?

为了解决这个问题就有了SATB write barrier。G1 GC具体使用的是“湯浅”(Yuasa)式的SATB write barrier的变种。它的相关论文是:
Real-time garbage collection on general-purpose machines, Taiichi Yuasa(湯淺 太一

Write barrier是对“对引用类型字段赋值”这个动作的环切,也就是说赋值的前后都在barrier覆盖的范畴内。在赋值前的部分的write barrier叫做pre-write barrier,在赋值后的则叫做post-write barrier。
在HotSpot VM里,在引入G1 GC之前,其它GC都只用了post-write barrier,所以它在源码里没有特别的前后缀;而G1 GC特有的pre-write barrier则在源码里有_pre的后缀,可以留意一下。

C代码 收藏代码

  1. void oop_field_store(oop* field, oop value) {
  2. pre_write_barrier(field);
  3. *field = value; // the actual store
  4. post_write_barrier(field, value);
  5. }

Pre/post-write barrier跟SATB有啥关系呢?

前面提到SATB要维持“在GC开始时活的对象”的状态这个逻辑snapshot。除了从root出发把整个对象图mark下来之外,其实只需要用pre-write barrier把每次引用关系变化时旧的引用值记下来就好了。这样,等concurrent marker到达某个对象时,这个对象的所有引用类型字段的变化全都有记录在案,就不会漏掉任何在snapshot里活的对象。当然,很可能有对象在snapshot中是活的,但随着并发GC的进行它可能本来已经死了,但SATB还是会让它活过这次GC。

所以在G1 GC里,整个write barrier+oop_field_store是这样的:

C代码 收藏代码

  1. void oop_field_store(oop* field, oop new_value) {
  2. pre_write_barrier(field); // pre-write barrier: for maintaining SATB invariant
  3. *field = new_value; // the actual store
  4. post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference
  5. }

按照湯浅式SATB barrier的设计,pre-write barrier里面的抽象逻辑应当如下:

C++代码 收藏代码

  1. void pre_write_barrier(oop* field) {
  2. if ($gc_phase == GC_CONCURRENT_MARK) { // SATB invariant only maintained during concurrent marking
  3. oop old_value = *field;
  4. if (old_value != null && !is_marked(old_value)) {
  5. mark_object(old_value);
  6. $mark_stack->push(old_value); // scan all of old_value’s fields later
  7. }
  8. }
  9. }

在每次引用关系发生变化时,旧的引用所指向的对象就会被mark上,其子孙也会被递归mark上,这样就不会漏mark任何对象,snapshot的完整性也就得到了保证。

但实际去看G1的论文和代码,会发现它的pre-write barrier却是类似这样的:

C++代码 收藏代码

  1. void pre_write_barrier(oop* field) {
  2. oop old_value = *field;
  3. if (old_value != null) {
  4. if ($gc_phase == GC_CONCURRENT_MARK) { // SATB invariant only maintained during concurrent marking
  5. $current_thread->satb_mark_queue->enqueue(old_value);
  6. }
  7. }
  8. }

这比原本的湯浅式设计少了些东西:没有检查目标对象是否已经mark,也不去对目标对象做mark和扫描它的字段。
实际上该做的事情还是得做,只是不在这里做而已。后面讲到logging barrier的时候就会展开说明了。

(Pre-write barrier的实际代码有好几个版本,其中最简单明白的版本是:

C++代码 收藏代码

  1. // This notes that we don’t need to access any BarrierSet data
  2. // structures, so this can be called from a static context.
  3. template <class T> static void write_ref_field_pre_static(T* field, oop newVal) {
  4. T heap_oop = oopDesc::load_heap_oop(field);
  5. if (!oopDesc::is_null(heap_oop)) {
  6. enqueue(oopDesc::decode_heap_oop(heap_oop));
  7. }
  8. }

enqueue动作的实际代码则在G1SATBCardTableModRefBS::enqueue(oop pre_val)。
它判断当前是否在concurrent marking phase用的是:

C++代码 收藏代码

  1. JavaThread::satb_mark_queue_set().is_active()

SATBMarkQueueSet只有在concurrent marking时才会被置为active。

CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue。

2、logging write barrier

为了尽量减少write barrier对mutator性能的影响,G1将一部分原本要在barrier里做的事情挪到别的线程上并发执行。
实现这种分离的方式就是通过logging形式的write barrier:mutator只在barrier里把要做的事情的信息记(log)到一个队列里,然后另外的线程从队列里取出信息批量完成剩余的动作。

以SATB write barrier为例,每个Java线程有一个独立的、定长的SATBMarkQueue,mutator在barrier里只把old_value压入该队列中。一个队列满了之后,它就会被加到全局的SATB队列集合SATBMarkQueueSet里等待处理,然后给对应的Java线程换一个新的、干净的队列继续执行下去。

并发标记(concurrent marker)会定期检查全局SATB队列集合的大小。当全局集合中队列数量超过一定阈值后,concurrent marker就会处理集合里的所有队列:把队列里记录的每个oop都标记上,并将其引用字段压到标记栈(marking stack)上等后面做进一步标记。

3、”Points-into” remembered set

G1 GC的heap与HotSpot VM的其它GC一样有一个覆盖整个heap的card table。
逻辑上说,G1 GC的remembered set(下面简称RSet)是每个region有一份。这个RSet记录的是从别的region指向该region的card。所以这是一种“points-into”的remembered set。

用card table实现的remembered set通常是points-out的,也就是说card table要记录的是从它覆盖的范围出发指向别的范围的指针。以分代式GC的card table为例,要记录old -> young的跨代指针,被标记的card是old gen范围内的。

G1 GC则是在points-out的card table之上再加了一层结构来构成points-into RSet:每个region会记录下到底哪些别的region有指向自己的指针,而这些指针分别在哪些card的范围内。
这个RSet其实是一个hash table,key是别的region的起始地址,value是一个集合,里面的元素是card table的index。

举例来说,如果region A的RSet里有一项的key是region B,value里有index为1234的card,它的意思就是region B的一个card里有引用指向region A。所以对region A来说,该RSet记录的是points-into的关系;而card table仍然记录了points-out的关系。

Monica写的Tips for Tuning the Garbage First Garbage Collector里有幅图形象的描述了points-into remembered set的关系,下面引用过来:

Monica Beckwith 写道

img

为了维持这种RSet,G1 GC的post-write barrier的抽象逻辑需要做下面的事情:
(暂时忽略hot card的特殊处理,同时忽略evacuation已经开始之后对collection set内的card的特殊处理)

C代码 收藏代码

  1. void post_write_barrier(oop* field, oop new_value) {
  2. uintptr_t field_uint = (uintptr_t) field;
  3. uintptr_t new_value_uint = (uintptr_t) new_value;
  4. uintptr_t comb = (field_uint ^ new_value_uint) >> HeapRegion::LogOfHRGrainBytes;
  5. if (comb == 0) return; // field and new_value are in the same region
  6. if (new_value == null) return; // filter out null stores
  7. // Otherwise, add to remembered set
  8. // first, add to card table
  9. volatile jbyte* card_ptr = card_for(field); // get pointer to the card for this field
  10. // in generational G1 mode, skip dirtying cards for young gen regions,
  11. // – young gen regions are always collected
  12. // if (*card_ptr == g1_young_gen) return;
  13. if (*card_ptr != dirty_card) {
  14. // dirty the card to reduce the work for multiple stores to the same card
  15. *card_ptr = dirty_card;
  16. // clean the card the get ready to do the real work
  17. *card_ptr = clean_card;
  18. // find the memory region representing the card
  19. HeapWord* start = $card_table->addr_for(card_ptr);
  20. HeapWord* end = start + CARD_SIZE_IN_WORDS;
  21. MemRegion* dirty_mem_region(start, end);
  22. // and find the G1 heap region containing the card
  23. HeapRegion* from_region = $g1_heap->heap_region_containing(start);
  24. // scan all reference fields in dirtied region
  25. foreach (oop from_obj in dirty_mem_region) {
  26. foreach (oop* f in from_obj->oop_fields()) {
  27. ​ oop to_obj = *f;
  28. ​ HeapRegion* to_region = $g1_heap->heap_region_containing(to_obj);
  29. if (from_region != to_region) {
  30. ​ CardIdx_t from_card = from_region->card_index_for(card_ptr);
  31. ​ to_region->remembered_set->add_reference(from_region, from_card);
  32. ​ }
  33. }
  34. }
  35. }
  36. }

可以看到一个region的RSet是如何与card table里的card关联在一起的。
中间有一处“奇怪”的代码,把card给涂黑然后又马上清掉。这在实际代码里其实是在两个不同线程上做的:在mutator线程上把card给dirty了之后加到一个队列里,然后ConcurrentG1RefineThread从队列里把card拿出来之后再置为clean。上面是为了把整体过程放在一起方便说明所以写成这样。
另外还有一部分注释掉的关于分代式G1模式的代码,这部分代码的作用就是过滤掉从young gen region出发的引用涉及的RSet维护。G1的论文讲解的基本算法是不分代的纯G1(pure garbage-first)只是简单提到了有分代式G1(generational garbage-first)。实际在JDK7或以上可以用的只有分代模式的G1,没有可用参数选择纯G1模式。为了贴合原始算法描述,这里就把分代相关的处理列出来但注释掉。

每次向引用类型字段赋值都要经过这么多步骤来更新RSet的话开销实在太大,而实际G1的实现是类似:

C代码 收藏代码

  1. void post_write_barrier(oop* field, oop new_value) {
  2. uintptr_t field_uint = (uintptr_t) field;
  3. uintptr_t new_value_uint = (uintptr_t) new_value;
  4. uintptr_t comb = (field_uint ^ new_value_uint) >> HeapRegion::LogOfHRGrainBytes;
  5. if (comb == 0) return; // field and new_value are in the same region
  6. if (new_value == null) return; // filter out null stores
  7. // Otherwise, log it
  8. volatile jbyte* card_ptr = card_for(field); // get address of the card for this field
  9. // in generational G1 mode, skip dirtying cards for young gen regions,
  10. // – young gen regions are always collected
  11. // if (*card_ptr == g1_young_gen) return;
  12. if (*card_ptr != dirty_card) {
  13. // dirty the card to reduce the work for multiple stores to the same card
  14. *card_ptr = dirty_card;
  15. // log the card for concurrent remembered set refinement
  16. JavaThread::current()->dirty_card_queue->enqueue(card_ptr);
  17. }
  18. }

这是logging barrier在G1 write barrier上的又一次应用。

跟SATB marking queue类似,每个Java线程有一个dirty card queue,也就是论文里说的每个线程的remembered set log;然后有一个全局的DirtyCardQueueSet,也就是论文里说的全局的filled RS buffers。
实际更新RSet的动作就交由多个ConcurrentG1RefineThread并发完成。每当全局队列集合超过一定阈值后,ConcurrentG1RefineThread就会取出若干个队列,遍历每个队列记录的card并将card加到对应的region的RSet里去。

先写到这里,有啥问题我再更新到这个回复来。

做菜与洗碗

今天晚饭,老婆连着用了4个锅来做了两菜一汤。好吃img
其中一道菜用了1个锅,另一道菜用了3个锅,汤用了2个锅。
还有中间工序放临时状态的食材用的大小碗碟若干。

嗯⋯嗯?但是总共只有4个锅诶。
于是做菜的过程中我也洗了几次碗。

突然想起了什么⋯
老婆做菜跟我洗碗的关系,就是mutator与collector的关系!

老婆作为mutator,做菜途中不断在切换工序的时候把中间状态的食材放到新的干净的锅/碗里。
锅/碗不够用的时候,老婆就调用了我,collector。

老婆跟我用粗粒度同步,大致是个stop-the-world collection。所以我洗碗的时候老婆就在一旁休息着。

我使用mark-compact算法,
(1) 找出所有要洗的锅碗瓢盆,
(2) 清洗它们,
(3) 想好要按什么顺序把它们放到架子上,
(4) 然后把它们放到架子上。

收集好之后,老婆恢复做菜流程,直到锅/碗再次用完⋯

今天状况有点特殊,老婆用锅用得特别快,我的洗碗速度有点跟不上。于是转入分代式收集模式:
更换速度快的锅/碗先洗,剩余的锅碗瓢盆先放一边。
终于又能跟上了~

疑问:

  1. 既然G1在清除Old generation时候会暂停下来进行evacuation 对象从而清除内存碎片,复制对象相对来说是一个耗时的过程,然而CMS在对Old generation进行回收的时候除了标记垃圾对象位置为空之外,是不会对剩余的的live对象进行移动,从而产生碎片问题,那么CMS不应该比G1更快吗???

​ G1之所以成为低延时,是因为其进行gc的时候并不会选择所有的region,而是会优先选择收益高的region,从而达到 延时相对可控(这是指的是软实时,g1会尽力的去做到目标但不保证一定成功)。不过既然要暂停下来拷贝对象,那么怎么说也肯定有一个延时的最低限制,所以一定不要把 -XX:MaxGCPauseMills 设置的太低,太低可能会导致在g1中垃圾堆积最后导致full gc降低性能。默认200ms是一个比较合理 的。 回到问题中其实对于较小的堆,合适的workload来说,CMS的暂停时间可轻松的低于G1,因为CMS只需要mark对象,并不需要进行碎片整理。通常来说CMS Stop the world最严重的时间是在remark阶段,期间会扫描根集合包括young generation,加入在并发标记的阶段,仍然有很多新的对象分配,那么remark阶段的时间就会越长,young gen越大,remark暂停时间就可能会越长。因此这种workload不太适合CMS。相反如果在并发标记的阶段mutator的分配速率比较温和,然后给足时间让并发的precleaning做好remark的前期工作那么CMS只需要很短的暂停时间,在这种场景下G1很难和CMS相提并论。G1适合更大点的的堆。

参考:

  1. 美团技术团队
  2. https://hllvm-group.iteye.com/group/topic/44381
  3. 做菜与洗碗哈哈哈哈哈

 评论


博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议

本站使用 Material X 作为主题 , 总访问量为 次 。
载入天数...载入时分秒...