垃圾收集底层算法实现
三色标记
在并发标记的过程中,因为标记期间应用程序还继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接指向某个白色对象。
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象只是存在一个引用还没有被扫描过。
- 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
public class ThreeColorRemark {
public static void main(String[] args) {
A a = new A();//开始做并发标记
D d = a.b.d; // 1.读
a.b.d = null; // 2.写
a.d = d; // 3.写
}
}
class A {
B b = new B();
D d = null;
}
class B {
C c = new C();
D d = new D();
}
class C {
}
class D {
}
多标-浮动垃圾
在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个GCRoot引用的对象之前又被扫描过(被标记为非垃圾对象),那么本轮GC不会回收这部分的内存。这部分本应该回收但是没有回收到内存中,被称之为"浮动垃圾"。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才能被清除。
漏标-读写屏障
漏标会导致被引用的对象当成垃圾误删除,这是严重的bug,必须解决。解决方案有:增量更新和原始快照(SATB)
增量新增:就是当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束后,再将这些记录过的引用关系中的黑色对象为跟,重新扫描一次。可以简化理解为:黑色对象一旦插入新的指向白色对象引用后,它就变回灰色对象了。
原始快照:就是当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下拉,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为跟,重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)。
以上物料是对象引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。
- CMS:写屏障+增量更新
- G1,Shenandoah:写屏障+SATB
- ZGC:读写障
为什么G1用SATB?CMS用增量更新?
SATB相对增量更新效率会更高,因为不需要重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的跟对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代码会比CMS高。
记忆集与卡表
在新生代做GCRoots 可达性扫描过程中可能会碰到跨代引用的对象,这种如果又去对老年代再去扫描效率太低。为此,在新生代可以引入记录集的数据结构,避免把整个老年代加入GCRoots扫描范围。事实上并不是新生代,老年代之间才有跨代引用的问题,所有涉及部分区域收集行为的垃圾收集器,典型的如G1都会面临相同的问题。