文章目录
MarkSweep 的逻辑是判断对象是否存在引用,而 引用计数法 则在这个逻辑上更进了一步,引入 计数器的概念来统计对象当前的被引用次数。如果次数为 0 了,那么也就是非活动对象,可以被回收,否则就是活动对象。
基础实现
引用计数法实现的核心是针对计数器本身的增减,并且当减至为 0 的时候,执行回收操作,所以在该算法中并没有如 MarkSweep 中那么明确的 GC 执行开始时间。
代码
计数器修改的时机有两个,分别是对象创建,以及指针更新的时候
// 对象定义
type object struct {
ReferenceCount int
Filed interface{
}
Next *object
Size int
}
// 新建对象
func NewObject() *object {
return &object{
ReferenceCount: 1,
}
}
// 指针更新(引用更新)注意增减顺序
func UpdatePointer(ptr **object, o *object) {
increaseObjectRef(o)
decreaseObjectRef(ptr)
*ptr = o
}
func increaseObjectRef(o *object) {
o.ReferenceCount++
}
// 减少引用过程伴随对象回收的操作
func decreaseObjectRef(o *object) {
o.ReferenceCount--
if o.ReferenceCount == 0 {
decreaseObjectRef(o.Next)
reclaim(o)
}
}
注意: UpdatePointer 增减计数器的顺序,是先增加被引用对象的计数以后,再减少要变更引用的对象的计数的, 考虑特殊情况,假如两者是相同对象,则会导致出错。因为
decreaseObjectRef
中有对象回收的操作
注意:golang 函数是值传递,所以这里的 UpdatePointer 传参相对原书是有所修改。
优点
- 可以立即回收垃圾 (
decreaseObjectRef
中实现的) - 最大暂停时间短(可以认为几乎没有,因为该算法的 GC 相关都在对象的操作中完成了)
- 没有必要沿指针查找 (没必要由根沿指针查找。当我们想减少沿指 针查找的次数时,它就派上用场了)
缺点
- 计数器的操作繁重(尤其是从根出发的引用)
- 计数器占位多
- 实现繁琐复杂
- 循环引用无法回收
下面同样是重点针对缺点做分析
计数器操作繁重
通过代码可以看到,每次指针变化要对应两次计数器的操作,那么当指针变化频繁时,计数器部分的消耗就无法忽视。尤其是从根(调用栈、寄存器)引用的指针变化会非常频繁。
改进措施有
- 延迟引用计数法
计数器占位多
这个是存储的问题,严格讲,计数器最大可以到 2 的(机器位数)次方(理论上一个对象可以被引用这么多次数),那么内存浪费就太大了。
改进措施有
- Sticky 引用计数法
- 1 位引用计数法
实现繁琐复杂
因为那个 UpdatePointer
是要在应用程序内调用的,那么就不能按照语言自身的变更去写 ptr=&obj
, 这样是很麻烦的。
循环引用无法回收
假设 A 引用 B 的同时,B 也引用了 A,那么 A 和 B 的引用计数都会是 1,那么在该算法下就无法被回收了
改进措施有
- 部分 MarkSweep 算法(4 色算法)
算法改进
- 延迟引用计数法 (改善计数繁重)
- Sticky 引用计数法(改善计数占位)
- 1 位引用计数法(改善计数占位)
- 部分 MarkSweep 算法(解决不能回收循环垃圾的问题)
延迟引用计数法
原生算法的计数繁重,尤其是是来自根的引用变化引发的部分,而延迟引用计数法的主要目的就是解决这部分,但是也同时引入了新的计数问题导致与原生算法不能兼容,最后又通过引入新的数据结构 ZCT
以及配套的措施来解决这个问题。所以,该算法可以被分解成两个部分
- 根引用变化不使用引用计数法,而是直接修改
- 引入的 ZCT ,以及针对 ZCT 部分的逻辑
ZCT
ZCT
就是 zero count table
,该表的功能是用来记录下在 decreaseObjectRef
(也就是减少引用)时候,计数器变为 0 的对象。当没有空余空间时,则通过扫描 ZCT
判断是否被根引用,如果被引用则跳过,没有引用则进行回收,从而释放空间。
zct
的操作方法有 push
和