python和ruby垃圾回收机制

时间:2022-02-09 06:21:43

先了解一下蟒蛇中的其他概念:

1.小整数对象池

整数在程序中的使用非常广泛,Python为了优化速度,使用了小整数对象池,避免为整数频繁申请和销毁内存空间。

Python对小整数的定义是[-5,257)这些整数对象是提前建立好的,不会被垃圾回收。在一个Python的程序中,所有位于这个范围内的整数使用的都是同一个对象。

同理,单个字母也是这样的。

但是当定义2个相同的字符串时,引用计数为0时,触发垃圾回收

2.大整数对象池

每一个大整数,均创建一个新的对象。

得到整数对象池的范围:

a = 0 def get_max(): global a num = 0 while True#这个判断可以省略 if a == num: if ida)== id(num): a + = 1  num + = 1 elseprinta  num 'id不一样' idaid(num)) break  get_max()

                                         
a = 0 def get_min(): global a num = 0 while Trueif a == num: if ida)== id(num): a - = 1  num - = 1 elseprinta  num 'id不一样' idaid(num)) break  get_min()

                                 

得到整数池范围:( - 6〜257)

257 257 id不一样4301041264 4316057552  - 6 - 6 id不一样4301041264 4316057552

3. intern机制

A1 = '你好世界'  A2 = '世界你好'  A3 = '你好世界  A4 = '你好世界'  A5 = '你好世界'  A6 = '你好世界'  A7 = '你好世界'  A8 = '你好世界'  A9 = '世界你好' 打印IDA1)== ID(A2)== ID(A3)== ID(A4)== ID(A5)== ID(A6)== ID(A7)== ID( a8)== id(a9))

结果打印:真

思考一下,python会不会创建9个对象呢?在内存中会不会开辟9个“hello world”的内存空间呢?如果是这样的话,我们写10000个对象,比如a1 =“HelloWorld”...。 .a1000 =“HelloWorld”,那他岂不是开辟了1000个“HelloWorld”所占的内存空间了呢?如果真这样,内存不就爆了吗?所以python中有这样一个机制-intern机制,让他只占用一个” HelloWorld”的所占的内存空间。靠引用计数去维护何时释放。

总结

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

4.垃圾回收(GC垃圾回收)

现在的高级语言如JAVA,C#等,都采用了垃圾收集机制,而不再是C,C ++里用户自己管理维护内存的方式。自己管理内存极其*,可以任意申请内存,但如同一把双刃剑,为大量内存泄露,悬空指针等bug埋下隐患。对于一个字符串,列表,类甚至数值都是对象,且定位简单易用的语言,自然不会让用户去处理如何分配回收内存的问题。python里也同java一样采用了垃圾收集机制,不过不一样的是: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     ; \
    否则 \ __Py_Dealloc((PyObject *)(op))

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

引用计数机制的优点:
  • 简单
  • 实时性:一旦没有引用,内存就直接释放了不用像其他机制等到特定时机实时性还带来一个好处:处理回收内存的时间分摊到了平时。
引用计数机制的缺点:
  • 维护引用计数消耗资源
  • 循环引用
list1的 = [ 1 '字符串' 1 2 3 { '姓名''张飞龙' }  [ 4 7 5 ] list2中= [ 2 'INT' , 9 7 6 { '年龄'28 }  [ 4 3 ]]

list1 .append(list2) list2.append(list1

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

5.画说Ruby与Python垃圾回收

5.1应用程序那颗跃动的心

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

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

如果将应用程序比作人的身体:?所有你所写的那些优雅的代码,业务逻辑,算法,应该就是大脑以此类推,垃圾回收机制应该是那个身体器官呢(我从RuPy听众那听到了不少有趣的答案:腰子,白血球:))

我认为垃圾回收就是应用程序那颗跃动的心。像心脏为身体其他器官提供血液和营养物那样,垃圾回收器为你的应该程序提供内存和对象。如果心脏停跳,过不了几秒钟人就完了。如果垃圾回收器停止工作或运行迟缓,像动脉阻塞,你的应用程序效率也会下降,直至最终死掉。

5.2一个简单的例子

运用实例一贯有助于理论的理解下面是一个简单类,分别用的Python和Ruby的写成,我们今天就以此为例:

python和ruby垃圾回收机制

顺便提一句,两种语言的代码竟能如此相像:Ruby和Python在表达同一事物上真的只是略有不同。但是在这两种语言的内部实现上是否也如此相似呢?

5.3 Ruby的对象分配

当我们执行上面的Node.new时,Ruby到底做了什么?前,林心如就提前创建了成百上千个对象,并把它们串在链表上,名曰:可用列表下图所示为可用列表的概念图:

python和ruby垃圾回收机制

想象一下每个白色方格上都标着一个“未使用预创建对象”。当我们调用Node.new时,Ruby只需取一个预创建对象给我们使用即可:

python和ruby垃圾回收机制

上图中左侧灰格表示我们代码中使用的当前对象,同时其他白格是未使用对象(请注意:。无疑我的示意图是对实际的简化实际上,红宝石会用另一个对象来装载字符串 “ABC”,另一个对象装载节点类定义,还有一个对象装载了代码中分析出的抽象语法树,等等)

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

python和ruby垃圾回收机制

这个简单的用户表来预分配对象的算法已经发明了超过50年,而发明人这是赫赫有名的计算机科学家John McCarthy,一开始是用Lisp实现的.Lisp不仅是最早的函数式编程语言,在计算机科学领域也有许多创举。其一就是利用垃圾回收机制自动化进行程序内存管理的概念。

标准版的Ruby,也就是众所周知的“Matz's Ruby Interpreter”(MRI),所使用的GC算法与McCarthy在1960年的实现方式很类似。无论好坏,Ruby的垃圾回收机制已经53岁高龄了。像Lisp语言一样,Ruby的预先创建一些对象,然后在你分配新对象或者变量的时候供你使用。

5.4 Python的对象分配

我们已经了解了红宝石预先创建对象并将它们存放在可用列表中。那Python的又怎么样呢?

尽管由于许多原因Python也使用可用列表(用户回收一些特定对象比如列表),但在为新对象和变量分配内存的方面Python和Ruby是不同的。

例如我们用Pyhon来创建一个节点对象:

python和ruby垃圾回收机制

与红宝石不同,当创建对象时的Python立即向操作系统请求内存。(Python中实际上实现了一套自己的内存分配系统,在操作系统堆之上提供了一个抽象层。)当我们创建第二个对象的时候,Python会再次向操作系统请求更多内存:

python和ruby垃圾回收机制

看起来够简单吧,在我们创建对象的时候,巨蟒会花些时间为我们查找内存并分配内存。

5.5 Ruby开发者住在一个杂乱的房子里

python和ruby垃圾回收机制

红宝石把无用的对象继续留在内存里,直到下一次GC进程执行。

在红宝石里随着我们创建越来越多的对象,红宝石会持续按照预创建对象的。可用列表里给我们使用因此,可用列表会逐渐变短。:

python和ruby垃圾回收机制

...然后更短:

python和ruby垃圾回收机制

请注意我一直在为变量N1赋新值,红宝石把旧值留在原处。 “ABC”, “JKL” 和 “MNO” 三个节点实例还滞留在内存中.Ruby不会立即清除代码中不再使用的旧对象!Ruby的开发者们就像是住在一间凌乱的房间,地板上摞着衣服,要么洗碗池里都是脏盘子。作为一个Ruby的程序员,无用的垃圾对象会一直环绕着你。

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

python和ruby垃圾回收机制

在您的代码完成使用后,Python会立即清理垃圾对象

垃圾收集在Python的中的工作方式与红宝石相比完全不同。让我们回到之前的三个Python的节点对象:

python和ruby垃圾回收机制

在内部,每当我们创建一个对象时,Python中总是在对象的Ç结构体里保存一个整数,称为  最初,Python的将这个值设置为1:引用数(引用计数

python和ruby垃圾回收机制

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

python和ruby垃圾回收机制

与以前一样,Python的将JKL中的引用计数设置为1。但是,由于我们将N1 指向更改为JKL,因此它不再指向ABC,并且Python的将引用计数递减为0。

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

python和ruby垃圾回收机制

上面是Python回收了ABC节点使用的内存的实例,但是Ruby弃旧对象于原地不顾,也不释放它们的内存。

Python的这种垃圾回收算法被称为引用计数。它是由George-Collins(乔治柯林斯)于1960年发明的,恰巧与John McCarthy发明的可用列表算法在同一年出现。正如Mike-Bernstein在6月份哥谭市Ruby大会杰出的垃圾回收机制演讲中说的:“1960年是垃圾收集器的黄金年代...”

作为Python的开发人员工作就像住在一个整洁的房子里。如你所愿,你的室友有点强迫症,在并且你活动之后经常清理房间一旦你放下脏盘子或玻璃杯,室友已经把它放在洗碗机里了!

。举现在个第二例子假设我们将N2设置为与N1相同的节点

python和ruby垃圾回收机制

上图中左边的DEF的引用数已经被Python减少了,垃圾回收器会立即回收DEF对象。同时JKL的引用数已经变为了2,因为n1和n2都指向了它。

5.7标记 - 清除

最终那间凌乱的房间里充斥着垃圾,再也不能岁月静好了在Ruby的程序运行了一阵子以后,可用列表最终被用光了。:

python和ruby垃圾回收机制

在这里,所有的预先创建的红宝石对象已被我们的应用程序使用(它们都是灰色的),空闲列表中没有任何对象(没有剩下白色方块)。

在这一点上,Ruby使用了McCarthy发明的另一种算法,称为Mark and Sweep第一个Ruby停止你的应用程序  Ruby使用“停止世界垃圾回收”。然后,Ruby将遍历所有指向对象和其他值的指针,变量和其他引用。红宝石也遍历其虚拟机使用的内部指针使用这些指针标记每个可以得到的对象。我在这里用字母中号表示这些标记:

python和ruby垃圾回收机制

上面标有“M”的三个对象是我们的应用程序仍在使用的活动对象在内部,红宝石使用实际上了一系列称为*位图产品的位值来跟踪哪标记些对象是否应当被标记:

python和ruby垃圾回收机制

红宝石将空闲位图保存在单独的内存位置,以充分利用Unix的写时拷贝优化。

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

python和ruby垃圾回收机制

接下来红宝石清除这些无用的垃圾对象,把它们送回到可用列表中:

python和ruby垃圾回收机制

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

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

标记扫描(红宝石)与引用计数(蟒)

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

然而,引用计数并不像看起来那么简单。有很多原因导致许多语言不使用像Python中那样的引用计数GC算法:

  • 首先,难以实施。Python必须在每个对象的内部留出空间来容纳引用计数。这是一个小小的空间罚款。但更糟的是,一个简单的操作,例如改变变量或引用变成了一个更复杂的操作,因为Python的需要增加一个计数器,减少另一个计数器,并且可能释放该对象。

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


    事实证明,除了引用计数之外,Python还使用了第二种算法,称为generational garbage collection这意味着Python的垃圾收集器处理新创建的对象与旧的不同。从即将发布的2.1版本开始, MRI Ruby将首次采用分代GC。

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

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

    自20世纪60年代以来,计算机科学家已经意识到这种算法的理论问题:如果您的数据结构之一引用自身,如果的英文它  循环数据结构。,某些则引用计数将永远不会变为零为了更好地理解这个问题,我们举个例子,下面的代码显示了我们上周使用的同一个节点python和ruby垃圾回收机制

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

    现在让我们在节点旁边分组中定义两个附加属性
    python和ruby垃圾回收机制

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

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

    python和ruby垃圾回收机制
    现在,Python的像往常一样将每个节点内的引用计数递减到1。

    用Python的生成零

    注意上图中我们已经结束了一个不寻常的情况:我们有一个“孤岛”一或相互组引用但没有外部引用的未使用对象换句话说,我们的程序不再使用任何一个节点对象,因此我们预计Python的垃圾收集器足够聪明,可以释放这两个对象并回收它们的内存用于其他目的。但是这不会发生,因为两个引用计数都是1而不是0. Python的引用计数算法无法处理彼此引用的对象!

    当然,这是一个人为的例子,但是你自己的程序可能会包含这样的循环引用,以你可能没有意识到的微妙方式。事实上,随着您的Python的程序运行一段时间,它将会建立一定量的“浮动垃圾”,这是Python的收集器无法处理的未使用的对象,因为引用计数从未达到零。


    这就是Python的代数算法出现的地方!就像Python使用链表(空闲列表跟踪未使用的*对象一样,Python使用不同的链表来跟踪活动对象。Python的内部C代码不是将其称为“活动列表“,而是将其称为Generation Zero每次在程序中创建对象或其他值时,Python都会将其添加到Generation Zero链表中:


    python和ruby垃圾回收机制

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

    同样,当我们创建DEF节点时,巨蟒将它添加到同一个链表中:

    python和ruby垃圾回收机制
    现代,第0代包含两个节点对象。(它还将包含我们的Python代码创建的每一个其他值,以及Python本身使用的许多内部值。)

    检测循环引用

    稍后Python循环遍历生成列表中的对象,并检查列表中每个对象引用的其他对象,并在引用计数递减时进行递减。通过这种方式,Python解决了从一个对象到另一个对象的内部引用,从而阻止了Python的尽早释放对象。

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

    python和ruby垃圾回收机制

    在上面,您可以看到ABC和DEF节点包含引用计数1.其他三个对象也位于Generation Zero链接列表中。蓝色箭头表示某些对象被其他位于别处的对象引用 - 来自第零代外的引用。(由于其他指针指向它们,因此这些对象具有较高的引用计数。

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


    python和ruby垃圾回收机制

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

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

    蟒蛇中的垃圾收集阈值

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

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

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

    通过这种方式,您的Python的程序长时间使用的对象,即您的代码保持活动引用的对象,从零代移至一个至两个。使用不同的阈值,Python中以不同的时间间隔处理这些对象。Python的最频繁地处理生成零中的对象,第一代次数较少,第二代次数较少。

    弱代生假说

    这种行为是世代垃圾收集算法的关键:器收集旧比更对象地频繁新处理对象一个新的或 年轻的对象的英文您的程序刚刚创建³³的 对象,而旧一个或的成熟的对象的英文在某段时间内保持活动的对象。Python  将它从零代移到一个或从一个移动到两个时,它会促进一个对象。

    ?要这样做为什么该算法背后的基本思想被称为弱代数假设这个假设实际上包含两个想法:大多数新物体都会年轻化,而老物体很可能会长时间保持活跃状态

    假设我使用的Python或Ruby的创建一个新对象:

    python和ruby垃圾回收机制

    根据这个假设,我的代码很可能仅在短时间内使用新的ABC节点。该对象可能只是一个方法内部使用的中间值,一旦方法返回,该对象将变为垃圾。大多数新物体将以这种方式快速变为垃圾。但是,有时候,我的程序会创建一些对象,这些对象在较长时间内保持重要 - 例如Web应用程序中的会话变量或配置值。

    通过更频繁地处理生成零中的新对象,Python的垃圾收集器大部分时间都花在了最有利的地方:它处理新的对象,这些对象将快速而频繁地变成垃圾。很少,当分配阈值增加时,巨蟒的收集器是否处理较旧的对象

    回到红宝石中的免费列表

    即将发布的Ruby版本2.1现在首次使用分代垃圾收集器算法!(请记住,Ruby的其他实现,例如JRuby和Rubinius,已经使用了这个想法多年。)让我们回到我上一篇文章的*图表列表 来理解它的英文如何工作的。

    回想一下,当空闲列表用完时,红宝石标记你的程序仍在使用的对象:

    python和ruby垃圾回收机制

    在该图中,我们看到有三个活动对象,因为指针n1,  n2n3仍然指向它们。其余的物体,白色方块,是垃圾。(当然,空闲列表实际上将包含数千个以复杂模式相互作用引用的对象。我的简单图表可以帮助我交流的Ruby的GC算法的基本思想,而不会陷入细节。)

    还要回顾一下,红宝石将垃圾对象移回到空闲列表中,因为现在它们可以在分配新对象时被您的程序回收并重新使用:

    python和ruby垃圾回收机制

    Ruby 2.1中的Generational GC

    从红宝石2.1开始,GC代码采取了额外的步骤:剩余将活动的对象提升为成熟的一代(MRIç 源代码实际上使用旧的和没有  成熟。)下图显示了两个红宝石2.1对象代的概念视图:

    python和ruby垃圾回收机制

    左侧是免费列表的不同视图。我们看到垃圾对象是白色的,其余的活动对象是灰色的。灰色的物体被标记。

    标记和扫描过程完成后,Ruby 2.1将会考虑剩下的标记对象已经成熟:

    python和ruby垃圾回收机制

    Ruby 2.1没有像Python那样使用三代,而是使用了两个垃圾收集器。左边是年轻一代的新对象,右边是成熟世代的旧对象。一旦Ruby 2.1标记了一次对象,它就认为它已经成熟。红宝石投注对象一直保持活动时间足够长,以至于不会很快变成垃圾。

    重要说明:Ruby 2.1实际上并不在内存中复制对象。这些世代不包含不同的物理记忆区域。(其他语言和其他Ruby实现的一些GC算法(称为 复制垃圾收集器)实际上会在对象提升时复制对象。)相反,Ruby 2.1内部使用代码,在标记和清除过程中不包含先前标记的对象再次一旦对象被标记了一次,它就不会被包含在下一个标记和扫描过程中。

    现在假设你的Ruby的程序继续运行,创建更多新的,年轻的对象。这些在年轻一代又出现在左边:

    python和ruby垃圾回收机制

    就像Python的一样,红宝石的收藏家将精力集中在年轻一代。它只包含自标记和扫描算法中发生上次GC处理后创建的新的年轻对象。这是因为许多新对象可能已经是垃圾了(左边的白色框)。Ruby 不会重新标记右侧的成熟对象。因为他们已经在一个GC过程中幸存了下来,所以他们可能会保持活跃并且不会长时间成为垃圾。通过仅标记年轻对象,Ruby的GC代码运行得更快。它只是完全跳过成熟的对象,减少了程序等待垃圾收集完成的时间

    Ruby Ruby通过监视成熟对象的数量来决定何时运行完整的集合。当成熟对象的数量在上次完整收集后翻倍时,红宝石偶尔会运行一个“完整集合”,重新标记并重新扫描成熟对象。会清除这些标记并将所有对象再次视为年轻

    写障碍

    这种算法的一个重要挑战值得进一步解释假设您的代码创建一个新的年轻对象,并将其添加为现有成熟对象的子对象例如,如果您向已存在很长时间的数组添加新值,就会发生这种情况。

    python和ruby垃圾回收机制

    在左边,我们再次看到右侧有新的,年轻的物体和成熟的物体。在左侧,标记过程已经识别出5个新对象仍然是活动的(灰色),而两个新对象是垃圾(白色)。但是那个中心新的年轻物体呢?这是用白色标有问号的那个这个新对象是垃圾还是活动的?

    当然,它是活跃的,因为从右侧的成熟对象中可以看出它的存在。但是请记住,Ruby 2.1不包含标记和清除中的成熟对象(直到完成收集)。这意味着新对象将被错误地视为垃圾并被释放,导致程序丢失数据!

    Ruby 2.1通过监视成熟的对象来克服这个挑战,以查看你的程序是否将它们引用到新的年轻对象。Ruby 2.1使用称为写入屏障的旧GC技术来监视对成熟对象的更改 - 无论何时添加从一个对象到另一个对象的引用(无论何时写入或修改对象),都会触发写入屏障。障碍检查源对象是否成熟,如果是,则将成熟对象添加到特定列表。后来的红宝石2.1将这些修改过的成熟对象包含在下一个标记和清除过程中,从而防止活动的年轻对象被错误地视为垃圾

    Ruby 2.1实际的写屏障实现非常复杂,主要是因为现有的C扩展没有包含它们。Koichi Sasada 和Ruby核心团队使用了许多聪明的解决方案来克服这一挑战。要了解更多有关这些技术细节,看弘一的演讲引人入胜EuRuKo 2013

    站在巨人的肩膀上

    乍一看,Ruby和Python似乎非常不同地实现垃圾收集。Ruby 使用John McCarthy的原始标记和扫描算法,而Python使用引用计数。但是当我们仔细观察时,我们发现Python使用标记和扫描思想的一些部分来处理循环引用,并且Ruby和Python都以类似的方式使用分代垃圾收集。Python使用三代,而Ruby 2.1使用两代。

    这种相似性不应该是一个惊喜。这两种语言都在使用几十年前的计算机科学研究 - 在Ruby或Python发明之前。我觉得有趣的是,当你用不同的编程语言看“引擎盖下”时,你经常会发现所有人都使用相同的基本思想和算法现代编程语言对约翰麦卡锡和他的同时代人在20世纪60年代和70年代做过的突破性计算机科学研究有很大的帮助。

    一.垃圾回收机制

    Python中的垃圾回收是以引用计数为主,分代收集为辅。

    1、导致引用计数+1的情况

    • 对象被创建,例如a=23
    • 对象被引用,例如b=a
    • 对象被作为参数,传入到一个函数中,例如func(a)
    • 对象作为一个元素,存储在容器中,例如list1=[a,a]

    2、导致引用计数-1的情况

    • 对象的别名被显式销毁,例如del a
    • 对象的别名被赋予新的对象,例如a=24
    • 一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会)
    • 对象所在的容器被销毁,或从容器中删除对象

    3、查看一个对象的引用计数

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

    二.循环引用导致内存泄露

    引用计数的缺陷是循环引用的问题

    import gc
    
    
    class ClassA():
        def __init__(self):
            print('object born,id:%s'%str(hex(id(self))))
    
    def f2():
        while True:
            c1 = ClassA()
            c2 = ClassA()
            c1.t = c2
            c2.t = c1
            del c1
            del c2
    
    
    gc.disable()
    f2()

    执行f2(),进程占用的内存会不断增大。

    • 创建了c1,c2后这两块内存的引用计数都是1,执行c1.t=c2c2.t=c1后,这两块内存的引用计数变成2.
    • 在del c1后,内存1的对象的引用计数变为1,由于不是为0,所以内存1的对象不会被销毁,所以内存2的对象的引用数依然是2,在del c2后,同理,内存1的对象,内存2的对象的引用数都是1。
    • 虽然它们两个的对象都是可以被销毁的,但是由于循环引用,导致垃圾回收器都不会回收它们,所以就会导致内存泄露。

    三.垃圾回收

    import gc
    
    class ClassA():
        def __init__(self):
            print('object born,id:%s'%str(hex(id(self))))
        # def __del__(self):
        #     print('object del,id:%s'%str(hex(id(self))))
    
    def f3():
        print("-----0------")
        # print(gc.collect())
        c1 = ClassA()
        c2 = ClassA()
        c1.t = c2
        c2.t = c1
        print("-----1------")
        del c1
        del c2
        print("-----2------")
        print(gc.garbage)
        print("-----3------")
        print(gc.collect()) #显式执行垃圾回收
        print("-----4------")
        print(gc.garbage)
        print("-----5------")
    
    if __name__ == '__main__':
        gc.set_debug(gc.DEBUG_LEAK) #设置gc模块的日志
        f3()
    运行结果
    -----0------
    object born,id:0x724b20
    object born,id:0x724b48
    -----1------
    -----2------
    []
    -----3------
    gc: collectable <ClassA instance at 0x724b20>
    gc: collectable <ClassA instance at 0x724b48>
    gc: collectable <dict 0x723300>
    gc: collectable <dict 0x71bf60>
    4
    -----4------
    [<__main__.ClassA instance at 0x724b20>, <__main__.ClassA instance at 0x724b48>, {'t': <__main__.ClassA instance at 0x724b48>}, {'t': <__main__.ClassA instance at 0x724b20>}]
    -----5------
    说明:
    • 垃圾回收后的对象会放在gc.garbage列表里面
    • gc.collect()会返回不可达的对象数目,4等于两个对象以及它们对应的dict

    有三种情况会触发垃圾回收:

    1. 调用gc.collect(),
    2. 当gc模块的计数器达到阀值的时候。
    3. 程序退出的时候

    四.gc模块常用功能解析

    gc模块提供一个接口给开发者设置垃圾回收的选项。上面说到,采用引用计数的方法管理内存的一个缺陷是循环引用,而gc模块的一个主要功能就是解决循环引用的问题。

    常用函数:

    1、gc.set_debug(flags) 设置gc的debug日志,一般设置为gc.DEBUG_LEAK

    2、gc.collect([generation]) 显式进行垃圾回收,可以输入参数,0代表只检查第一代的对象,1代表检查一,二代的对象,2代表检查一,二,三代的对象,如果不传参数,执行一个full collection,也就是等于传2。 返回不可达(unreachable objects)对象的数目

    3、gc.get_threshold() 获取的gc模块中自动执行垃圾回收的频率。

    4、gc.set_threshold(threshold0[, threshold1[, threshold2]) 设置自动执行垃圾回收的频率。

    5、gc.get_count() 获取当前自动执行垃圾回收的计数器,返回一个长度为3的列表

    gc模块的自动垃圾回收机制

    必须要import gc模块,并且is_enable()=True才会启动自动垃圾回收。

    这个机制的主要作用就是发现并处理不可达的垃圾对象

    垃圾回收=垃圾检查+垃圾回收

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

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

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

    print gc.get_count() # (590, 8, 0)
    a = ClassA()
    print gc.get_count() # (591, 8, 0)
    del a
    print gc.get_count() # (590, 8, 0)
    

    3是指距离上一次二代垃圾检查,一代垃圾检查的次数,同理,0是指距离上一次三代垃圾检查,二代垃圾检查的次数。

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

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

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

    注意点

    gc模块唯一处理不了的是循环引用的类都有__del__方法,所以项目中要避免定义__del__方法

    import gc
    
    class ClassA():
        pass
        # def __del__(self):
        # print('object born,id:%s'%str(hex(id(self))))
    
    gc.set_debug(gc.DEBUG_LEAK)
    a = ClassA()
    b = ClassA()
    
    a.next = b
    b.prev = a
    
    print "--1--"
    print gc.collect()
    print "--2--"
    del a
    print "--3--"
    del b
    print "--3-1--"
    print gc.collect()
    print "--4--"
    

    运行结果:

    --1--
    0
    --2--
    --3--
    --3-1--
    gc: collectable <ClassA instance at 0x21248c8>
    gc: collectable <ClassA instance at 0x21248f0>
    gc: collectable <dict 0x2123030>
    gc: collectable <dict 0x2123150>
    4
    --4--
    

    如果把del打开,运行结果为:

    --1--
    0
    --2--
    --3--
    --3-1--
    gc: uncollectable <ClassA instance at 0x6269b8>
    gc: uncollectable <ClassA instance at 0x6269e0>
    gc: uncollectable <dict 0x61bed0>
    gc: uncollectable <dict 0x6230c0>
    4
    --4--