标记-清除算法

分为标记和清除两个阶段:首先标记处所有需要回收的对象,标记完成后统一回收所有被标记的对象;是最基础的收集算法,其它的收集算法都是基于这种思路并对其不足进行改进而得到的。

不足:
a) 效率问题,标记和清除两个过程的效率都不高;
b) 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存二不得不提前触发另一次垃圾收集动作。

复制算法

为了解决效率问题,复制收集算法出现了,他将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

不足:这种算法的代价是将内存缩小为原来的一般,代价太高
用法:存活区采用这种算法,因为新生代中的对象98%是“朝生夕死”,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。不能保证每次回收都只有不多于10%的对象存活,当Survivor空间不够时,需要依赖老年代进行分配担保(Handle Promotion)

标记-整理算法

老年代对象存活率较高,一般不能选择复制算法,根据老年代特点,提出标记-整理算法,标记过程和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法,根据对象存活周期的不同将内存分为几块。一般是把java对分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中每次垃圾收集时都有大量对象死去,只有少量对象存活,所以就采用复制算法,只需要付出少了存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对他进行分配担保,就要使用“标记-清除”或“标记-整理”算法

三色标记算法

将对象分成三种类型:
1、黑色:根对象,或者该对象与它的子对象都被扫描过(对象被标记了,且它的所有 field 也被标记完了)。
2、灰色:对象本身被扫描,但还没扫描完该对象中的子对象(它的field还没有被标记或标记完)。
3、白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,既垃圾对象(对象没有被标记到)。

三色标记算法的整个过程:
1、初始时,所有对象都在 【白色集合】中;
2、将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
3、从灰色集合中获取对象:
3.1 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
3.2 将本对象 挪到 【黑色集合】里面。
4、重复步骤3,直至【灰色集合】为空时结束。
5、结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收。

而当需要支持并发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。

多标-浮动垃圾:
当在标记过程中发生已标记过得变成白色,那应该被回收,这部分对象仍会被标记为存活,即本轮GC不会回收这部分内存,被称之为“浮动垃圾”。另外,针对并发标记开始后的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。

漏标-读写屏障:
漏标只有同时满足以下两个条件时才会发生:
条件一:灰色对象 断开了 白色对象的引用;即灰色对象 原来成员变量的引用 发生了变化。
条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。

将新加的对象G记录起来,然后作为灰色对象再进行遍历即可。比如放到一个特定的集合,等初始的GC Roots遍历完(并发标记),该集合的对象 遍历即可(重新标记)。
重新标记是需要STW(stop the work)的,因为应用程序一直在跑的话,该集合可能会一直增加新的对象,导致永远都跑不完。
扫描所有GC Roots 这个操作(即初始标记)通常是需要STW的,否则有可能永远都扫不完,因为并发期间可能增加新的GC Roots。
(1) 写屏障 + SATB (Snapshot At The Beginning,原始快照)
(2) 写屏障 + 增量更新
(3) 读屏障(Load Barrier)

三色标记法与现代垃圾回收器

现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同:比如白色/黑色集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可以是广度/深度遍历等等。
对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下:
CMS:写屏障 + 增量更新
G1:写屏障 + SATB
ZGC:读屏障
CMS中使用的增量更新,在重新标记阶段,除了需要遍历 写屏障的记录,还需要重新扫描遍历GC Roots(当然标记过的无需再遍历了),这是由于CMS对于astore_x等指令不添加写屏障的原因

参考:https://www.processon.com/view/5f7fdf845653bb06eff0a16d