三色标记垃圾收集器在并发标记的过程中,标记执行过程中应用线程仍然并行运行,对象之间的引用关系改变了所有time,garbage标记过程中,回收器容易出现多标和漏标(其实多标和漏标统称为误标)。针对这个问题,我们使用“三色标记”作为辅助推导的理论工具,在垃圾遍历对象引用的过程中,将“根据是否被访问”的条件标记为三种颜色集电极。黑色:表示该对象已经被垃圾收集器访问过,所有对该对象的引用都已扫描。幸存下来是安全的,如果有其他物体指向黑色物体,则不需要重新扫描。黑色物体不能直接(不通过灰色物体)指向白色物体。灰色:表示它已经被垃圾收集器访问过,但是至少有一个指向这个对象的引用还没有被扫描到。白色:表示该对象还没有被垃圾收集器访问过。显然,在可达性分析开始时,所有对象都是白色的,如果分析结束时对象仍然是白色,则说明该对象不可达。三色标记示例代码(例子来自网络):publicclassTriColorMarking{publicstaticvoidmain(String[]args){Aa=newA();//开始并发标记Dd=a.b.d;//1.读取a.b.d=null;//2.写a.d=d;//3.写}}classA{Bb=newB();Dd=空;}B类{Cc=newC();Dd=newD();}classC{}classD{}例子的简单解释:1.newA()时会建立引用关系A->B,B->C,B->D;2、我们做并发标记的时候,垃圾回收器已经访问了A、B、C、D,最后把它标记为黑色。但是这时候程序执行a.b.d=null表示实际上并没有引用D。理论上,D对象是可以回收的。这种情况产生了“漂浮垃圾”。3、当我们发现D没有被引用时,就标记为白色,但是标记完成后,发现a.d=d。添加了对象引用。如果d被回收了,程序会报错,这是肯定不行的。这是一个典型的“多标准”场景。接下来,我们将分析并发标记过程中出现的缺失和多标签场景。在并发标记的过程中,将原本死亡的对象标记为存活的对象,这就是缺失的标记。会产生浮动垃圾,需要在下次GC时清理掉。生成过程:程序删除所有从灰色对象到白色对象的直接或间接引用标记。从图1到下图的过程中,浮动垃圾其实是可以接受的,只会影响垃圾收集器的效率,或者说收集率。.在并发标记过程中,多标记将原本存活的对象标记为需要回收的对象。生成过程:程序从黑色物体向白色物体插入一个或多个新的参考标记。这种情况从图1到下图都是不可接受的。如果正在使用的程序对象被JVM回收,将导致程序运行错误不可接受,并可能导致严重的错误。解决缺标和多标的方案有两种:增量更新(IncrementalUpdate)和原始快照(SnapshotAtTheBeginning,STAB)增量更新(IncrementalUpdate),这是一个并发的标记过程,当黑色对象插入一个指向白色的新引用关系,记录插入的引用,并发标记结束后,以记录的引用关系中的黑色对象为根,再次重新扫描。为了简化理解,一旦黑色对象新插入了对白色对象的引用,它就变成了灰色对象。在原始快照(SnapshotAtTheBeginning,STAB)的并发标记过程中,当灰色对象要删除白色对象的引用关系时,会记录这次需要删除,并发扫描完成后,这些记录的引用关系中的灰色对象为根,再次扫描,这样可以扫描到白色对象,直接将白色对象标记为黑色(目的是让这个对象在本轮GC清理中存活下来,而等到下一轮GC时重新扫描时,这个对象也可能变成浮动垃圾)总之,无论是插入还是删除引用关系记录,虚拟机的记录操作都是通过writebarrier来实现的。写屏障(WriteBarrier)JVM通过写屏障(WriteBarrier)维护卡表,卡表是内存集的实现。内存集用于缩小GCRoot的扫描范围。我们只需要在GC时过滤掉卡表的脏(Dirty)元素,找到卡页的特定内存块,放入GCRoot中进行扫描即可。这是一个大概的过程,后面再说,先有个印象吧。说回writebarrier,下面是一个对象赋值操作:,oopnew_value){*field=new_value;//赋值操作}writebarrier可以看作是对虚拟机执行对象字段赋值的拦截,类似于SpringAOP的切面思想。voidoop_field_store(oop*field,oopnew_value){pre_write_barrier(field);//预写屏障*field=new_value;post_write_barrier(字段,值);//post-writebarrier}writebarrier,SATB作为对象B的成员变量当引用发生变化时,比如引用消失(a.b.d=null),我们可以使用writebarrier来记录对象D的引用B的原始成员变量:voidpre_write_barrier(oop*field){oopold_value=*field;//获取旧值remark_set.add(old_value);//记录原始引用对象}writebarrier,增量更新当对象A的成员变量的引用发生变化时,比如增加了一个新的引用(a.d=d),我们可以使用writebarrier,记录新的成员变量引用A的对象D:voidpost_write_barrier(oop*field,oopnew_value){remark_set.add(new_value);//记录新引用的对象}readbarrier(LoadBarrier)oopoop_field_load(oop*field){pre_load_barrier(field);//读取屏障-在返回*field之前读取;*字段){oopold_value=*字段;remark_set.add(old_value);//记录读取的对象}记忆集和卡片表(RememberedSetAndCardTable)垃圾收集器在新生代Set中创建记忆集(RememberedSetAndCardTable)数据结构,用于避免扫描GC根整个老一代。其实,不仅仅是新生代和老一代之间的代际参照问题。所有涉及PartialGC行为的垃圾收集器,例如G1、ZGC和Shenandoah收集器,都会面临同样的问题。问题。内存集是一个抽象的数据结构,记录了从非收集区到收集区的指针集合。Hotspot使用一种叫做“卡片表”(CardTable)的方法来实现内存设置,这也是目前最常用的方法。卡表和内存集合的关系可以类比Java语言中HashMap和Map的关系。卡表实现为一个字节数组:CARD_TABLE[],每个元素对应一个标识的内存区域和一个特定大小的内存块,称为“卡页”。Hotsport卡片页面的大小为2^9,即512字节。一个卡片页面可以包含多个对象。只要卡页中一个或多个对象的字段存在跨代引用,对应卡表的元素标识就变为1,说明该元素是脏的,否则为0。GC时,你只需要过滤掉card表中的脏元素,加入到GCRoot中。卡表的维护是如何使卡表变脏的,即引用字段赋值时,如何将卡表对应的标识符更新为1。Hotspot使用写屏障来维护卡表的状态。收集器CMS采用的方案:writebarrier,增量更新G1,Shednandoah:writebarrier+STABZGC:readbarrier为什么G1用SATB,CMS用增量更新?因为SATB比增量更新更高效(当然,SATB可能会造成更多的浮动垃圾),因为在remarking阶段不需要再次深度扫描被删除的引用对象,CMS会在根对象上做一次深度扫描增量更新的,G1因为很多物体位于不同的区域,CMS是老年代的区域,G1在深度上重新扫描物体的代价会比CMS高,所以G1选择SATB不扫描物体在深度depth,只是简单的标记一下,等待下一轮GC做deepscan。参考资料1.《深入理解 JAVA虚拟机-第三版》周志明
