三色标记
在垃圾回收机制并发标记的过程中,因为标记期间应用线程还在继续跑着,对象间的引用实时发生变化,多标和漏标的情况就会发生。这就引入了三色标记的概念,实现的条件就是“是否被访问”来进行三种颜色的标记,分别分为以下三种:
- 黑色:黑色标记的对象代表垃圾回收器已经访问过了,而且这个对象下所有引用的对象都被访问过,说明他是安全存活的,如果有其他对象引用指向这个黑色对象,则无需再重新扫描。黑色对象不可能直接指向某个白色对象。
- 灰色:灰色标识的对象代表已经被垃圾回收器访问过,但这个对象下面至少有一个对象还没有被扫描过。
- 白色:白色标识的对象代表还没有被扫描过,说明可达性分析刚刚开始,所有的对象都是白色的,如果在分析结束时仍然是白色对象,则代表该对象不可达,可以被回收掉。
多标-浮动垃圾
在并发标记过程中,如果这个方法运行结束导致一部分的局部变量被gtroot,这个gcroot引用的对象之前又被扫描过,那么这次gc不会将其回收。这部分应该回收的没有被回收掉的叫“浮动垃圾”。浮动垃圾不影响回收的准确性,待下一轮的回收机制触发是也会被清除。针对并发标记开始后产生的新对象,通常也是直接标记为黑色,代表本次不清除,也算是浮动垃圾的一部分。
漏标-读写屏障
漏标是很严重的问题,会导致对象当做垃圾误删除,针对这种问题也有两种解决办法:
“增量更新”和“原始快照”(STAB)。
- 增量更新:当黑色对象插入新的指向白色对象关联关系时,就会把这个新插入的引用记录,等到并发扫描后再次把这些记录的引用关系中黑色的对象为初始点再扫描一次。就是黑色对象一单插入对白色的引用后,他就变成灰色对象了。
- 初始快照:当灰色对象要删除指向白色对象引用的关联时,先将这个引用记录下来,等扫描结束后再将这些记录的引用关系中会对象为初始点重新扫描一次。这样做的目的就是将扫描到白色对象直接变成黑色。为了解决这种对象再本轮gc清理中存活下来,等下次gc时重新扫描,这个对象也可能是浮动垃圾。
注:上面两种方式都是通过“写屏障”实现的。
写屏障
写屏障就是再赋值操作前,加一些处理,原理与AOP的概念很相似。
pre_write_barrier(tail); // 写屏障‐写前操作
tail = new_value;
post_write_barrier(tail, value); // 写屏障‐写后操作
读屏障
读屏障为读取前进行操作,读取表链成员时一律记录下来。
“可达性分析”的垃圾回收器都借鉴了三色标记的算法思想,尽管实现的方式不太相同,比如白色、黑色集合一般不会出现、灰色集合可以通过栈、队列、缓存等实现。
对于读写屏障,并发标记时漏标的处理方案如下:
CMS:写屏障+增量更新
G1:写屏障+(初始快照)STAB
最后,我们思考一下为什么G1用STAB,CMS用增量更新?
这个问题并没有什么标准而权威的答案,我对此的理解是:SATB相对增量更新的效率是高的,因为不需要重新标记的解决再次深度扫描被删除引用的对象,而CMS对增量引用的根对象会深度扫描。G1因为很多对象都在不同的小格子里(region),CMS就一块老年代区域,重新深度扫描的话G1的代价太大,所有G1算则了STAB不深度扫描,就是简简单单的做个标记,等下一轮的GC时在进行深度扫描。