Python内存管理机制 之 垃圾回收

参考链接:

https://www.cnblogs.com/alexzhang92/p/9416692.html

https://www.cnblogs.com/neillee/p/6259590.html

目录

一、基本知识

二、GC垃圾回收

1、引用计数

引用计数的增减

引用计数法有很明显的优点:

引用计数机制的缺点:

2、画说 Ruby 与 Python 垃圾回收

2.1 应用程序那颗跃动的心

一个简单的例子

免费清单

在Python中分配对象

Ruby开发人员住在凌乱的房子里

Python开发人员生活在一个整洁的家庭

标记和扫描

标记和扫描与参考计数

三、Python和Ruby中的分代GC

Python中的循环数据结构和引用计数

Python中的Generation Zero

检测循环参考

Python中的垃圾收集阈值



一、基本知识

  • 小整数[-5,257)共用对象,常驻内存
  • 单个字符共用对象,常驻内存
  • 单个单词,不可修改,默认开启intern机制,共用对象,引用计数为0,则销毁 
  • 字符串(含有空格),不可修改,没开启intern机制,不共用对象,引用计数为0,销毁 
  • 大整数不共用内存,引用计数为0,销毁 
  • 数值类型和字符串类型在 Python 中都是不可变的,这意味着你无法修改这个对象的值,每次对变量的修改,实际上是创建一个新的对象 

二、GC垃圾回收

Python采用的是引用计数机制为主标记-清除和分代收集两种机制为辅的策略

1、引用计数

要保持追踪内存中的对象,Python使用了引用计数这一简单的技术。

sys.getrefcount(a)可以查看a对象的引用计数,但是比正常计数大1,因为调用函数的时候传入a,这会让a的引用计数+1

python里每一个东西都是对象,它们的核心就是一个结构体:PyObject

typedef struct_object {
    int ob_refcnt;
    struct_typeobject *ob_type;
} PyObject;

PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少

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

当引用计数为0时,该对象生命就结束了。

引用计数的增减

1、增加引用计数

当对象被创建并(将其引用)赋值给变量时,该对象的引用计数被设置为1。

对象的引用计数增加的情况:

  • 对象被创建:x = 3.14
  • 另外的别名被创建:y = x
  • 对象被作为参数传递给函数(新的本地引用):foobar(x)
  • 对象成为容器对象的一个元素:myList = [123, x, 'xyz']

2、减少引用计数

对象的引用计数减少的情况:

  • 一个本地引用离开了其作用范围。如fooc()函数结束时,func函数中的局部变量(全局变量不会)
  • 对象的别名被显式销毁:del y
  • 对象的一个别名被赋值给其他对象:x = 123
  • 对象被从一个窗口对象中移除:myList.remove(x)
  • 窗口对象本身被销毁:del myList

3、del语句

Del语句会删除对象的一个引用,它的语法如下:del obj[, obj2[, ...objN]]

例如,在上例中执行del y会产生两个结果:

  • 从现在的名称空间中删除y
  • x的引用计数减1

引用计数法有很明显的优点:

  1. 高效
  2. 运行期没有停顿,也就是实时性:一旦没有引用,内存就直接释放了。同时把处理回收内存的时间分摊到了平时。
  3. 对象有确定的生命周期
  4. 易于实现

引用计数机制的缺点:

  • 维护引用计数消耗资源
  • 循环引用
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)

list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。 对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制。(标记清除和分代收集)

2、画说 Ruby 与 Python 垃圾回收

英文原文:

http://patshaughnessy.net/2013/10/24/visualizing-garbage-collection-in-ruby-and-python

http://patshaughnessy.net/2013/10/30/generational-gc-in-python-and-ruby
 

2.1 应用程序那颗跃动的心

GC系统所承担的工作远比"垃圾回收"多得多。实际上,它们负责三个重要任务。它们

  • 为新生成的对象分配内存
  • 识别那些垃圾对象,并且
  • 从垃圾对象那回收内存。

想象一下,如果您的应用程序是一个人体:您编写的所有优雅代码,您的业务逻辑,您的算法,将是应用程序内部的大脑或智能。 按照这个比喻,你认为垃圾收集器的身体部位是什么? [我从RuPy观众那里得到了很多有趣的答案:肾脏,白细胞:)]

我认为垃圾收集器是您应用程序的跳动核心。 正如您的心脏为身体的其他部分提供血液和营养,垃圾收集器为您的应用程序提供内存和对象。 如果你的心脏停止跳动,你会在几秒钟内死亡。 如果垃圾收集器停止或运行缓慢 - 如果它堵塞了动脉 - 你的应用程序将减速并最终死亡!

 

一个简单的例子

使用示例来理解理论总是有帮助的。这是一个用Python和Ruby编写的简单类,我们今天可以用它作为例子:

免费清单

当我们调用上面的Node.new(1)时,Ruby究竟做了什么?Ruby如何为我们创建一个新对象?

令人惊讶的是,它确实很少!实际上,在您的代码开始运行之前很久,Ruby就会提前创建数千个对象并将它们放在一个名为空闲列表的链表上。从概念上讲,这是免费列表的样子:

想象一下,上面的每个白色方块都是一个未使用的,预先创建的Ruby对象。当我们调用Node.new时,Ruby只需将其中一个对象交给我们:

在上图中,左侧的灰色方块表示我们在代码中使用的活动Ruby对象,而剩余的白色方块是未使用的对象。[注意:当然,我的图表是现实的简化版本。事实上,Ruby会使用另一个对象来保存字符串“ABC”,第三个对象用于保存Node的类定义,还有其他对象用于保存我的代码的解析后的抽象语法树(AST)表示等。

如果我们再次调用Node.new,Ruby只会给我们另一个对象:

在Python中分配对象

我们已经看到Ruby提前创建对象并将它们保存在空闲列表中。那Python怎么样?

虽然Python在内部也出于各种原因使用空闲列表(它会回收某些对象,例如列表),但它通常会为新对象和值分配内存,而不像Ruby那样。

假设我们使用Python创建一个Node对象:

与Ruby不同,Python会在您创建对象时立即向操作系统询问内存。(Python实际上实现了自己的内存分配系统,它在操作系统堆之上提供了额外的抽象层。但是我今天没有时间进入这些细节。)

当我们创建第二个对象时,Python将再次向操作系统询问更多内存

看似简单; 在我们创建对象的那一刻,Python花时间为我们查找和分配内存。

 

Ruby开发人员住在凌乱的房子里


 

                                                                      Ruby将未使用的对象留在内存中,直到下一个GC进程运行。

回到Ruby。随着我们分配越来越多的对象,Ruby将继续从自由列表中提取预先创建的对象。在这样做时,免费列表将变短:

......而且更短:

请注意,当我继续为n1分配新值时,Ruby 会将旧值留下。ABC,JKL和MNO节点保留在内存中。Ruby不会立即清理我的代码不再使用的旧对象!作为Ruby开发人员工作就像生活在一个凌乱的房子里,衣服躺在地板上或厨房水槽里的脏盘子。作为Ruby开发人员,您必须使用周围未使用的垃圾对象。

Python开发人员生活在一个整洁的家庭


Python使用它们完成代码后立即清理垃圾对象。

垃圾收集在Python中的工作方式与在Ruby中完全不同。让我们回到之前的三个Python Node对象:

在内部,每当我们创建一个对象时,Python都会在对象的C结构中保存一个整数,称为引用计数。最初,Python将此值设置为1:

值1表示对三个对象中的每一个都有一个指针或引用。现在假设我们创建了一个新节点JKL:

与以前一样,Python将JKL中的引用计数设置为1.但是,也注意到因为我们将n1更改为指向JKL,所以它不再引用ABC,并且Python将其引用计数减少到0。

此时,Python垃圾收集器立即开始行动!每当对象的引用计数达到零时,Python立即释放它,将其内存返回给操作系统:

上面的Python回收了ABC节点使用的内存。请记住,Ruby只是留下旧物体,并且不释放它们的记忆。

这种垃圾收集算法称为引用计数。它是由乔治·柯林斯(George Collins)在1960年发明的 - 同年约翰·麦卡锡(John McCarthy)发明了自由列表算法。正如迈克·伯恩斯坦在他梦幻般的说垃圾收集呈现 在世界街头红宝石会议在6月份:“1960年对垃圾收集的好年景......”

作为Python开发人员工作就像生活在一个整洁的房子里; 你知道,你的室友有点强迫症的地方,并在你之后不断清理。一旦你放下一个脏盘子或玻璃杯,有人已经把它放在洗碗机里!

现在再举一个例子。假设我们将n2设置为与n1引用相同的节点:

在左上方,您可以看到Python已经减少了DEF的引用计数,并将立即垃圾收集DEF节点。另请注意,JKL现在的引用计数为2,因为n1和 n2都指向它。

标记和扫描

最终,一个混乱的房子充满了垃圾,生活无法像往常一样继续。Ruby程序运行一段时间后,免费列表最终将被完全用完:

这里所有预先创建的Ruby对象都已被我们的应用程序使用(它们都是灰色的)并且没有对象保留在空闲列表中(没有留下白色方块)。

在这一点上,Ruby使用麦卡锡发明的另一种算法Mark and Sweep。首先Ruby停止你的应用程序; Ruby使用“停止世界垃圾收集。”然后Ruby循环遍历我们的代码对对象和其他值的所有指针,变量和其他引用。Ruby还迭代其虚拟机使用的内部指针。它使用这些指针标记它能够到达的每个对象。我在这里用字母M表示这些标记:

标有“M”的三个对象上方是我们的应用程序仍在使用的实时活动对象。在内部,Ruby实际上使用一系列称为自由位图的位来跟踪标记的对象:

Ruby将自由位图保存在单独的内存位置,以便充分利用Unix写时复制优化。

如果标记的对象是活动的,则剩余的未标记对象必须是垃圾,这意味着我们的代码不再使用它们。我将垃圾对象显示为下方的白色方块:

Next Ruby 将未使用的垃圾对象回到空闲列表中:

在内部,这种情况很快发生,因为Ruby实际上并没有将对象从一个地方复制到另一个地方。相反,Ruby通过调整内部指针以形成新的链表,将垃圾对象放回到空闲列表中。

现在,Ruby可以在下次创建新的Ruby对象时将这些垃圾对象返回给我们。在Ruby中,对象被转世,享受多重生命!

标记和扫描与参考计数

乍一看,Python的GC算法似乎远远优于Ruby:为什么当你可以住在一个整洁的房子里时,住在一个凌乱的房子里?为什么Ruby强制你的应用程序每次清理时都会定期停止运行,而不是使用Python的算法?

然而,参考计数并不像乍一看那么简单。许多语言不使用像Python这样的引用计数GC算法有很多原因:

  • 首先,它很难实施。Python必须在每个对象内留出空间来保存引用计数。对此有一个小的空间惩罚。但更糟糕的是,改变变量或引用这样的简单操作变得更复杂,因为Python需要递增一个计数器,递减另一个计数器,并可能释放该对象。

  • 其次,它可能会更慢。虽然Python在应用程序运行时可以顺利地执行GC工作(一旦将它们放入接收器中就清理脏盘子),但这不一定更快。Python不断更新引用计数值。当您停止使用大型数据结构(例如包含许多元素的列表)时,Python可能必须同时释放多个对象。减少引用计数可能是一个复杂的递归过程。

  • 最后,它并不总是有效。正如我们将在下一篇文章中看到的,其中包含本演示文稿其余部分的注释,引用计数无法处理 循环数据结构 - 包含循环引用的数据结构。

三、Python和Ruby中的分代GC

 

Python中的循环数据结构和引用计数

我们上次看到Python使用保存在每个对象内部的整数值(称为引用计数)来跟踪引用该对象的指针数量。每当程序中的变量或其他对象开始引用对象时,Python就会递增此计数器; 当程序停止使用对象时,Python会递减计数器。一旦引用计数变为零,Python就会释放对象并回收其内存。

自20世纪60年代以来,计算机科学家已经意识到这种算法存在一个理论问题:如果你的一个数据结构引用自身,如果它是一个 循环数据结构,那么一些参考计数永远不会变为零。为了更好地理解这个问题,我们举一个例子。下面的代码显示了我们上周使用的相同Node类:

我们有一个构造函数(在Python 中称为__init__),它在实例变量中保存单个属性。在类定义下面,我们创建了两个节点,ABC和DEF,我使用左边的矩形表示。我们两个节点内的引用计数最初是一个,因为一个指针(分别为n1和n2)指的是每个节点。

现在让我们在节点中定义两个附加属性,next和prev:

与Ruby不同,使用Python可以像这样动态定义实例变量或对象属性。这似乎是Ruby缺少的一些有趣的魔法。(免责声明:我不是Python开发人员,所以我可能在这里有一些错误的命名法。)我们将n1.next设置为引用n2,将n2.prev设置 为指向n1。现在,我们的两个节点使用圆形指针模式形成双向链表。另请注意,ABC和DEF的引用计数已增加到两个。有两个指针指向每个节点:n1和n2如前所述,现在也是next和 prev。

现在让我们假设我们的Python程序停止使用节点; 我们将n1和n2都设置为null。(在Python中,null称为None。)

现在,Python像往常一样将每个节点内的引用计数减少到1。

Python中的Generation Zero

请注意,在上图中我们最终得到了一个不寻常的情况:我们有一个“孤岛”或一组未使用的对象,这些对象相互引用,但没有外部引用。换句话说,我们的程序不再使用任何一个节点对象,因此我们希望Python的垃圾收集器足够智能以释放两个对象并回收其内存以用于其他目的。但这不会发生,因为两个引用计数都是一个而不是零。Python的引用计数算法无法处理相互引用的对象!

当然,这是一个人为的例子,但是你自己的程序可能包含你可能不知道的微妙方式的循环引用。事实上,随着Python程序的运行,它将构建一定数量的“浮动垃圾”,Python收集器无法处理的未使用对象,因为引用计数永远不会达到零。

这就是Python的代际算法的用武之地!就像Ruby使用链表(自由列表)跟踪未使用的自由对象一样,Python使用不同的链表来跟踪活动对象。而不是将其称为“活动列表”,Python的内部C代码将其称为Generation Zero每次在程序中创建对象或其他值时,Python都会将其添加到Generation Zero链接列表中:

上面你可以看到我们创建ABC节点时,Python将它添加到Generation Zero。请注意,这不是您在程序中看到和访问的实际列表; 此链接列表完全是Python运行时的内部。

同样,当我们创建DEF节点时,Python会将其添加到同一个链表中:

现在,Generation Zero包含两个节点对象。(它还将包含我们的Python代码创建的所有其他值,以及Python本身使用的许多内部值。)

检测循环参考

稍后Python循环遍历Generation Zero列表中的对象,并检查列表中每个对象引用的其他对象,随着时间的推移递减引用计数。通过这种方式,Python考虑了从一个对象到另一个对象的内部引用,这阻止了Python先前释放对象。

为了使这更容易理解,让我们举一个例子:

在上面你可以看到ABC和DEF节点包含引用计数1.其他三个对象也在Generation Zero链表中。蓝色箭头表示某些对象由位于其他位置的其他对象引用 - 来自Generation Zero外部的引用。(正如我们稍后将看到的,Python还使用另外两个名为Generation One和Generation Two的列表。)这些对象具有更高的引用计数,因为其他指针指向它们。

下面你可以看到Python的垃圾收集器处理Generation Zero后会发生什么。

通过识别内部引用,Python可以减少许多Generation Zero对象的引用计数。在顶行的上方,您可以看到ABC和DEF现在的引用计数为零。这意味着收集器将释放它们并回收它们的记忆。然后将剩余的活动对象移动到新的链接列表:第一代。

在某种程度上,Python的GC算法类似于Ruby使用的标记和扫描算法。它定期跟踪从一个对象到另一个对象的引用,以确定哪些对象保持活动,我们的程序仍在使用的活动对象 - 就像Ruby的标记过程一样。

 

Python中的垃圾收集阈值

Python何时执行此标记过程?当您的Python程序运行时,解释器会跟踪它分配的新对象数量,以及由于零引用计数而释放的对象数量。从理论上讲,这两个值应保持不变:程序创建的每个新对象最终都应该被释放。

当然,事实并非如此。由于循环引用,并且由于程序使用的某些对象比其他对象更长,因此分配计数和发布计数之间的差异会缓慢增长。一旦此delta值达到某个阈值,就会触发Python的收集器并使用上面的减法算法处理Generation Zero列表,释放“浮动垃圾”并将幸存的对象移动到第一代。

随着时间的推移,Python程序长时间使用的对象将从Generation Zero列表迁移到Generation One。在分配释放计数增量值达到甚至更高的阈值之后,Python以类似的方式处理第一代列表上的对象。Python将剩余的活动对象移动到第二代列表。

通过这种方式,您的Python程序长时间使用的对象,您的代码保持活动引用,从Generation Zero移动到One到Two。使用不同的阈值,Python以不同的间隔处理这些对象。Python最常处理Generation Zero中的对象,第一代处理频率较低,而第二代甚至更少。

GC模块的自动垃圾回收触发机制

参考链接:https://blog.csdn.net/u014745194/article/details/70506761

在Python中,采用分代收集的方法。把对象分为三代,一开始,对象在创建的时候,放在0代中,如果在一次0代的垃圾检查中,改对象存活下来,就会被放到一代中,同理在一次一代的垃圾检查中,该对象存活下来,就会被放到二代中。

gc模块里面会有一个长度为3的列表的计数器,可以通过gc.get_count()获取当前阀值。

例如(666,6,0),其中666是指距离上一次0代垃圾检查,Python分配内存的数目减去释放内存的数目,注意是内存分配,而不是引用计数的增加; 
66是指距离上一次一代垃圾检查,0代垃圾检查的次数; 
同理,0是指距离上一次二代垃圾检查,一代垃圾检查的次数。 

案例:

gc模快有一个自动垃圾回收的阀值,即通过gc.get_threshold函数获取到的长度为3的元组(700,10,10)。每一次计数器的增加,gc模块就会检查增加后的计数是否达到阀值的数目,如果是,就会执行对应的代数的垃圾检查,然后重置计数器

例如,假设阀值是(700,10,10):

1,当计数器从(699,6,0)增加到(700,6,0),gc模块就会执行gc.collect(0),即检查0代对象的垃圾,并重置计数器为(0,7,0)

2,当计数器从(699,9,0)增加到(700,9,0),gc模块就会执行gc.collect(1),即检查0、一代对象的垃圾,并重置计数器为(0,0,1)

3,当计数器从(699,9,9)增加到(700,9,9),gc模块就会执行gc.collect(2),即检查0、一、二代对象的垃圾,并重置计数器为(0,0,0)
 

什么时候触发垃圾回收机制

1、当gc模块的计数器达到阀值的时候,自动回收垃圾。 
2、手动调用gc.collect(),手动回收垃圾 
3、程序退出的时候,Python解释器会回收垃圾。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值