开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第10天,点击查看活动详情
垃圾回收原理
上两节介绍了垃圾回收算法和目前使用的所有垃圾回收器,本节将围绕垃圾回收原理介绍,包括三色标记,读写屏障,卡表等,同时也会根据这些原理继续深入介绍ZGC垃圾回收器。
三色标记
在并发标记过程中,对象的状态还在改变,有可能会发生漏标和多标。
黑色:表示垃圾回收器访问过,并且这个对象所有的引用已经扫描过。同时也代表着如果有别的对象引用到黑色的对象,就不必重新进行扫描。
灰色:表示垃圾回收器访问过,至少有一个对象引用没有被扫描。
白色:表示垃圾回收器没有访问过,在标记的开始所有对象都是白色的。在标记结束后,如果对象还是白色,那么表示该对象不可达。
上一节讲到的ZGC Mark1和Mark2就是用来表示黑灰白的,00表示黑色,01表示灰色,11表示白色。
多标
多标并不算是一个很严重的问题,虽然它会产生一些浮动垃圾,但是不会产生致命的影响。如果在并发标记阶段方法运行结束,有局部GC Root的对象被销毁,那么这个GC Root对象引用的其他被扫描过对象都不会被销毁,这一部分是应该被销毁而没被销毁的部分,被称为“浮动垃圾”。
另外的一部分浮动垃圾是在并发标记过程中产生的新对象,这些新对象通常都会全部当成黑色对象,也就是本轮不会进行清除。
漏标
漏标会导致被引用的对象当成垃圾误删,是一个很严重的问题。
只有一种情况会导致漏标,就是在并发标记过程中,一个灰色对象删除了白色对象的引用,并且这个白色对象被一个黑色对象引用了。
对于这种情况,我们需要从灰色对象或者黑色对象的角度去解决问题。那么会有两种解决方案,分别是增量更新和原始快照(SATB)。
增量更新:如果有黑色对象插入了新的对白色对象的引用,将新的引用记录下来,在重新标记阶段以被标记的黑色对象为根再重新扫描一次,也就是相当于如果有新的白色对象加入,将引用该对象的黑色对象变为灰色对象。
原始快照(SATB):当灰色对象删除对白色对象的引用时,将这个删除的引用记录下来,在重新标记阶段将这些记录过引用关系的灰色对象为根再重新扫描一次,因为之前记录过删除的引用,因此能够扫描到白色对象,将白色对象置为黑色。这种方法是为了在下一次GC再去回收白色对象,这种方法避免了漏标但是会产生一定的浮动垃圾。
虽然两种方案都是可行的,但是不同的垃圾回收器会选择不同的方案,CMS会选择增量更新,G1会选择SATB,因为CMS服务的是老年代,重新扫描的话不会引起跨代的问题,G1有很多个Region,很有可能会跨Region访问,因此使用SATB不深度扫描只简单标记一下。
无论是增量更新还是原始快照都借助写屏障去进行。
屏障
读写屏障是GC中重要的实现功能手段,其中写屏障实现了上面所说的增量更新和原始快照,而读屏障则帮助ZGC实现对象地址修正。
写屏障
一个正常的对象赋值应该是这样的
void filed_store(oop* field, oop new_value) {
*field = new_value;
}
而加上写屏障则是(有点类似Spring的AOP)
void filed_store(oop* field, oop new_value) {
pre_write_barrier(field);
*filed = new_value;
post_write_barrier(field);
}
那么如STAB,有一个引用要消失就可以
void pre_write_barrier(opp* field) {
opp old_value = field;
remark_set.add(old_value); //记录原值
}
增量更新则是要新增引用
void post_write_barrier(opp* field) {
remark_set.add(field); //记录新引用的对象
}
读屏障
读屏障主要应用在ZGC中,正常数据读取应该是
void filed_read(oop* field) {
return field;
}
加上读屏障则是
void filed_read(oop* field) {
pre_load_barrier(field);
return field;
}
ZGC加读屏障是为了在读取数据的同时,检查颜色指针,查看这个对象是否因为GC而发生位置改变,如果发生了位置改变则更新引用地址。
void pre_load_barrier(oop* field) {
if (check_print_pointer(field.print)){
change_address(field);
}
return field;
}
记忆集与卡表
在新生代做GC Roots可达性扫描时,有可能会碰到跨代引用的对象。又或者涉及部分区域如Region收集的垃圾回收器也会面临相似的问题。我们需要引入记忆集的概念去记录从非收集区到收集区的指针集合,避免将整个非收集区都扫描一次。
在垃圾回收过程中,只需要判断非收集区的记忆集是否存在指向收集区域的指针即可。
hotspot是通过卡表的方式去实现记忆集,通过一个字节数组CARD_TABLE[]实现。HotSpot通过写屏障去维护卡表的状态,如果有卡表变脏则将卡表标识标1,在GC时只要筛选卡表中的脏数据加入GC Roots即可。
安全点与安全区域
安全点是代码中的一些特定位置,当线程运行到这个位置时它的状态才是确定的,比如GC,并不是说我像什么时候GC都行,我们需要等待所有线程都运行到安全点才能触发。
安全点包括一下四种:
1.方法返回之前
2.调用某个方法之后
3.抛出异常的位置
4.循环的末尾
在垃圾线程需要中断线程时,仅简单设定一个标志位,每个线程会不停地询问这个标志位,如果标志位为真,代表我现在要进行垃圾回收了,那么线程就在最近的安全点中断挂起。
那如果说一个线程处于sleep状态,那么它就没办法运行到安全点上,那么就引出了安全区域,如果在一段代码中引用不会发生改变,那么他就是安全区域,在这个区域内GC也是安全的。
ZGC回收过程
把基本的垃圾回收原理了解完后,可以看一下ZGC实际的回收过程
并发标记(Concurrent Mark):和G1一样,遍历对象图进行可达性分析,Mark Start和Mark End会造成短时间的停顿,在三色标记过程中,G1是操作对象头,ZGC则是操作颜色指针中的Mark1以及Mark2。
并发预备重分配(Concurrent Prepare for Reloc.):根据查询的条件去扫描要清理哪些Region,并组合成重分配集。ZGC会扫描所有的Region,减少G1维护记忆集的成本。
并发重分配(Concurrent Relocate):ZGC的核心过程,在将存活对象复制到新的Region。并为重分配集的每一个Region维护转发表,记录旧对象到新对象的转向关系。如果用户线程访问到了重分配集中的对象,那么前面提到会用读屏障的截获,再根据转发表的转发到新对象,并及时修改引用值,这就是ZGC的自愈能力。
并发重映射(Concurrent Remap):修正重分配集中的旧引用,释放转发表,但是这个由于ZGC的自愈能力所以不是非常迫切,可以合并到并发标记过程中。
ZGC可以定时触发GC,也可以去计算内存将要耗尽的时间点,耗尽之前触发GC。
总结
本节介绍了垃圾回收原理,包括三色标记,屏障记忆集于卡表,并且总结了ZGC的回收过程。下一节将结合前面讲到的知识去了解在实际开发中应该要通过什么操作去对项目进行分析。
感谢观看!