G1的工作步骤
这一部分,也是耳熟能详的部分,但是忍一忍,马上就要到你高呼:卧槽,牛逼的部分了。
众所周知,一般我们说G1的收集过程分为下面这四个步骤(下面四个步骤的描述来自于《深入理解Java虚拟机(第3版)》):
说实在的,下面的描述确实看的让人很懵逼的。面试的过程中问到这一部分的时候,我相信大多数朋友都是硬背下来的。
所以,本文的目的就是为了让你理解下面这几个阶段的具体过程。
这么说吧,如果看完这篇文章你还是没搞懂上面这几个阶段的话,那你再读一遍。
再读一遍,还是没懂的话,那我这篇文章就算写失败了。
初始标记(Initial Marking):这阶段仅仅只是标记GC Roots能直接关联到的对象并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的Region中创建新对象,这阶段需要停顿线程,但是耗时很短。
而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
并发标记(Concurrent Marking):从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,但是可以与用户程序并发执行。
当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。
筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。
可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。
这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
上面虽然有4个阶段,但是从上帝视角,我们可以把它分为两大部分,或者说从整个算法的角度,我们可以切分为两大部分:
1.Global Concurrent Marking:全局并发标记。
2.Evacuation Pauses:该阶段是负责把一部分Region里的活对象拷贝到空Region里面去,然后回收原本的Region空间。
为什么我敢这样去划分呢?
一部分原因来自这篇论文中:
《Garbage-First Garbage Collection》这篇论文是 sun 实验室在 2004 年发布的第一篇关于 G1 的论文。够权威吧?
该论文中,2.3小节就是介绍 Evacuation Pauses ,2.5小节就是介绍 Concurrent Marking ,下面是部分内容截图:
另一部分原因是 R大 也这样说的(见文末参考资料)。
接下来,要回答读者提出的问题,我们就需要了解全局并发标记阶段。
全局并发标记
这一节就是回答这个问题:用户线程执行的时候不仅修改了对象引用关系,还新分配了新对象,G1 是如何找到并处理这些对象的呢?
要回答这个问题,就涉及到 TAMS 了。前面我引用的书里说:
初始标记(Initial Marking):这阶段仅仅只是标记 GC Roots 能直接关联到的对象并修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的 Region 中创建新对象。
这句话,每个字都能看懂,连在一起读,也品出点儿味道,但是总感觉似懂非懂的样子。
什么是 TAMS?什么是正确可用的 Region?新对象是创建在 Region 中的哪个位置的?
我们先从论文入手,我捡关键点给你说:
1.有两个 bitmap。
2.一个叫 previous,一个叫 next。
3.previous bitmap 是 concurrent marking 阶段完成后的最后一个 bitmap。(有点绕,后面会解释)。
4.next bitmap 是当前将要或正在进行的 concurrent marking 的结果。
5.当标记完成后,两个 bitmap 会交换角色。
1.标记周期的第一个阶段就是清理 next bitmap。
2.然后,初始标记阶段 Stop The World(后面简称STW),目的是标记 GC Roots 能直接关联到的对象。该阶段借助 Minor GC 完成,没有额外的停顿。
3.每个 Region 包含两个 TAMS。
4.一个对应前一轮标记,一个对应下一次标记。
从论文中我们可以知道,G1的Concurrent Marking 用了两个 marking bitmap。
一个 previous Bitmap 记录的是上一轮 Concurrent Marking 后的对象标记状态,因为上一轮已经完成,所以这个bitmap的信息可以直接使用。
一个 next Bitmap 记录的是当前这一轮 Concurrent Marking 的结果。这个bitmap是当前将要或正在进行的 Concurrent Marking 的结果,尚未完成,所以还不能使用。
我们可以假设一次并发标记变成后的 Bitmap(previous Bitmap) 大概长这样:
白色地址之间是可以回收的对象,灰色地址之间是不可以回收的对象。
除了两个 bitmap 外,还有两个 TAMS(top at mark start)。每个 Region 都有两个 TAMS,分别是 previous TAMS 和 next TAMS。
bitmap 和 TAMS 可以用下面的图片来表示:
首先我们可以看到 bottom 和 top 之间是一个 Region 已使用的部分。Top 到 end 之前是一个 Region 未使用的部分。
另外可以看到上面我留了四个问号,接下我们的目的就是填补这些问号。当这些问号被填上之后,所有的问题都会迎刃而解。
两个 Bitmap 和两个 TAMS 是怎么工作的呢?
接下来按照:
初始标记(Initial Marking)
初始标记(Initial Marking)
从图片可以看到初始标记阶段 nextBitmap 是清空状态,没有标记任何存活的对象。
接着我们再次回到书中的描述里,我给你逐字描述清楚:
初始标记(Initial Marking):这阶段仅仅只是标记 GC Roots 能直接关联到的对象并修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的 Region 中创建新对象。
GC Roots 能直接关联到的对象:就是一个 Region 已经使用过的部分,所以在 Bottom 与 top 之间。
修改 TAMS 的值:就是让此时的 prevTAMS 指向 Bottom ,也就是一个 Region 内存地址起始值。让此时的 nextTAMS 指向 Top。Top 实际上就是一个 Region 未分配区域和已分配区域的分界点。
**正确的可用的 Region **:对一个 Region 来说,当上面的 nextBitmap 为空、4个指针都准备就绪后,这个 Region 在下一阶段用户程序并发运行时,就是一个正确的 Region。
下一阶段用户程序并发运行时,在正确的可用的 Region 中创建新对象是什么意思呢?
下一阶段用户程序并发运行时指的就是并发标记阶段。
并发标记(Concurrent Marking)
最终标记(final marking,也叫Remark)
清理阶段(Cleanup)
这四个阶段作图说明