HotSpot的算法细节
可达性分析算法
以一系列“GC Roots”根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
从GC Roots开始向下进行引用搜索,如果某对象和任何GC Root没有关联,则认为该对象不再被使用。
GC Roots
- 虚拟机栈中引用的对象,对应虚拟机方法栈中当前执行的方法所使用的参数,局部变量,临时变量等。
- 方法区中:类的静态变量,常量
- 本地方法栈中引用的对象
记忆集与卡表
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
卡表:是记忆集的一种实现形式,采用的是“卡精度”的方式实现记忆集。卡精度指精确到一块内存区域(该内存区域又称为“卡页”),这个区域内有跨代指针的话,就将其标识出来(实际是使用的0/1标识);
在进行GCROOTS扫描时,同时再去筛选卡表中变脏的元素(扫描指定的内存区域块),可以快速定位到关联区域,将跨区引用的对象一起加入GCROOTS扫描。
写屏障
写屏障主要解决的是卡表元素的维护更新,即处理卡表变脏的问题。
何时变脏:其他分代区域有对象引用了该区域对象时,其对应的卡表元素则变脏。
写屏障的处理类似于一个AOP切面,即在本区对象被引用时,添加了Around环绕通知,可在引用赋值前后添加写前屏障和写后屏障。通常虚拟机在写后屏障中增加维护卡表的操作。
卡表在高并发场景下还存在伪共享的问题,由于64个卡表共享一个缓存行,当多个线程更新同一缓存行数据时,会出现并发更新影响性能的情况。
同时虚拟机提供-XX:+UseCondCardMark参数配置,开启该参数则会预先检查该卡表是否已变脏,再行更新的策略。对未变脏的进行更新,已经变脏的卡表不再更新。开启该参数增加异常额外判断的开销,但可以避免伪共享的问题
SATB(snapshot at the beginning)原始快照
沿着GCRoots进行并发扫描时,通常用户线程也在并发执行。这时会面临着已经被垃圾收集器扫描的对象图被用户线程更改的情况。
情况一:某些对象被用户线程断开了引用,其实该对象已经成为垃圾,现在仍被垃圾收集器标识为存活
情况二:某些被垃圾收集器标识为垃圾的对象被用户线程重新引用,导致存活对象被垃圾收集器回收了
综合以上两种情况可知,情况一其实可以容忍,只是程序产生了一些浮动垃圾,待下一次垃圾收集时可以一并回收。情况二把原本存活的对象标记为了死亡,则会造成程序的致命错误。
关于情况二即并发扫描时的对象消失问题,不同的垃圾收集器的解决办法不同。CMS中使用增量更新的方式,G1和shenandoah采用原始快照的方式。
CMS使用增量更新的方式:
当有黑色对象插入指向白色对象的引用时,就将这个引用关系记录下来。在并发扫描结束后(最终标记),以这些黑色对象为根再次进行扫描一次。
垃圾收集器扫描完成的对象引用了一个未被扫描过(新创建或已断开引用)的对象,会将该引用关系记录下来,在最终标记阶段再以该新创建的对象为根,再次进行扫描一次。
G1和shenandoah的原始快照方式:
当灰色对象要删除一个指向白色对象的引用时,就将该要删除引用关系记录下来。在并发扫描结束再以这些灰色对象为根进行扫描一次。这种做法最终导致无论关系删除与否,都会按照垃圾收集器开始扫描的那一刻的对象图来进行对象搜索。
TAMS(Top at mark start)标记顶部
G1收集器中的概念,G1为每个region设计两个TAMS的指针,用于在并发回收阶段新的对象的分配。在回收阶段新对象分配的内存地址将落在region的两个TAMS指针之间。G1默认在两个指针之间的对象是存活的,不对其进行垃圾回收。