Python垃圾回收机制

Stella981
• 阅读 315

对于Python垃圾回收机制主要有三个,首先是使用引用计数来跟踪和回收垃圾,为了解决循环
引用问题,就采用标记-清除的方法,标记-清除的方法所带来的额外操作实际上与系统中总的内存
块的总数是相关的,当需要回收的内存块越多,垃圾检查带来的额外操作就越多,为了提高垃圾收集
的效率,采用“空间换时间的策略”,即使用分代机制,对于长时间没有被回收的内存就减少对它的
垃圾回收频率。

首先看一下Python的内存管理架构:

layer 3: Object-specific memory(int/dict/list/string....)
Python 实现并维护
更高抽象层次的内存管理策略, 主要是各类特定对象的缓冲池机制

layer 2: Python's object allocator
Python 实现并维护
实现了创建/销毁Python对象的接口(PyObject_New/Del), 涉及对象参数/引用计数等

layer 1: Python's raw memory allocator (PyMem_ API)
Python 实现并维护, 包装了第0层的内存管理接口, 提供统一的raw memory管理接口
封装的原因: 不同操作系统行为不一定一致, 保证可移植性, 相同语义相同行为

layer 0: Underlying general-purpose allocator (ex: C library malloc)
操作系统提供的内存管理接口, 由操作系统实现并管理, Python不能干涉这一层的行为

引用计数机制

引用计数是一种垃圾收集机制,而且也是一种最直观,最简单的垃圾回收技术 当一个对象的引用被创建或者复制时,对象的引用计数加1;
当一个对象的引用被销毁 对象的引用计数减1。如果对象的引用计数减少为0,那么就意味着对象已经不会被任何人使用,可以将其所占有的内存释放。
引用计数机制的优点:实时性,对于任何内存一旦没有指向它的引用,就会立即被回收(这里需要满足阈值才可以)
引用计数机制的缺点:引用计数机制所带来的维护引用计数的额外操作与Python运行中所运行的内存分配和释放,引用赋值的
次数是成正比的,为了与引用计数机制搭配,在内存的分配和释放上获得最高的效率,Python设计了大量的
内存池机制,减少运行期间malloc和free的操作。

>>> from sys import getrefcount
>>> a = [1,2,3]
>>> getrefcount(a)
2
>>> b =a
>>> getrefcount(a)
3
>>>

标记-清除机制

引用计数机制有个致命的弱点,就是可能存在循环引用的问题:
一组对象的引用计数都不为0,然而这些对象实际上并没有被任何外部变量引用,它们之间只是相互引用,这意味这个不会
有人使用这组对象,应该回收这些对象所占的内存,然后由于互相引用的存在, 每个对象的引用计数都不为0,因此这些对象
所占用的内存永远不会被回收。
标记-清除机制就是为了解决循环引用的问题。首先只有container对象之间才会产生循环引用,所谓container对象即是内部
可持有对其他对象的引用的对象,比如list、dict、class等,而像PyIntObject、PyStringObject这些是绝不可能产生循环引用的
所以Python的垃圾回收机制运行时,只需要检查这些container对象,为了跟踪每个container,需要将这些对象组织到一个集合中。
Python采用了一个双向链表,所以的container对象在创建之后,就会被插入到这个链表中。这个链表也叫作可收集对象链表。

为了解决循环引用的问题,提出了有效引用计数的概念,即循环引用的两个对象引用计数不为0,实际上有效的引用计数为0
假设两个对象为A、B,我们从A出发,因为它有一个对B的引用,则将B的引用计数减1;然后顺着引用达到B,因为B有一个对A的引用,
同样将A的引用减1,这样,就完成了循环引用对象间环摘除。但是这样直接修改真实的引用计数,可能存在悬空引用的问题。
所以采用修改计数计数副本的方法。
这个计数副本的唯一作用是寻找root object集合(该集合中的对象是不能被回收的)。当成功寻找到root object集合之后,
我们就可以从root object出发,沿着引用链,一个接一个的标记不能回收的内存。首先将现在的内存链表一分为二,
一条链表中维护root object集合,成为root链表,而另外一条链表中维护剩下的对象,成为unreachable链表。之所以要剖成两个链表,
是基于这样的一种考虑:现在的unreachable可能存在被root链表中的对象,直接或间接引用的对象,这些对象是不能被回收的,
一旦在标记的过程中,发现这样的对象,就将其从unreachable链表中移到root链表中;当完成标记后,unreachable链表中剩下
的所有对象就是名副其实的垃圾对象了,接下来的垃圾回收只需限制在unreachable链表中即可。

分代回收

分代回收的思想:将系统中的所有内存块根据其存活时间划分为不同的集合,每一个集合就称为一个“代”
垃圾收集的频率随着“代”的存活时间的增大而减小,也就是说,活的越长的对象,就越可能不是垃圾,就应该
越少去收集。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代中。
在Python中总共有三个“代”,每个代其实就是上文中所提到的一条可收集对象链表。下面的数组就是用于分代
垃圾收集的三个“代”。

#define NUM_GENERATIONS 3
#define GEN_HEAD(n) (&generations[n].head)

// 三代都放到这个数组中
/* linked lists of container objects */
static struct gc_generation generations[NUM_GENERATIONS] = {
/* PyGC_Head, threshold, count */
{{{GEN_HEAD(0), GEN_HEAD(0), 0}}, 700, 0}, //700个container, 超过立即触发垃圾回收机制
{{{GEN_HEAD(1), GEN_HEAD(1), 0}}, 10, 0}, // 10个
{{{GEN_HEAD(2), GEN_HEAD(2), 0}}, 10, 0}, // 10个
};

PyGC_Head *_PyGC_generation0 = GEN_HEAD(0);

其中存在三个阈值,分别是700,10,10
可以通过get_threshold()方法获得阈值:

import gc
print(gc.get_threshold())
(700, 10, 10)

其中第一个阈值表示第0代链表最多可以容纳700个container对象,超过了这个极限值,就会立即出发垃圾回收机制。

后面两个阈值10是分代有关系,就是每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,
才会有1次的2代垃圾回收。也就是空间换时间的体现。垃圾回收的流程:
--> 分配内存的时候发现超过阈值(第0代的container个数),触发垃圾回收
--> 将所有可收集对象链表放在一起(将比当前处理的“代”更年轻的"代"的链表合并到当前”代“中)
--> 计算有效引用计数
--> 根据有效引用计数分为计数等于0和大于0两个集合
--> 引用计数大于0的对象,放入下一代
--> 引用计数等于0的对象,执行回收
--> 回收遍历容器内的各个元素, 减掉对应元素引用计数(破掉循环引用)
--> python底层内存管理机制回收内存

参考文档:
http://www.cnblogs.com/vamei/p/3232088.html
http://python.jobbole.com/83548/
http://python.jobbole.com/82061/
python源码剖析

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
2年前
Java系列笔记
Java垃圾回收概况  JavaGC(GarbageCollection,垃圾收集,垃圾回收)机制,是Java与C/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对JVM(J
Wesley13 Wesley13
2年前
CMS垃圾回收过程
1.总体介绍:CMS(ConcurrentMarkSweep)是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动JVM参数加上\XX:UseConcMarkSweepGC ,这个参数表示对于老年代的回收采用CMS。CMS采用的基础算法是:标记—清除。2.CMS
Wesley13 Wesley13
2年前
Java 内存区域和GC机制
Java垃圾回收概况  JavaGC(GarbageCollection,垃圾收集,垃圾回收)机制,是Java与C/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对JVM
Stella981 Stella981
2年前
Python C 扩展的引用计数问题探讨
PythonGC机制对于Python这种高级语言来说,开发者不需要自己管理和维护内存。Python采用了引用计数机制为主,标记清除和分代收集两种机制为辅的垃圾回收机制。首先,需要搞清楚变量和对象的关系:变量:通过变量指针引用对象。变量指针指向具体对象的内存空间,取对象的值。对象,类型已知,每个对象都包
Wesley13 Wesley13
2年前
PHP垃圾回收机制
php5.3之前使用的垃圾回收机制是单纯的“引用计数”,也就是每个内存对象都分配一个计数器,当内存对象被变量引用时,计数器1;当变量引用撤掉后,计数器1;当计数器0时,表明内存对象没有被使用,该内存对象则进行销毁,垃圾回收完成。“引用计数”存在问题,就是当两个或多个对象互相引用形成环状后,内存对象的计数器则不会消减为0;这时候,这一组内存对象已经
Stella981 Stella981
2年前
JVM调优总结(三)
可以从不同的的角度去划分垃圾回收算法:按照基本回收策略分引用计数(ReferenceCounting):比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。标记清除(MarkSweep):
Stella981 Stella981
2年前
Python的垃圾回收机制
垃圾回收机制「垃圾回收(GC)」大家应该多多少少都了解过,什么是垃圾回收呢?垃圾回收GC的全拼是GarbageCollection,在维基百科的定义是:在计算机科学中,垃圾回收(英语:GarbageCollection,缩写为GC)是一种自动的内存管理机制。当一个电脑上的动态内存不再需要时,就应该予以释放,以让出内存,这种内存资源
Stella981 Stella981
2年前
JavaScript:垃圾收集机制
  JavaScript具有自动垃圾收集机制。也就是说,执行环境会负责管理代码执行过程中使用的内存。开发人员不必关心内存分配和回收问题。  垃圾收集机制的原理:找到不再继续使用的变量,然后进行释放其占用的内存。所以,垃圾收集器会按照固定的时间间隔(或代码执行中设定的收集时间)持续执行这一操作。  垃圾收集器会跟踪哪些变量有用哪些变量没用,对没用的变量
Stella981 Stella981
2年前
Android开发的内存问题
不少人认为Java(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fwww.51code.com%2F)程序因为有垃圾回收机制,就不会有内存泄漏。其实如果我们一个程序中已经不再使用某个对象,但是依然有引用指向它,垃圾回收器就没有办法回收它,所以该对象占用的内存就无法被使用,造成内存泄露
Stella981 Stella981
2年前
JVM的GC算法总结
Java程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经失去标记,程序用不了它们了,对程序而言它们已经废弃),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC),这就是我们的垃圾回收机制,关于垃圾回收我总结了一下几种:标记–清除算法(MarkSweep)