简述Python的垃圾回收机制(garbage collection/gc)

最近在面试中被问到了python的gc,感觉自己不甚了解,在此进行一下学习和总结。

Python的垃圾回收机制

首先贴一段python doc中介绍garbage collection的原文

CPython 目前使用带有 (可选) 延迟检测循环链接垃圾的引用计数方案, 会在对象不可访问时立即回收其中的大部分,但不保证回收包含循环引用的垃圾

我们可以看到,python主要用的是引用计数(reference counting)处理垃圾回收。而当遇到类似循环引用这种无法用引用计数解决的情况时,python采用的是利用标记清除(mark-sweep)的分代回收(generational collection)。

引用计数

我们知道Python是一种面向对象的脚本语言,Python中所有东西,比如数字、字符串等全都是对象,其核心包含一个名为PyObject的结构体。

typedef struct_object{

  int ob_refcnt;

  struct_typeobject *ob_type;

}PyObject;
#define Py_INCREF(op)   ((op)->ob_refcnt++)          //增加计数
#define Py_DECREF(op)      \                         //减少计数        
     if (--(op)->ob_refcnt != 0)    \
         ;        \
     else         \
         __Py_Dealloc((PyObject *)(op))

我们可以看到,其中 ob_refcnt 就是引用计数。当一个对象有新的引用时,它的 ob_refcnt 就会增加,当引用它的对象消失时,它的 ob_refcnt 就会减少。当引用计数为0时,该对象生命就结束了。

增加引用计数的情况
  1. 对象的创建
>>> class foo:
... 	pass
...
>>> sys.getrefcount(foo())
1
  1. 对象的引用
>>> a = foo()
>>> sys.getrefcount(a)
2
  1. 对象作为参数传入函数中
>>> a = [0]
>>> sys.getrefcount(a)
2
>>> def f1(x):
...     x.append(0)
...     return x
... 
>>> f1(a)
[0, 0]
>>> sys.getrefcount(a)
3
  1. 对象作为元素储存在容器中
>>> import sys
>>> a = 789
>>> sys.getrefcount(a)
2
>>> b = [a]
>>> sys.getrefcount(a)
3
>>> c = (a,)
>>> sys.getrefcount(a)
4
减少引用计数的情况

与上面的情况类似,减少引用计数的情况分别为

  1. 当该对象的别名被显式销毁时,如 del a
  2. 当该对象的引别名被赋予新的对象,如 a=10
  3. 当对象离开它的作用域时。例如 func函数执行完毕时,函数里面的局部变量的引用计数器就会减一(但是全局变量不会)
  4. 当该元素从容器中删除时,或者容器被销毁时。
引用计数的优缺点

当指向该对象的内存的引用计数器为0的时候,该内存将会被Python虚拟机销毁。我们可以看到引用计数可以解决大多数垃圾回收的问题。
优点

  1. 实时性: 一旦没有引用,内存就直接释放了。 不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时
  2. 从逻辑上很简单: 我们只需要统计次数就可以了,很好理解

缺点

  1. 消耗资源:Python不得不在每个对象内部留一些空间来处理引用数。这样付出了一小点儿空间上的代价。但更糟糕的是,每个简单的操作(像修改变量或引用)都会变成一个更复杂的操作,因为Python需要增加一个计数,减少另一个,还可能释放对象。

  2. 速度较慢:虽然Python随着程序执行GC很稳健,但这并不一定更快。Python不停地更新着众多引用数值。特别是当你不再使用一个大数据结构的时候,比如一个包含很多元素的列表,Python可能必须一次性释放大量对象。减少引用数就成了一项复杂的递归过程了。

  3. 无法解决循环引用的问题:

    list1 = []
    list2 = []
    list1.append(list2)
    list2.append(list1)
    

    当代码出现如上情况时,我们讲无法利用引用计数清除掉这两个list。

分代回收

Python使用一种链表来持续追踪活跃的对象。Python的内部C代码将其称为零代(Generation Zero)。每次当你创建一个对象或其他什么值的时候,Python会将其加入零代链表。

随后,Python会循环遍历零代列表上的每个对象,检查列表中每个互相引用的对象,根据规则减掉其引用计数。在这个过程中,Python会一个接一个的统计内部引用的数量以防过早地释放对象。

触发条件

随着程序运行,Python解释器保持对新创建的对象和因引用计数为零而被释放掉的对象的追踪。从理论上说,这两个值应该保持一致,因为程序新建的每个对象都应该最终被释放掉。

但在实际情况中,由于循环或程序使用了一些比其他对象存在时间更长的对象,被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦差异累计超过某个阈值,则Python的收集机制就启动了,并且触发上边所说到的零代算法,释放“浮动的垃圾”,并且将剩下的对象移动到一代列表。

随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。而Python对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python会将剩下的活跃对象移动到二代列表。

通过这种方法,你的代码所长期使用的对象,那些你的代码持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python可以在不同的时间间隔处理这些对象。Python处理零代最为频繁,其次是一代然后才是二代

弱代假说

弱代假说由两个观点构成:年轻的对象通常死得也快,而老对象则很可能存活更长时间。

代垃圾回收算法的核心行为:垃圾回收器会更频繁的处理新对象。一个新的对象即是你的程序刚刚创建的,而一个来的对象则是经过了几个时间周期之后仍然存在的对象。

Python会在当一个对象从零代移动到一代,或是从一代移动到二代的过程中提升(promote)这个对象。这么做的缘故是弱代假说。

老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。

参考资料:
[1]: https://blog.csdn.net/anmi3721/article/details/101666340?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase
[2]: https://www.cnblogs.com/xiugeng/p/10514101.html#_label0_1
[3]: https://blog.csdn.net/chaxiaoli001/article/details/104048926?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-2
[4]: https://blog.csdn.net/anmi3721/article/details/101666340?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值