Mark-Sweep 算法
在 Mark 阶段,垃圾回收器遍历活跃对象,将它们标记为存活。在 Sweep 阶段,回收器遍历整个堆,然后将未被标记的区域回收。
Mark 阶段的核心任务是从根引用出发,根据引用关系进行遍历,所有可以遍历到的对象就是活跃对象。我们需要一边遍历,一边将哪些对象是活跃的记录下来。当遍历完成以后,我们就找到了所有的活跃对象。
Sweep 阶段要做的事情就是把非活跃对象,也就是垃圾对象的空间回收起来,重新将它们放回空闲链表中。具体做法就是按照空间顺序,从头至尾扫描整个空间里的所有对象,如果一个对象没有被标记,这个对象所占用内存就会被添加回空闲链表。
Mark-Sweep 算法回收的是垃圾对象,如果垃圾对象比较少,回收阶段所做的事情就比较少。所以它适合于存活对象多,垃圾对象少的情况。
分代垃圾回收算法
对于存活时间比较短的对象,我们可以用 Scavenge 算法回收;对于存活时间比较长的对象,就可以使用 Mark-Sweep 算法。这就是分代垃圾回收算法产生的动机。
在年轻代的幸存者空间中,对象都是紧密排列的,所以 B 和 D 会靠在一起。而在老年代空间,由于对象是从空闲区域中分配的,所以 A 和 F 不一定是靠在一起的。
记录跨代引用的集合就是记录集(Remember Set,RS)。
在进行对象的域赋值时,我们要先做以下三个判断:
- 被引用的对象是否在年轻代;
- 发出引用的对象,也就是引用者,是否在老年代;以上两点都满足,就说明产生了跨代引用;
- 检查记录集中是否已经包含了 obj。
如果以上三点都满足,就将 obj 添加到记录集中。还有一种情况可能产生跨代引用,那就是晋升。如果这个对象有引用年轻代对象,就把这个晋升后的对象添加到记录集中。
Card table:提升写屏障效率
Hotspot 的分代垃圾回收将老年代空间的 512bytes 映射为一个 byte,当该 byte 置位,则代表这 512bytes 的堆空间中包含指向年轻代的引用;如果未置位,就代表不包含。这个 byte 其实就和位图中标记位的概念很相似,人们称它为 card。多个 card 组成的表就是 Card table。一个 card 对应 512 字节,压缩比达到五百分之一,Card table 的空间消耗还是可以接受的。
Hotspot 中 CMS 的实现
- 初始标记阶段,标记老年代中的根对象,因为根对象中包含从栈上出发的引用等比较敏感的数据,并发控制难以实现,所以这一阶段一般都采用 Stop The World 的做法。这里一般不遍历年轻代对象,也就是不关注从年轻代指向老年代的引用。
- 并发标记阶段,比较复杂
- 重标记阶段,这一阶段会把年轻代对象也进行一次遍历,找出年轻代对老年代的引用,并且为并发标记阶段扫尾。
- 并发清除阶段,这一阶段会把垃圾对象归还给 freelist,只要注意好 freelist 的并发访问,实现垃圾回收线程和业务线程并发执行是简单的。
- 最终清理阶段,清理垃圾回收所使用的资源。为下一次 GC 执行做准备。
CMS 中最复杂的还是并发标记阶段。如果在并发标记的过程中,业务线程修改了对象之间的引用关系,CMS 采用的办法是:在 write barrier 中,只要一个对象 A 引用了另外一个对象 B,不管 A 和 B 是什么颜色的,都将 A 所对应的 card 置位。
当一轮标记完成以后,如果还有置位的 card,那么垃圾回收器就会开启新一轮并发标记。新的一轮标记,会从上一轮置位的 card 所对应的对象开始进行遍历,遍历完成后再把 card 全部清零,所以这样的一轮并发标记也被称为预清理(preclean)。但是如果每一轮都有 card 置位,应该怎么办呢?CMS 也会在预清理达到一定次数以后停止,进入重标记阶段。
重标记的作用是遍历新生代和 card 置位的对象,对老年代对象做一次重新标记。这一次是需要停顿的,因为这一次垃圾回收器将不允许 card 再被置位了。
可以在重标记之前,进行一次年轻代 GC,这样可以减少年轻代中的对象数量,减少重标记的停顿时间。这个功能可以使用参数 -XX:+CMSScavengeBeforeReMark 来打开。
因为 card table 有两个功能:维护跨代引用和标记灰色结点,所以,Hotspot 又引入了另外一个数据结构 mod union table(MUT)来用于维护跨代引用。在并发标记开始之前,card table 中的内容就会被复制到 MUT 里,如果在 Concurrent Mark 阶段,发生了年轻代的垃圾回收,则可以使用 MUT 来进行跨代扫描。
此文章为7月Day21学习笔记,内容来源于极客时间《编程高手必学的内存知识》