垃圾回收GC经典算法

垃圾回收GC(Garbage Collection)

1、什么是垃圾

c语言中malloc申请完内存后,需要手动回收free。
当忘记回收时就产生了垃圾

2、为什么要有GC

  1. 经常忘记回收
  2. 容易出现多次回收的情况

经典的GC算法

1、基本的一些概念

在这里插入图片描述

我们将内存视为一张有向图
每个数据块(block)在有向图中都是一个节点
每个指针在有向图中是一条边

根节点:包含指针指向堆但是不位于堆内的数据块称作根节点(例如:寄存器,栈上的位置,全局变量等)
可达节点:从根节点到该节点是有一条可达路径的
不可达节点:也就是垃圾,需要回收

2、标记清除算法(Mark and Sweep)

可以建立在malloc/free包之上

问题一:什么时候开始执行该算法?

空间不足时开始(用malloc分配内存直到空间不足out of space)

问题二:空间不足后该算法执行了什么?

标记(Mark):从根节点开始为每一个可达的节点设一个标记位
清楚(Sweep):扫描所有的节点并且释放那些没有被标记的节点

问题三:如何设标记位?

在每一个数据块(block)的头部使用一个额外的位进行标记

具体诠释:
在这里插入图片描述

伪代码:

void GC(){
    HaltAllProcessing();
    ObjectCollection Roots=GetRoots();//找到所有的根节点
    for(int i=0;i<root.Count();i++){
        Mark(root[i]);//标记
    }
    Sweep();//清除
}

包含三个阶段:

  1. 收集所有的根节点
  2. 从根开始标记所有可达节点
  3. 最后清除

第一个阶段:收集所有的根节点

运行时系统需要为GC提供一些方法来收集根节点的列表
例如:.NET维护这些根节点并且向GC提供API来收集这些根节点

第二个阶段:从根开始标记所有可达节点
伪代码:

ptr mark(ptr p){
    if(!is_ptr(p)) return; //如果p不是指针的话,什么都不做
    if(markBitSet(p)) return; //如果p已经被标记了的话,直接返回
    setMarkBit(p); //标记p
    for(i=0;i<length(p);i++){  //遍历判断p数据块是否指向其他节点
        mark(p[i]); 
    }
    return;
}

关于如何判断p是否是一个指针,还没有搞懂,搞懂再写zzz

第二个阶段:清除
伪代码:

ptr sweep(ptr p,ptr end){ 
    while (p<end){
        if (markBitSet(p)){
            clearMarkBit();//如果被标记了,就清除标记位,相当于置空
        }else if (allocateBitSet(p)){
            free(p);//如果没有被标记,且分配了空间就释放这一块空间
        }
        p+=length(p);//通过加上p的长度来到达下一个block,达到遍历整个heap的效果
    }
}

优缺点
优点:

不用自己写代码去释放内存,当heap满了的时候,它会自动执行
这种方式可以找到所有应该被释放的内存

缺点:

这种GC算法执行的时候会中断其他的进程,性能上可能会出现一个突然的下降
如上图所示:容易造成内存碎片

3、复制法(copy)

基本思路:使用2个堆

一个堆用在程序运行时
一个堆只在GC时使用

该GC算法执行步骤

  1. 从根节点开始遍历可达数据
  2. 从from-space(程序运行时的堆)复制可达数据到to-space(GC时使用的堆)
    注意:不可达节点留在了from-space
  3. 交换两个堆(即原来的from-space变为了to-space)

具体诠释:
在这里插入图片描述

优缺点
优点:

相较于标记和扫描算法,该算法运行速度更快。因为它只扫描了一次堆。

缺点:

缺点也很明显,只有效使用了堆空间的一半(典型地用空间换取时间)

4、引用计数法(Reference Counting)

基本思路:

跟踪指向每个对象的指针的数量
当引用计数的数量为0时,说明该对象是无法访问的垃圾

具体诠释:
在这里插入图片描述

优缺点:
优点:

该算法具有动态的优点,每当有分配或其他堆操作的时候,就执行该算法。

缺点:

无法检测到不可访问的循环列表
计数的成本高:过多的引用计数的增和减

目前的应用情况

在java中没有引用计数
Python还使用引用计数,并提供周期性检测

5、分代式垃圾回收法(Generational GC)

根据经验观察发现:

如果一个对象被访问了很长一段时间,它很可能会维持这样
在大多数的语言中:大多数的对象died young

结论

我们可以通过经常扫描新生代对象,很少扫描老一代对象来节省工作

达尔文的进化论:新生物种总是最容易被淘汰的。

具体诠释:
在这里插入图片描述

基本思路

将对象分配给不同的代G0,G1…
G0包含了新一代的对象,最有可能成为垃圾,
G0扫描的频率高于G1
效率得到大幅度提升

总结

  • 引用计数是解决显式内存分配问题的常用解决方案。实现赋值时递增和递减操作的代码通常是程序缓慢的原因之一。无论如何,引用计数也不是全面的解决方案,因为循环引用从不会被删除。
  • 垃圾回收只会在内存变得紧张时才会运行。当内存尚且宽裕时,程序将全速运行,不会在释放内存上花费任何时间。
  • 相对于过去的缓慢的垃圾回收程序,现代的垃圾回收程序要先进得多。分代、复制回收程序在很大程度上克服了早期的标记&清除算法的低效。
  • 现代垃圾回收程序进行堆紧缩。堆紧缩将减少程序引用的页的数量,这意味着内存访问命中率将更高,交换将更少。
  • 采用垃圾回收的程序不会因为内存泄漏的累积而崩溃。采用GC的程序拥有更长期的稳定性。采用垃圾回收的程序有更少的难以发现的指针错漏。这是因为没有指向已经释放的内存的悬挂指针。因为没有显式的内存管理代码,也就不可能有相应的错漏。
  • 采用垃圾回收的程序的开发和调试更快,因为不用开发、调试、测试或维护显式的释放代码。

垃圾回收并非什么仙丹妙药。它有着以下不足:

  • 内存回收何时运行是不可预测的,所以程序可能意外暂停。
    • 运行内存回收的时间是没有上界的。尽管在实践中它的运行通常很快,但无法保证这一点。
    • 除了回收程序以外的所有线程在回收进行时都会停止运行。
  • 垃圾回收程序也许会留下一些本该回收的内存。在实践中,这不是什么大问题,因为显式内存回收程序通常会泄露一些内存,这致使它们最终耗尽所有内存,另一个理由就是显式内存回收程序通常会把内存放回自己的内部内存池中而不是把内存交还给操作系统。
  • 垃圾回收应该被实现为一个基本的操作系统内核服务。但是因为现实并非如此,就造成了采用垃圾回收的程序被迫带着它们的垃圾回收实现到处跑。尽管这个实现可以被做成一个共享DLL,它也还是程序的一部分。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋千水竹马道

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值