常用GC
- 经典的GC算法有三种:引用计数(reference counting)、标记-清扫(mark & sweep)、复制收集(Copy and Collection)
引用计数法
- 记录对象引用个数
- 当引用为0时候删除对象
优点
- 无需长时间暂停程序,STW非常短,立刻回收垃圾
- 逻辑简单
缺点
- 无法解决循环引用的问题(最大问题)
- 占用内存较大(保存引用个数)
- 更新频繁,浪费时间片
- 内存碎片化(释放的内存分布地方不同)
改进方法
- 延迟引用技术
引用为0不清除,加入空闲的table,分配内存时候可以用
- 减少引用变量位宽
- 溢出直接不管,不当作垃圾
- 运行标记清除
总结
- 简单,但是有时候不得不运行标记清除解决问题
标记清除
- 第一阶段:标记。从根结点出发遍历对象,对访问过的对象打上标记,表示该对象可达。
- 第二阶段:清除。对那些没有标记的对象进行回收,这样使得不能利用的空间能够重新被利用。
优点
- 运行时候才调用,平时不占用内存和cpu
- 没有引用计数问题
缺点
- 需要暂停程序进行GC,会使得程序停止
- 内存碎片化(释放的内存分布地方不同)
复制收集
- 将内存一分为二,GC时候相当于搬家到另外一边
- 访问对象,把可达的所有对象和他们子对象通通拷贝到另外一半
优点
- 没有碎片化问题(内存都是一大块释放)
- 内存分配更加高效(因为上一个的原因)
缺点
- 内存利用率低(不到50%)
分代垃圾回收
- 不是单独一种GC方法,而是作为一种缝合不同GC的方法
- 将对象分为新生代(更加容易被回收)和老生代(难以回收)
- 新生代通常使用复制收集算法,老的通常为标记清除
三色标记法
代码参考
- C++:https://gitee.com/chenxuan520/tricolor-notation/blob/master/gc.h
说明
- 本质也是标记清除算法,但是解决它的程序停止的缺点(核心还是读写冲突的问题,如果不暂停程序就会出现读写冲突,三色标记法使用的是类似锁的方法解决)
- 垃圾收集的根对象一般包括全局变量和栈对象,因为栈对象永远是黑色(会自动释放),只有堆对象需要回收
- Go 语言的垃圾收集可以分成清除终止、标记、标记终止和清除四个不同阶段
过程
- 先从根开始标记灰色,三个桶
- 把灰色引用到的全部变灰色,灰色变黑
- 删除白色对象
- 继续上面过程
完整流程
- GCMark 标记准备阶段,为并发标记做准备工作,启动写屏障
- STWGCMark 扫描标记阶段,与赋值器并发执行,写屏障开启并发
- GCMarkTermination 标记终止阶段,保证一个周期内标记任务完成,停止写屏障
- GCoff 内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭
- GCoff 内存归还阶段,将过多的内存归还给操作系统,写屏障关闭。
写屏障
- 强三色不变性 — 黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象;
- 弱三色不变性 — 黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径
- 所有触发都是针对黑色和灰色对象,白色对象引用的改变不会触发
- 缺点: 需要给栈对象添加屏障,损耗指针性能
插入写屏障
writePointer(slot, ptr):
shade(ptr) //把要插入引用的对象先涂灰
*slot = ptr //改变指针到引用
- 把增加引用的对象变成灰色,添加引用时候触发,保证强三色不变性
删除写屏障
writePointer(slot, ptr)
shade(*slot) //把要删除的引用对象涂灰
*slot = ptr //改变指针到引用
- 删除引用之前,先把被删除引用对象涂灰
混合写屏障
writePointer(slot, ptr):
shade(*slot)
if current stack is grey://当前栈没有扫描,是白色
shade(ptr)
*slot = ptr
- 将创建的所有新对象都标记成黑色,防止新分配的栈内存和堆内存中的对象被错误地回收
各种语言使用GC
- GO: 三色标记法
- Python:引用计数器、标记-清除机制、分代垃圾收集
- Java: 标记-清除机制、分代垃圾收集
参考
- https://www.cnblogs.com/Delo/articles/12553593.html
- https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/
- https://www.cnblogs.com/wuyanzu123/p/14813886.html