您需要了解的有关Python中垃圾收集器的所有信息

通常,您在编写Python代码时无需担心垃圾收集器和使用内存的问题。 一旦不再需要对象,Python就会自动从它们下面释放内存。 尽管如此,了解GC的工作原理将有助于您编写更好的代码。

内存管理器


与其他流行语言不同,Python删除对象后不会立即将所有内存释放回操作系统。 相反,它使用为小对象(大小小于512字节)设计的附加内存管理器。 为了处理此类对象,他分配了大块内存,将来会在其中存储许多小对象。

一旦删除了其中一个小对象-它下面的内存就不会进入操作系统,Python会将其留给具有相同大小的新对象。 如果分配的内存块之一中没有剩余的对象,则Python可以将其释放到操作系统。 通常,当脚本创建许多临时对象时释放块。

因此,如果长时间运行的Python进程开始随着时间消耗更多的内存,则这并不意味着您的代码存在内存泄漏问题。 如果您想了解更多有关Python中的内存管理器的信息,可以在我的另一篇文章中了解它。

垃圾收集算法


标准python解释器(CPython)一次使用两种算法,即引用计数和世代垃圾收集器(以下称为GC),这是Python的标准gc模块

链接计数算法非常简单有效,但是它有一个很大的缺点。 他不知道如何定义循环引用。 因此,在python中有一个额外的收集器,称为世代GC,用于监视具有潜在循环引用的对象。

在Python中,引用计数算法是基本的,不能禁用,而GC是可选的,可以禁用。

链接计数算法


链接计数算法是最简单的垃圾收集技术之一。 一旦不再引用对象,它们将被删除。

在Python中,变量不存储值,但充当对对象的引用。 也就是说,当您将值分配给新变量时,首先会创建一个具有该值的对象,然后该变量才开始引用它。 多个变量可以引用一个对象。

Python中的每个对象都包含一个附加字段(引用计数器),该字段存储指向该对象的链接数。 只要有人引用一个对象,该字段就会增加一。 如果由于某种原因链接消失了,那么该字段将减少一。

链接数增加时的示例:

  • 赋值运算符
  • 传递参数
  • 在工作表中插入一个新对象(该对象的链接数量增加)
  • 形式foo = bar的构造(foo开始与bar指代同一对象)

一旦特定对象的参考计数器达到零,解释器就会开始销毁该对象的过程。 如果远程对象包含指向其他对象的链接,则这些链接也将被删除。 因此,去除一个物体可能需要去除其他物体。

例如,如果删除列表,则其所有元素中的引用计数都将减少一。 如果列表中的所有对象未在其他任何地方使用,则它们也将被删除。

在函数,类和块之外声明的变量称为全局变量。 通常,此类变量的生命周期等于Python进程的生命周期。 因此,对全局变量引用的对象的引用数量永远不会降为零。

在块内声明的变量(函数,类)具有局部可见性(即,仅在块内可见)。 python解释器一旦退出该块,它就会破坏其中的局部变量创建的所有链接。

您始终可以使用sys.getrefcount函数检查链接数。

链接计数器的示例:

 foo = [] # 2 ,    foo    getrefcount print(sys.getrefcount(foo)) def bar(a): # 4  #  foo,   (a), getrefcount       print(sys.getrefcount(a)) bar(foo) # 2 ,      print(sys.getrefcount(foo)) 

标准解释器(CPython)使用引用计数器的主要原因是历史性的。 目前,有关此方法的争论很多。 有人认为,如果没有链接计数算法,垃圾收集器的效率会更高。 该算法存在许多问题,例如循环链接,阻塞线程以及内存和cpu的额外开销。

该算法的主要优点是,不需要的对象会立即删除。

可选垃圾收集器


当我们已经有引用计数时,为什么需要额外的算法?

不幸的是,经典的链接计数算法有一个很大的缺点-它不知道如何找到循环链接。 当一个或多个对象相互引用时,就会发生环回。

两个例子:

图片

如您所见,第一个对象引用自身,而object1object2引用。 对于此类对象,参考计数将始终为1。

Python演示:

 import gc #  ctypes        class PyObject(ctypes.Structure): _fields_ = [("refcnt", ctypes.c_long)] gc.disable() #   GC lst = [] lst.append(lst) #    lst lst_address = id(lst) #   lst del lst object_1 = {} object_2 = {} object_1['obj2'] = object_2 object_2['obj1'] = object_1 obj_address = id(object_1) #   del object_1, object_2 #          # gc.collect() #    print(PyObject.from_address(obj_address).refcnt) print(PyObject.from_address(lst_address).refcnt) 

在上面的示例中,del指令删除了对我们对象的引用(而不是对象本身)。 Python执行del语句后,这些对象将无法从Python代码访问。 但是,关闭gc模块后,它们仍将保留在内存中,因为 他们有通函,计数器仍然是一。 您可以使用objgraph库以可视方式探索此类关系。

为了解决此问题,在Python 1.5中添加了称为gc模块的其他算法。 唯一的任务是删除不再可以从代码访问的循环对象。

环回只能发生在“容器”对象中。 即 在可以存储其他对象的对象中,例如列表,字典,类和元组。 除了元组,GC不会跟踪简单和不可变的类型。 当满足某些条件时,某些元组和词典也将从跟踪列表中排除。 对于所有其他对象,保证可以应对参考计数算法。

触发GC时


与参考计数算法不同,循环GC不能实时工作,而是定期运行。 收集器的每次运行都会在代码中创建微暂停,因此CPython(标准解释器)使用各种启发式方法来确定垃圾收集器的频率。

循环垃圾收集器将所有对象分为3代(Generations)。 新对象属于第一代。 如果新设施在垃圾收集过程中仍然存在,那么它将转移到下一代。 生成的次数越高,对其进行垃圾扫描的频率就越少。 由于新对象的寿命通常很短(是临时的),因此与已经经历了多个垃圾收集阶段的对象相比,对它们进行更多访问是有意义的。

每个世代都有一个特殊的计数器和一个响应阈值,达到该阈值时将触发垃圾收集过程。 每个计数器存储分配数量减去给定代中的重新分配数量。 只要在Python中创建了任何容器对象,它就会检查这些计数器。 如果条件有效,则垃圾收集过程开始。

如果几代或更多世代一次超过阈值,则选择最高级。 这是由于以下事实:较早的几代人还会扫描所有以前的人。 为了减少长寿命对象的垃圾回收暂停次数,最老的一代还具有一组附加条件

世代的标准阈值分别设置为gc.get_threshold gc.set_threshold 700, 10 10 ,但是您始终可以使用gc.get_threshold gc.set_threshold来更改它们。

循环搜索算法


循环搜索算法的完整描述将需要单独的文章。 简而言之,GC会遍历选定世代中的每个对象,并暂时删除单个对象中的所有链接(此对象引用的所有链接)。 经过一整遍之后,所有链接数少于两个的对象都被认为无法从python访问,并且可以删除。

为了更深入地理解,我建议阅读(译者注:英文资料) 来自Neil Schemenauer的算法原始描述以及来自CPython来源collect函数。 Quora的描述以及有关垃圾收集器帖子也可能很有用。

值得注意的是,自Python 3.4起,算法原始说明中描述的析构函数问题已得到解决( PEP 442中有更多详细信息)。

优化技巧


循环通常发生在现实生活中的任务中;它们可以在图形,链接列表或需要跟踪对象之间关系的数据结构中发现。 如果您的程序负载很大并且需要延迟,那么最好避免循环。

在您有意使用圆形链接的地方,可以使用“弱”链接。 弱链接是在weakref模块中实现的,与常规链接不同,它不会以任何方式影响链接计数器。 如果事实证明具有弱引用的对象被删除,则返回None

在某些情况下,禁用gc模块的自动构建并手动调用非常有用。 为此,只需调用gc.disable() ,然后手动调用gc.collect()

如何查找和调试循环链接


调试循环可能很痛苦,尤其是在您的代码使用许多第三方模块的情况下。

gc模块提供了有助于调试的帮助程序功能。 如果GC参数设置为DEBUG_SAVEALL标志,则所有不可访问的对象都将添加到gc.garbage列表中。

 import gc gc.set_debug(gc.DEBUG_SAVEALL) print(gc.get_count()) lst = [] lst.append(lst) list_id = id(lst) del lst gc.collect() for item in gc.garbage: print(item) assert list_id == id(item) 

一旦确定了问题点,就可以使用objgraph对其进行可视化。

图片

结论

主要的垃圾收集过程由链接计数算法执行,该算法非常简单并且没有设置。 附加算法仅用于搜索和删除具有循环引用的对象。

您不应该对垃圾收集器的代码进行过早的优化;在实践中,垃圾收集的问题很少见。

PS:我是本文的作者,您可以提出任何问题。

Source: https://habr.com/ru/post/zh-CN417215/


All Articles