三色标记(Tri-Color-Marking),垃圾收集器在并发标记的过程中,执行标记期间应用线程还在并行运行,对象间的引用关系时刻发生变化,垃圾收集器在标记过程中就容易发生多标和漏标(其实多标和漏标我们统称为误标)。,针对这一问题我们通过 “三色标记 (Tri-Color-Marking)” 作为理论工具来辅助推导,将垃圾收集器遍历对象引用的过程中,“按照是否访问过” 这个条件标记成三种颜色。,,三色标记示例代码(示例来源于网络):,例子的一个简单说明:
1. 在 new A() 的时候会创建引用关系 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 回收,会导致程序运行错误,是不可以接受的会导致严重 BUG。,解决漏标和多标分别有两种解决方案:增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning, STAB),这并发标记过程中,当黑色对象插入了新的指向白色引用关系时,就将这个插入引用记录下来,并发标记结束后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。简化理解, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变成灰色对象。,这并发标记过程中,当灰色对象要删除白色对象的引用关系时,就将这个需要删除的记录下来,在并发扫描结束后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次,这样就能扫描到白色对象,将白色的对象直接标记为黑色(目的就是为了让这种对象在本轮 GC 清理中能够存活下来,待下一轮 GC 的时候重新扫描,这个对象也可能成为浮动垃圾) 总之,无论是引用关系记录插入还是删除,虚拟机的记录操作都是通过写屏障来实现的。,JVM 通过写屏障(Write Barrier)来维护卡表,卡表是记忆集的实现。记忆集是用来缩小 GC Root 的扫描范围,我们在 GC 的时候只需要去过滤卡表变脏(Dirty)的元素,找到具体一块卡页内存块,放入 GC Root 中一块扫描。这是大概的一个流程,后续会讲到,先有一个印象。再回到写屏障,下面是一个对象赋值操作:,写屏障可以看做是虚拟机执行对象字段赋值的一个拦截,类比 Spring AOP 的切面思想。,当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B原来成员变量的引用对象D记录下来:,当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D记录下来:,读屏障是直接针对第一步:D d = a.b.d,当读取成员变量时,一律记录下来:,垃圾收集器在新生代建立了记忆集(Remembered Set)的数据结构,用来避免把整个老年代的 GC root 扫描一遍。事实上并不只是新生代、 老年代之间才有跨代引用的问题, 所有涉及部分区域收集(Partial GC) 行为的垃圾收集器, 典型的如G1、 ZGC 和 Shenandoah 收集器, 都会面临相同的问题。记忆集是一种记录非收集区域指向收集区域的指针集合抽象的数据结构。,Hotspot 中使用一种叫做 “卡表” (Card Table)的方式来实现记忆集,也是目前最常用的一种方式。卡表和记忆集的关系,可以类比 Java 语言中 HashMap 和 Map 之间的关系。卡表是一个字节数组实现:CARD_TABLE[], 每个元素都对应着一个标识的内存区域一块特定大小的内存块,称为“卡页”。Hotsport 卡页的大小是 2^9 也就是 512 字节。,,一个卡页中可以包含多个对象,只要卡页内一个或者多个对象的字段存在跨代引用,其对应的卡表的元素标识就变成了1,表示该元素变脏,否则为 0。GC 时,只需要筛选卡表中变脏的元素加入到 GCRoot 中。,如何让卡表变脏,即发生引用字段赋值时,如何更新卡表对应的标识为 1。Hotspot使用写屏障维护卡表状态。,CMS : 写屏障,增量更新 ,G1,Shednandoah: 写屏障 + STAB ,ZGC:读屏障,为什么 G1 采用 SATB,CMS 使用增量更新? ,因为SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量更新的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。,1.《深入理解 JAVA 虚拟机-第三版》周志明
文章版权声明
1 原创文章作者:cmcc,如若转载,请注明出处: https://www.52hwl.com/17524.html
2 温馨提示:软件侵权请联系469472785#qq.com(三天内删除相关链接)资源失效请留言反馈
3 下载提示:如遇蓝奏云无法访问,请修改lanzous(把s修改成x)
4 免责声明:本站为个人博客,所有软件信息均来自网络 修改版软件,加群广告提示为修改者自留,非本站信息,注意鉴别