垃圾回收

1. 什么是垃圾回收

  垃圾回收是一种自动内存管理方法,垃圾收集器定期的扫描内存中不再被使用的对象,并将该对象所占用的内存资源释放出来以供其他程序使用。

  注意:垃圾回收回收的主要是内存资源,其他的资源如网络连接,数据库连接,文件与设备等是无法通过垃圾回收自动释放。这类资源的释放通常是由finalizer来管理,比如Java中的finalize方法,Python中的__del__方法,C#中的~Foo析构方法,以及C++中的析构函数。

  通常,当一个对象不可达时,垃圾收集器便会将该对象标记为垃圾。在内存被释放之前,finalizer方法会被调用。然而finalizer方法的调用是不确定的,它由垃圾收集器决定,很可能永不会调用,这是跟析构函数最大的不同,因为析构函数在对象被释放的时候是肯定会被调用的。

  同时,垃圾收集器在调用finalizer方法的时候,还需要检测是否有对象被重新激活。若有对象被重新激活,则取消其析构函数的调用。因此带有finalizer的对象的垃圾收集开销会比较大,相比没有finalizer方法的对象其被垃圾收集的频率会较低。

  若对象由于finalizer方法的调用被重新激活,那么一个新的问题是当该对象被重新垃圾回收的时候,是否还要重新调用finalizer方法——这是跟析构函数的另一个不同:finalizer方法可能会被调用多次。于是产生了新的问题:对象成为垃圾 finalizer方法被调用 对象被重新激活 对象重新成为垃圾 finalizer 方法被再次调用 对象被再次激活。循环往复以致无穷。这类问题出现在CPython版本的Python 3.4之前,以及C#中。为了避免此类问题,很多编程语言,比如JavaObject-CPython 3.4之后的版本中,finalizer最多只允许调用一次,因此需要跟踪finalizer方法是否被调用。在C#中,对finalizer的跟踪是跟对象分离的,因此一个对象可以被多次的注册或取消注册finalization方法的调用。

2. 几种常见的垃圾回收方法

一个对象x是可达的,当且仅当:
1. 寄存器中存储了指向该x的指针,或者
2. 一个可达的对象y有指向x的指针。

因此,从寄存器出发,沿着指针找到可达的对象,所有不可达的对象便可标记为垃圾用于回收。基于以上原理,有以下方法来实现自动垃圾回收算法。

1. Mark and Sweep

每个对象中有一个标记位

bool marked = false;

在mark阶段,扫描对象图并对可达的对象进行标记marked = true

let todo = {all roots}
while todo != null:
    pick v in todo
    todo = todo - v
    if not v.marked :
        v.marked = true
        let v1...vn be the pointers contained in v
        todo = todo | {v1,...vn}

在Sweep阶段对所有marked = false的对象进行垃圾回收。

// sizeof(p) is the size of block starting at p
p = bottom of heap
while p < top of heap:
    if p.marked:
        p.marked = false
    else
        add block..(p + sizeof(p) - 1 ) to freelist
    p = p + sizeof(p) 

优点:简单,不惧循环引用,空间花费小,GC期间不会移动对象。
缺点:
1. 在垃圾收集期间,程序会处于暂停运行无响应状态。
2. heap内存要被扫描两次,时间复杂度高O(内存大小)
3. 会产生内存碎片,因此有改进算法在Sweep之后尽可能合并free list。

2. Stop and Copy

将堆内存划分成两部分,一部分用于内存分配(old space),一部分用于GC(new space)。
当old space空间用完的时候
1. 将所有活动的对象copy到new space,这样所有不活动的对象就全都留在了old space
2. 由于只拷贝活动对象,因此copy后所占用的new space内存会比原来占用的old space内存少。
3. copy完之后,交换old space和new space角色,恢复程序运行。

优点:不会产生内存碎片,被认为是速度最快的垃圾收集方法,时间复杂度为O(活动对象)。
缺点:
1. 至少有一半的内存无法被充分利用。
2. 需要移动对象,若程序占用的内存空间非常大,则移动对象也会话费很长时间。

由于需要移动对象,对象被移动之后地址会发生变化,因此需要修改指向对象的指针,于是产生一个新的问题:我们希望能以O(1)的空间复杂度重置对象指针。

解决方法:将new space划分成三段

第一段表示已经copy且指针被重置过的,第二段表示已经copy但是指针还没有被重置,第三段表示还没被占用的空白内存区域。
整个过程分为以下几步:
假设一开始是这样的

1. 根据root指针指向的堆空间,拷贝一个object,设置forwording指针。注意forwarding指针是从old space上的移动对象指向new space上的拷贝对象(下图虚线部分)。

2. A有指向C的指针,因此拷贝对象C。此时scan指针和alloc指针不相等,因此需要移动scan指针,重置对象A的引用对象的地址指向新的C

3. 重复步骤2,拷贝对象F,重置对象C的引用对象的地址指向新的F

4. 重复步骤2,没有对象拷贝,只需要重置对象F的引用对象的地址,发现它原来指向的A的forwarding指针指向了新的A,因此直接修改F的指针指向新的A。

5. 此时scan指针和alloc指针相等,可以交换old space和new space的角色

3. Mark and Compact

是对Mark and Sweep算法的改进,算是结合了Mark and Sweep与Copy算法,一方面可以解决内存碎片问题,另一方面不需要将heap空间划分成两份,应用程序可以直接利用全部的内存空间。主要步骤包括
1. Mark阶段,标记活动对象
2. Sweep阶段,回收不可用对象
3. Compact阶段,将对象Copy对齐
在Copy阶段同样需要处理指针地址问题,算法跟上面的一样。

4. Reference Count

对象中保存引用计数,每次有赋值操作时都会导致其指向的对象的引用计数的变化。

int refCount = 0;

这样,不是在内存不足时才进行垃圾回收,而是只要有一个对象没有了引用,即引用计数为0时,就可以将其当作垃圾来回收,且垃圾回收时程序暂停时间很短。

注意:当一个对象没有了引用,被回收之后,该对象所指向的引用也将消失。
比如A -> B -> C,当A -> B指针被改变之后,B的引用计数为0而被回收,则C的引用计数也为0,也需要被回收。

缺点:
1. 无法处理环形引用。
2. 每次赋值操作都需要更新refCount,开销比较大。编译器可以通过优化将多次refCount的修改改成一次修改,但实现起来比较难。

解决环形引用问题,有以下方法:

Weak Reference

强引用就是我们平常编写程序时的引用方式。这种引用方式在基于引用计数的垃圾回收机制中,无法处理循环引用的情况。为了解决该问题,引入weak reference。被weak reference引用的对象,在GC眼中相当于未被引用,在内存空间紧张的时候可以被安全的释放。比如在一个Cache系统中,随着系统运行时间的增加,Cache的体积越来越大,如果是strong reference,那么GC无法通过自动释放部分Cache以缓解内存压力。如果是weak reference的话,GC就可以自动的释放这部分内存空间。

由于weak reference引用的内存的释放是不确定的,因此调用的时候需要判断引用的对象是否已经释放,比如在Java中

import java.lang.ref.WeakReference;

WeakReference<Foo> foo = new WeakReference<Foo>();
if(foo.get() != null){
    // foo is still alive
}else{
    // otherwise, it's been collected as garbage.
}

3. 现实中的垃圾回收

C++

C++中的智能指针shared_ptr使用引用计数法来确定是否释放一个引用的对象。使用weak_ptr可以打破循环引用。

PHP

PHP使用引用计数来进行垃圾回收,允许用户开启或关闭GC。

Python

Python主要是通过引用计数来进行垃圾回收的。在处理环形引用问题时,使用GC模块。

import sys, gc
def make_cycle():
    l = { }
    l[0] = l

def main():
    collected = gc.collect()
    print "Garbage collector: collected %d objects." % (collected)
    print "Creating cycles..."
    for i in range(10):
        make_cycle()
    collected = gc.collect()
    print "Garbage collector: collected %d objects." % (collected)

if __name__ == "__main__":
    ret = main()
    sys.exit(ret)
输出
Garbage collector: collected 0 objects.
Creating cycles...
Garbage collector: collected 10 objects.

gc模块还可以禁用垃圾回收gc.disable(),它是通过分代垃圾回收的方法来处理环形引用的问题,每一代都是一个链表。假设当前在yongest链表中,做如下操作

update_refs(young) // 将每个对象的refCount拷贝一份,以便进行减法操作
substrat_refs(young) // 对于yong中的每个对象i,找到i所引用的所有对象j,对j的refCount进行减一操作。
gc_init_list(unreachable) // 所有refCount为0的是垃圾,所有refCount不为0的不是垃圾,他们引用的对象如果之前标记为垃圾,此时再标记回来。
move_unreachable(young, unreachable) // unreachable里的对象都可以进行垃圾回收,将剩下的对象move到older generation

为何引用计数减为0的就是垃圾呢?首先环形引用的必然是减为0的,其次一个正常引用的对象,由于存在old generation指向young generation的引用,因此在本generation中检测引用计数的时候,引用计数值非0,其所有引用的对象也会从垃圾标记回非垃圾。因而可以搞定循环引用的问题。

CLR

CLR使用Mark and Compact进行垃圾回收。同时也有类似Python中的分代垃圾回收。抄袭JVM。

JVM

JVM使用Mark and Compact进行垃圾回收,同时也有类似Python中的分代垃圾回收。抄袭CLR。

4. QA

  1. GC root是指什么?
    GC root是指能在heap外部被访问的对象,比如
    • 静态类的对象
    • stack里的引用的局部对象
    • 一些handler,比如socket,file等

to be continued

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值