• 目前垃圾回收比较通用的解决办法有三种,引用计数,标记清除以及分代回收。

引用计数

  • 引用计数也是一种最直观,最简单的垃圾收集技术。
  • 在 Python 中,大多数对象的生命周期都是通过对象的引用计数来管理的。
  • 其原理非常简单,我们为每个对象维护一个 ref 的字段用来记录对象被引用的次数,每当对象被创建或者被引用时将该对象的引用次数加一,当对象的引用被销毁时该对象的引用次数减一,当对象的引用次数减到零时说明程序中已经没有任何对象持有该对象的引用,换言之就是在以后的程序运行中不会再次使用到该对象了,那么其所占用的空间也就可以被释放了了。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import os
import psutil


def print_memory_info(name):
    """
    内存计算函数
    """
    pid = os.getpid()
    p = psutil.Process(pid)

    info = p.memory_full_info()
    MB = 1024 * 1024
    memory = info.uss / MB
    print('%s used %d MB' % (name, memory))
   
def test(): 
    print_memory_info('test start')
    length = 1000 * 1000
    list1 = [i for i in range(length)]
    print_memory_info('test end')
    return list1


res = test()
print_memory_info("main end")

out:
    test start used 4 MB
	test end used 23 MB
	main end used 23 MB

由上述例子:如下情况会存在引用计数加一

  • 对象被创建(num=2)
  • 对象被引用(count=num)
  • 对象作为参数传递到函数内部
  • 对象作为一个元素添加到容器中

而如下情况会导致引用次数减一:

  • 对象的别名被显式销毁(del num)
  • 对象的别名被赋予新的对象(num=30)
  • 对象离开它的作用域(函数局部变量)
  • 从容器中删除对象,或者容器被销毁

引用计数的不足:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def test():
    print_memory_info("foo start")
    length = 1000 * 1000
    list_a = [i for i in range(length)]
    list_b = [i for i in range(length)]
    list_a.append(list_b)
    list_b.append(list_a)
    print_memory_info("foo end")

out:
    test start used 4 MB
	test end used 42 MB
	main end used 42 MB

分析:

在函数 test内部生成了两个列表 list_a 和 list_b,然后将两个列表分别添加到另外一个中。由结果可以看出,即使 foo 函数结束之后其所占用的内存空间依然未被释放。这是因为对于 list_a 和 list_b 来说虽然没有被任何外部对象引用,但因为二者之间交叉引用,以至于每个对象的引用计数都不为零,这也就造成了其所占用的空间永远不会被回收的尴尬局面。这个缺点是致命的。

为了解决交叉引用的问题,Python 引入了标记清除算法和分代回收算法。

标记清除

  • 由上,可以包含其他对象引用的容器对象都有可能产生交叉引用问题,而标记清除算法就是为了解决交叉引用的问题的。

  • 标记清除(Mark—Sweep)算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:

    • 第一阶段是标记阶段,GC会把所有的活动对象打上标记
    • 第二阶段是把那些没有标记的对象非活动对象进行回收。

    对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。

    img

    在上图中,可以从程序变量直接访问块1,并且可以间接访问块2和3。程序无法访问块4和5。第一步将标记块1,并记住块2和3以供稍后处理。第二步将标记块2,第三步将标记块3,但不记得块2,因为它已被标记。扫描阶段将忽略块1,2和3,因为它们已被标记,但会回收块4和5。

    标记清除算法作为Python的辅助垃圾收集技术,主要处理的是一些容器对象,比如list、dict、tuple等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。

    不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。

分代回收

分代回收是建立在标记清除技术基础之上的,是一种以空间换时间的操作方式。

  • 由于标记清除算法需要扫描整个堆的所有对象导致其性能有所损耗,而且当可以回收的对象越少时性能损耗越高。因此 Python 引入了分代回收算法,将系统中存活时间不同的对象划分到不同的内存区域,共三代,分别是 0 代,1 代 和 2 代。新生成的对象是 0 代,经过一次垃圾回收之后,还存活的对象将会升级到 1 代,以此类推,2 代中的对象是存活最久的对象。

那么什么时候触发进行垃圾回收算法呢 ?

  • 事实上随着程序的运行会不断的创建新的对象,同时也会因为引用计数为零而销毁大部分对象,Python 会保持对这些对象的跟踪,由于交叉引用的存在,以及程序中使用了长时间存活的对象,这就造成了新生成的对象的数量会大于被回收的对象数量,一旦二者之间的差值达到某个阈值就会启动垃圾回收机制,使用标记清除算法将死亡对象进行清除,同时将存活对象移动到 1 代。 以此类推,当二者的差值再次达到阈值时又触发垃圾回收机制,将存活对象移动到 2 代。

  • 这样通过对不同代的阈值做不同的设置,就可以做到在不同代使用不同的时间间隔进行垃圾回收,以追求性能最大。

事实上,所有的程序都有一个相同的现象,那就是大部分的对象生存周期都是相当短的,只有少量对象生命周期比较长,甚至会常驻内存,从程序开始运行持续到程序结束。而通过分代回收算法,做到了针对不同的区域采取不同的回收频率,节约了大量的计算从而提高 Python 的性能。

除了上面所说的差值达到一定阈值会触发垃圾回收之外,我们还可以显式的调用 gc.collect() 来触发垃圾回收,最后当程序退出时也会进行垃圾回收

python的gc模块

gc — 垃圾回收器接口


  • 此模块提供可选的垃圾回收器的接口,提供的功能包括:
    • 关闭收集器、调整收集频率、设置调试选项。
  • 它同时提供对回收器找到但是无法释放的不可达对象的访问。由于 Python 使用了带有引用计数的回收器,如果你确定你的程序不会产生循环引用,你可以关闭回收器。可以通过调用 gc.disable() 关闭自动垃圾回收。若要调试一个存在内存泄漏的程序,调用 gc.set_debug(gc.DEBUG_LEAK) ;需要注意的是,它包含 gc.DEBUG_SAVEALL ,使得被垃圾回收的对象会被存放在 gc.garbage 中以待检查。

gc 模块提供下列函数:

  • gc.enable()

    启用自动垃圾回收

  • gc.disable()

    停用自动垃圾回收

  • gc.isenabled()

    如果启用了自动回收则返回 True

  • gc.collect(generation=2)

    若被调用时不包含参数,则启动完全的垃圾回收。可选的参数 generation 可以是一个整数,指明需要回收哪一代(从 0 到 2 )的垃圾。当参数 generation 无效时,会引发 ValueError 异常。返回发现的不可达对象的数目。每当运行完整收集或最高代 (2) 收集时,为多个内置类型所维护的空闲列表会被清空。 由于特定类型特别是 float 的实现,在某些空闲列表中并非所有项都会被释放。

  • gc.set_debug(flags)

    设置垃圾回收器的调试标识位。调试信息会被写入 sys.stderr 。此文档末尾列出了各个标志位及其含义;可以使用位操作对多个标志位进行设置以控制调试器。

  • gc.get_debug()

    返回当前调试标识位。

  • gc.get_objects(generation=None)

    返回一个收集器所跟踪的所有对象的列表,所返回的列表除外。 如果 generation 不为 None,则只返回收集器所跟踪的属于该生成的对象。在 3.8 版更改: 新的 generation 形参。

  • gc.get_stats()

    返回一个包含三个字典对象的列表,每个字典分别包含对应代的从解释器开始运行的垃圾回收统计数据。字典的键的数目在将来可能发生改变,目前每个字典包含以下内容:collections 是该代被回收的次数;collected 是该代中被回收的对象总数;uncollectable 是在这一代中被发现无法收集的对象总数 (因此被移动到 garbage 列表中)。3.4 新版功能.

  • gc.set_threshold(threshold0[, threshold1[, threshold2]])

    设置垃圾回收阈值(收集频率)。 将 threshold0 设为零会禁用回收。垃圾回收器把所有对象分类为三代,取决于对象幸存于多少次垃圾回收。新创建的对象会被放在最年轻代(第 0 代)。如果一个对象幸存于一次垃圾回收,则该对象会被放入下一代。第 2 代是最老的一代,因此这一代的对象幸存于垃圾回收后,仍会留在第 2 代。为了判定何时需要进行垃圾回收,垃圾回收器会跟踪上一次回收后,分配和释放的对象的数目。当分配对象的数量减去释放对象的数量大于阈值 threshold0 时,回收器开始进行垃圾回收。起初只有第 0 代会被检查。当上一次第 1 代被检查后,第 0 代被检查的次数多于阈值 threshold1 时,第 1 代也会被检查。相似的, threshold2 设置了触发第 2 代被垃圾回收的第 1 代被垃圾回收的次数。

  • gc.get_count()

    将当前回收计数以形为 (count0, count1, count2) 的元组返回。

  • gc.get_threshold()

    将当前回收阈值以形为 (threshold0, threshold1, threshold2) 的元组返回。

  • gc.get_referrers(*objs)

    返回直接引用任意一个 ojbs 的对象列表。这个函数只定位支持垃圾回收的容器;引用了其它对象但不支持垃圾回收的扩展类型不会被找到。需要注意的是,已经解除对 objs 引用的对象,但仍存在于循环引用中未被回收时,仍然会被作为引用者出现在返回的列表当中。若要获取当前正在引用 objs 的对象,需要调用 collect() 然后再调用 get_referrers() 。在使用 get_referrers() 返回的对象时必须要小心,因为其中一些对象可能仍在构造中因此处于暂时的无效状态。不要把 get_referrers() 用于调试以外的其它目的。

  • gc.get_referents(*objs)

    返回被任意一个参数中的对象直接引用的对象的列表。返回的被引用对象是被参数中的对象的C语言级别方法(若存在) tp_traverse 访问到的对象,可能不是所有的实际直接可达对象。只有支持垃圾回收的对象支持 tp_traverse 方法,并且此方法只会在需要访问涉及循环引用的对象时使用。因此,可以有以下例子:一个整数对其中一个参数是直接可达的,这个整数有可能出现或不出现在返回的结果列表当中。

  • gc.is_tracked(obj)

    当对象正在被垃圾回收器监控时返回 True ,否则返回 False 。一般来说,原子类的实例不会被监控,而非原子类(如容器、用户自定义的对象)会被监控。然而,会有一些特定类型的优化以便减少垃圾回收器在简单实例(如只含有原子性的键和值的字典)上的消耗。»>>>> gc.is_tracked(0) False >>> gc.is_tracked("a") False >>> gc.is_tracked([]) True >>> gc.is_tracked({}) False >>> gc.is_tracked({"a": 1}) False >>> gc.is_tracked({"a": []}) True 3.1 新版功能.

  • gc.freeze()

    冻结 gc 所跟踪的所有对象 —— 将它们移至永久代并忽略所有未来的集合。 这可以在 POSIX fork() 调用之前使用以便令对写入复制保持友好或加速收集。 并且在 POSIX fork() 调用之前的收集也可以释放页面以供未来分配,这也可能导致写入时复制,因此建议在主进程中禁用 gc 并在 fork 之前冻结,而在子进程中启用 gc。3.7 新版功能.

  • gc.unfreeze()

    解冻永久代中的对象,并将它们放回到年老代中。3.7 新版功能.

  • gc.get_freeze_count()

    返回永久代中的对象数量。3.7 新版功能.

提供以下变量仅供只读访问(你可以修改但不应该重绑定它们):

  • gc.garbage

    一个回收器发现不可达而又无法被释放的对象(不可回收对象)列表。 从 Python 3.4 开始,该列表在大多数时候都应该是空的,除非使用了含有非 NULL tp_del 空位的 C 扩展类型的实例。如果设置了 DEBUG_SAVEALL ,则所有不可访问对象将被添加至该列表而不会被释放。在 3.2 版更改:interpreter shutdown 即解释器关闭时,若此列表非空,会产生 ResourceWarning ,即资源警告,在默认情况下此警告不会被提醒。如果设置了 DEBUG_UNCOLLECTABLE ,所有无法被回收的对象会被打印。在 3.4 版更改: 根据 PEP 442 ,带有 __del__() 方法的对象最终不再会进入 gc.garbage

  • gc.callbacks

    在垃圾回收器开始前和完成后会被调用的一系列回调函数。这些回调函数在被调用时使用两个参数: phaseinfophase 可为以下两值之一:“start”: 垃圾回收即将开始。“sweight”: 垃圾回收已结束。info is a dict providing more information for the callback. The following keys are currently defined:“generation”(代) :正在被回收的最久远的一代。“collected”(已回收的 ): 当phase 为 “sweight” 时,被成功回收的对象的数目。“uncollectable”(不可回收的): 当 phase 为 “sweight” 时,不能被回收并被放入 garbage 的对象的数目。应用程序可以把他们自己的回调函数加入此列表。主要的使用场景有:统计垃圾回收的数据,如:不同代的回收频率、回收所花费的时间。使应用程序可以识别和清理他们自己的在 garbage 中的不可回收类型的对象。3.3 新版功能.

以下常量被用于 set_debug()

  • gc.DEBUG_STATS

    在回收完成后打印统计信息。当回收频率设置较高时,这些信息会比较有用。

  • gc.DEBUG_COLLECTABLE

    当发现可回收对象时打印信息。

  • gc.DEBUG_UNCOLLECTABLE

    打印找到的不可回收对象的信息(指不能被回收器回收的不可达对象)。这些对象会被添加到 garbage 列表中。在 3.2 版更改:interpreter shutdown 时,即解释器关闭时,若 garbage 列表中存在对象,这些对象也会被打印输出。

  • gc.DEBUG_SAVEALL

    设置后,所有回收器找到的不可达对象会被添加进 garbage 而不是直接被释放。这在调试一个内存泄漏的程序时会很有用。

  • gc.DEBUG_LEAK

    调试内存泄漏的程序时,使回收器打印信息的调试标识位。(等价于 DEBUG_COLLECTABLE | DEBUG_UNCOLLECTABLE | DEBUG_SAVEALL )。

参考文章:

https://docs.python.org/zh-cn/3/library/gc.html

http://www.ityouknow.com/python/2020/01/06/python-gc-111.html

http://www.bieryun.com/5338.html