G1垃圾收集器读书总结
G1垃圾收集器早就听说了,但是一直没用过。因为在公司申请的服务器上默认垃圾收集器是CMS所以,一直没真正用过G1。用了一段时间后,发现G1的FGC次数确实比CMS少了很多(测试环境,3G堆空间)。
所以大概经历了三周的时间一直在学习JVM以及G1的相关知识。本文就是记录下G1的读书笔记。
G1 中的region
四种,Eden Survivor Old Humongous
对于Humongous 这种Region被设计用来保存超过Region的50%空间的对象,它们存储在一系列连续的Region中。通常来说,超大对象只会在最终标记结束后的清理阶段(cleanup)才会被回收,或者发生FullGC时。
Region数量最多不会超过2048个,每个Region的大小在1~32M之间,且必须是2的N次方。这就意味着,如果堆内存少于2048M(2G),那么Region数量就会少于2048个。
G1能解决的问题
-
对于大于8G的堆,回收的很快,因为不会对整个队进行FGC
-
自适应算法,能根据用户配置的启动参数指定的暂停时间,尽可能做到按照配置的暂停时间内完成一个MIXED GC
G1的重要概念
-
SATB
-
write barrier
-
Remembered Set
-
Card Table
SATB,Card Table和Write Barrier
在垃圾回收的过程中,标记阶段最怕的就是出现错标和漏标。漏标还好说,可以在下一次GC中回收。而且无论是CMS和G1都不可能完全解决漏标问题。
但是错标是千万不能的。为了解决错标的问题。接产生了write barrier以及对应的satb和card table。
write barriar分为两种:pre 和 post ,SATB以来的就是pre write baririer,而post 使用过card table和remembered set(简称rset)实现的
-
pre:SATB
-
post:card table 和rset
那什么是写屏障呢,代码示例
void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field); // pre-write barrier: for maintaining SATB invariant
*field = new_value; // the actual store
post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference
}
就是当有一个对象的引用类型的字段在进行赋值的时候,此时写屏障就发生了作用。
我们先说说post 写屏障吧
POST WRITE BARRIER
当对象的某一个字段写完成,表示该字段指向了新的引用。此时写屏障做的是就是这样的
-
void post_write_barrier(oop* field, oop new_value) {
-
uintptr_t field_uint = (uintptr_t) field;
-
uintptr_t new_value_uint = (uintptr_t) new_value;
-
uintptr_t comb = (field_uint ^ new_value_uint) >> HeapRegion::LogOfHRGrainBytes;
-
-
if (comb == 0) return; // field and new_value are in the same region
-
if (new_value == null) return; // filter out null stores
-
-
// Otherwise, log it
-
*volatile jbyte* card_ptr = card_for(field);* // get address of the card for this field
-
-
// in generational G1 mode, skip dirtying cards for young gen regions,
-
// -- young gen regions are always collected
-
// if (*card_ptr == g1_young_gen) return;
-
-
if (*card_ptr != dirty_card) {
-
// dirty the card to reduce the work for multiple stores to the same card
-
*card_ptr = dirty_card;
-
-
JavaThread::current()->dirty_card_queue->enqueue(card_ptr);
-
}
-
}
如果有region之间的引用变更,必须是OLD-->OLD ,OLD --> YOUNG. YOUNG -->OLD无需考虑,无需记录。
从该代码可以看出来,所谓的dirty表示的是 该card中的某个对象的成员变量的引用发生了改变,指向了新的引用。那么该成员变量field所在的catd变为dirty。
注意这句,JavaThread::current()->dirty_card_queue->enqueue(card_ptr);
把这个脏卡放到一个queue里,等待着Concurrent refinement threads去处理,处理的过程也就是更新rset的过程。
接下来我们说说RSET。
RSET的作用就是相当于字典的目录,是为了避免扫描整个region而设计的。每个region都有其RSET,
RSET的结构类似hashmap,我引用一段R大的原文:
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的关系。
这就好理解了,本region中哪些对象被引用了只需要查询rset就够了。
把这个过程串起来一下:
1 post写屏障把脏card压入queue,queue满了会存入到全局的一个大queue里,等待着refine线程处理。
2 在remark阶段refine线程开始处理queue
3 refine线程拿到脏页后,得到对象的field,同时能得到field所引用的其他的region regionB 中的对象。现在能够拿到的是regionB,regionA的脏页,脏页里的field机器引用
4 这样就可以update regionb的rset了。
这就是rset的update过程。
而RSET被使用的阶段是evacuation阶段,通过RSET可以直接使用copy算法,找出来应该保留的对象并拷贝到其他region。
而RSET被使用的阶段是evacuation阶段,通过RSET可以直接使用copy算法,找出来应该保留的对象并拷贝到其他region。
PRE WRITE BARRIER
介绍完了post write barrier,再来看下pre write barrier。我们之前说,标记过程中最怕的是错标和漏标,这里pre write barrier就是解决错标的手段。还是引用下大佬R大的原话
前面提到SATB要维持“在GC开始时活的对象”的状态这个逻辑snapshot。除了从root出发把整个对象图mark下来之外,其实只需要用pre-write barrier把每次引用关系变化时旧的引用值记下来就好了。这样,等concurrent marker到达某个对象时,这个对象的所有引用类型字段的变化全都有记录在案,就不会漏掉任何在snapshot里活的对象。当然,很可能有对象在snapshot中是活的,但随着并发GC的进行它可能本来已经死了,但SATB还是会让它活过这次GC。
那么jvm是怎么做的是,看下源码
void pre_write_barrier(oop* field) {
oop old_value = *field;
if (old_value != null) {
if ($gc_phase == GC_CONCURRENT_MARK) { // SATB invariant only maintained during concurrent marking
$current_thread->satb_mark_queue->enqueue(old_value);
}
}
}
说明SATB队列是存在于线程中的,也是进入一个queue中以便后续处理。
现在我抛出来一个问题,既然都有了rset为啥还要satb呢?我当初也有这个疑问,但是重新看了下R大发出来的帖子,又有了新的认识。
因为,G1的标记是并发标记,而且post写屏障也是放到queue里异步处理,难以保证标记的顺序。而且G1采用的是三色标记法,而且不同于CMS,在比较过程中不会变更已标记的颜色。也就是说黑的节点,就是不会再次扫描的。这也是G1比CMS快的原因之一。因为CMS会把黑节点变成灰节点,这就导致remark节点要扫描整个堆区,而G1不会,扫过了就绝不会再次扫描,节省时间,只不过浮动垃圾会比CMS还多。
总结SATB和RSET,CARD TABLE
1 SATB使用阶段是并发标记周期,防止有对象被错标
2 RSET使用阶段是evacuation,清理阶段
Region内的结构
对应的,每个region都有这么几个指针:
|<-- (1) -->|<-- (2) -->|<-- (3) -->|<-- (4) -->|
bottom prevTAMS nextTAMS top end
其中top是该region的当前分配指针,[bottom, top)是当前该region已用(used)的部分,[top, end)是尚未使用的可分配空间(unused)。
(1): [bottom, prevTAMS): 这部分里的对象 <u>存活信息可以通过prevBitmap</u> 来得知
(2): [prevTAMS, nextTAMS): 这部分里的对象在第n-1轮concurrent marking是隐式存活的
(3): [nextTAMS, top): 这部分里的对象在第n轮concurrent marking是隐式存活的
这里提到了bitmap的概念,在标记阶段,每一个region的标记是需要rset和bitmap一起使用的.liveness bitmap其实就是个外置的mark bit啦,没啥神奇的。
带有marking阶段的GC,要记录某个对象是否已经被mark,可以在对象头上借用一个bit,也可以在一个外部的bitmap里记录。G1的global concurrent marking用的是外部的bitmap方式.就是表示对象是否被标识。
还有个小细节:在有liveness bitmap指明对象生死情况的地方,G1可以借助liveness bitmap来减少card table引致的floating garbage。具体是: 假如region A的RSet记录着有来自region B的card 123有incoming reference,而这个card 123正好在第n-1轮global concurrent marking已经记录下了对象生死在prev liveness bitmap里,那么在扫描card 123的时候只有prev liveness bitmap说还活着的对象的字段会被扫描,bitmap说已经死掉的对象则不会被扫描。
G1的回收周期
以下是R大的原文
一个假想的混合的STW时间线:
启动程序 -> young GC -> young GC -> young GC -> young GC + initial marking (... concurrent marking ...) -> young GC (... concurrent marking ...) (... concurrent marking ...) -> young GC (... concurrent marking ...) -> final marking -> cleanup -> mixed GC -> mixed GC -> mixed GC ... -> mixed GC -> young GC + initial marking (... concurrent marking ...) ...
其中可以看到young GC可以单独运行,也可以搭上initial marking在同一个STW中运行。 “young GC (... concurrent marking ...)”的行里面young GC还是单独运行的,跟后台正在进行的concurrent marking不冲突。
可以看得出来,mixed gc会进行多次

该表来自 https://blog.csdn.net/renfufei/article/details/41897113
下表是老年代的GC步骤,但是这里面也包含了young gc。
| 阶段 | 说明 |
|---|---|
| (1) 初始标记(Initial Mark) | (Stop the World Event,所有应用线程暂停) 此时会有一次 stop the world(STW)暂停事件. 在G1中, 这附加在(piggybacked on)一次正常的年轻代GC. 标记可能有引用指向老年代对象的survivor区(根regions). |
| (2) 扫描根区域(Root Region Scanning) | 扫描 survivor 区中引用到老年代的引用. 这个阶段应用程序的线程会继续运行. 在年轻代GC可能发生之前此阶段必须完成. |
| (3) 并发标记(Concurrent Marking) | 在整个堆中查找活着的对象. 此阶段应用程序的线程正在运行. 此阶段可以被年轻代GC打断(interrupted). |
| (4) 再次标记(Remark) | (Stop the World Event,所有应用线程暂停) 完成堆内存中存活对象的标记. 使用一个叫做 snapshot-at-the-beginning(SATB, 起始快照)的算法, 该算法比CMS所使用的算法要快速的多. |
| (5) 清理(Cleanup) | (Stop the World Event,所有应用线程暂停,并发执行) 在存活对象和完全空闲的区域上执行统计(accounting). (Stop the world) 擦写 Remembered Sets. (Stop the world) 重置空heap区并将他们返还给空闲列表(free list). (Concurrent, 并发) |
| (*) 拷贝(Copying) | (Stop the World Event,所有应用线程暂停) 产生STW事件来转移或拷贝存活的对象到新的未使用的heap区(new unused regions). 只在年轻代发生时日志会记录为 `[GC pause (young)]`. 如果在年轻代和老年代一起执行则会被日志记录为 `[GC Pause (mixed)]`. |
浙公网安备 33010602011771号