Python如何释放内存?你需要知道的垃圾收集

2021年9月12日16:54:35 发表评论 4,610 次浏览

本文介绍了 Python 3.7 中的垃圾回收 (GC)。

Python如何释放内存?通常,您无需担心内存管理。当不再需要对象时,Python垃圾收集机制会自动从它们中回收内存。但是,了解 GC 的工作原理可以帮助您编写更好更快的 Python 程序。

如何释放Python内存?内存管理

与许多其他语言不同,Python 不一定会将内存释放回操作系统。相反,它有一个专用的对象分配器,用于小于 512 字节的对象,它保留一些已经分配的内存块以备将来进一步使用。Python 拥有的内存量取决于使用模式。在某些情况下,只有在 Python 进程终止时才能释放所有分配的内存。

如果长时间运行的 Python 进程随着时间的推移占用更多内存,这并不一定意味着您有内存泄漏。如果你对 Python 的内存模型感兴趣,可以阅读我关于内存管理的文章。

由于大多数对象都很小,自定义内存分配器在内存分配上节省了大量时间。即使是导入第三方库的简单程序,也可以在程序生命周期内分配数百万个对象。

Python垃圾收集算法

在 Python 中,一切都是对象。甚至整数。知道何时分配它们很容易。当您需要创建一个新对象时,Python 会这样做。与分配不同,自动解除分配很棘手。Python 需要知道何时不再需要您的对象。过早地移除对象会导致程序崩溃。

Python如何释放内存?垃圾收集算法跟踪哪些对象可以被释放并选择一个最佳时间来释放它们。标准 CPython 的垃圾收集器有两个组件,引用计数收集器和分代垃圾收集器,称为gc 模块

所述引用计数算法是非常有效的和直接的,但它不能检测引用周期。这就是为什么 Python 有一个称为分代循环 GC 的补充算法。它只处理引用循环。

引用计数模块是 Python 的基础,不能被禁用,而循环 GC 是可选的,可以手动触发。

引用计数

引用计数是一种简单的技术,当程序中没有对它们的引用时,对象就会被释放。

Python清除变量内存:Python 中的每个变量都是对对象的引用(指针),而不是实际值本身。例如,赋值语句只是在右侧添加了一个新引用。单个对象可以有多个引用(变量名)。

此代码创建对单个对象的两个引用:

a = [1, 2, 3]
b = a

赋值语句本身(左侧的所有内容)从不复制或创建新数据。

为了跟踪引用,每个对象(甚至是整数)都有一个称为引用计数的额外字段,当创建或删除指向该对象的指针时,该字段会增加或减少。有关详细说明,请参阅对象、类型和引用计数部分。

引用计数增加的示例:

  • 赋值运算符
  • 参数传递
  • 将对象附加到列表中(对象的引用计数将增加)。

如果引用计数字段达到零,CPython 会自动调用特定于对象的内存释放函数。如果一个对象包含对其他对象的引用,那么它们的引用计数也会自动递减。因此,其他对象可以依次解除分配。例如,当一个列表被删除时,它所有项目的引用计数都会减少。如果另一个变量引用了列表中的一个项目,该项目将不会被释放。

在函数、类和块之外声明的变量称为全局变量。通常,这些变量会一直存在到 Python 进程结束。因此,由全局变量引用的对象的引用计数永远不会降为零。为了让它们保持活力,所有全局变量都存储在字典中。您可以通过调用该globals()函数来获取它。

在块内(例如,在函数或类中)定义的变量具有局部作用域(即,它们是其块的局部)。当 Python 解释器从块中退出时,它会破坏在块内创建的局部变量及其引用。换句话说,它只会破坏names

重要的是要了解,在程序停留在块中之前,Python 解释器假定其中的所有变量都在使用中。要从内存中删除某些内容,您需要为变量分配一个新值或从代码块中退出。在 Python 中,最流行的代码块是函数;这是大多数Python垃圾收集发生的地方。这是保持函数小而简单的另一个原因。

您始终可以使用sys.getrefcount函数检查当前引用的数量。

这是一个简单的Python释放内存代码例子:

import sys

foo = []

# 2 references, 1 from the foo var and 1 from getrefcount
print(sys.getrefcount(foo))


def bar(a):
    # 4 references
    # from the foo var, function argument, getrefcount and Python's function stack
    print(sys.getrefcount(a))


bar(foo)
# 2 references, the function scope is destroyed
print(sys.getrefcount(foo))

Python如何释放内存?在上面的例子中,你可以看到函数的引用在 Python 退出后被销毁。

Python清除变量内存:有时您需要提前删除全局或局部变量。为此,您可以使用del删除变量及其引用(而不是对象本身)的语句。这在 Jupyter 笔记本中工作时通常很有用,因为所有单元格变量都使用全局范围。

CPython 使用引用计数的主要原因是历史性的。现在有很多关于这种技术的弱点的争论。有些人声称现代垃圾收集算法可以更高效,根本不需要引用计数。引用计数算法有很多问题,比如循环引用、线程锁定、内存和性能开销。引用计数是 Python 无法摆脱GIL的原因之一。

这种方法的主要优点是可以在不再需要对象后立即轻松地销毁它们。

如何释放Python内存?分代Python垃圾收集器

当我们有引用计数时,为什么我们需要额外的垃圾收集器?

不幸的是,经典的引用计数有一个基本问题——它无法检测引用循环。当一个或多个对象相互引用时,就会发生引用循环。

这里有两个例子:Python循环引用管理

正如我们所看到的,“善堂”对象指向自身,而且,object 1object 2都指向对方。此类对象的引用计数始终至少为 1。

为了获得更好的想法,您可以使用一个简单的 Python 示例,下面是Python释放内存代码实例:

import gc

# We use ctypes moule  to access our unreachable objects by memory address.
class PyObject(ctypes.Structure):
    _fields_ = [("refcnt", ctypes.c_long)]


gc.disable()  # Disable generational gc

lst = []
lst.append(lst)

# Store address of the list
lst_address = id(lst)

# Destroy the lst reference
del lst

object_1 = {}
object_2 = {}
object_1['obj2'] = object_2
object_2['obj1'] = object_1

obj_address = id(object_1)

# Destroy references
del object_1, object_2

# Uncomment if you want to manually run garbage collection process 
# gc.collect()

# Check the reference count
print(PyObject.from_address(obj_address).refcnt)
print(PyObject.from_address(lst_address).refcnt)

在上面的例子中,该del语句删除了对我们对象的引用(即,将引用计数减 1)。在 Python 执行该del语句后,我们的对象不再能从 Python 代码访问。但是,这些对象仍然位于内存中。发生这种情况是因为它们仍在相互引用,并且每个对象的引用计数为 1。您可以使用objgraph模块直观地探索这些关系。

为了解决这个问题,Python 1.5 中引入了额外的循环检测算法。该GC模块负责这一点,只存在于处理这样的问题。

引用循环只能发生在容器对象中(即,在可以包含其他对象的对象中),例如列表、字典、类、元组。垃圾收集器算法不会跟踪除元组之外的所有不可变类型。根据某些条件,也可以不跟踪仅包含不可变对象的元组和字典。因此,引用计数技术处理所有非循环引用。

分代GC什么时候触发?Python如何释放内存?

与引用计数不同,循环 GC 不是实时工作而是定期运行。为了减少 GC 调用和微暂停的频率,CPython 使用了各种启发式方法。

Python清除变量内存:GC 将容器对象分为三代。每个新对象都从第一代开始。如果一个对象在垃圾回收轮中幸存下来,它就会移动到较旧(更高)的一代。较低代的收集频率高于较高代。由于大部分新创建的对象都在年轻时死亡,因此提高了 GC 性能并减少了 GC 暂停时间。

如何释放Python内存?为了决定何时运行,每一代都有一个单独的计数器和阈值。计数器存储自上次收集以来对象分配减去解除分配的数量。每次分配新的容器对象时,CPython 都会在第一代计数器超过阈值时进行检查。如果是这样,Python 将启动收集过程。

如果我们有两代或更多代超过阈值,GC 会选择最旧的一代。那是因为最老的世代也收集所有以前的(年轻的)世代。为了减少长寿命对象的性能下降,第三代有额外的要求可供选择。

标准阈值分别设置为 (700, 10, 10),但您始终可以使用该gc.get_threshold功能检查它们。您还可以使用该gc.set_threshold功能针对您的特定工作负载调整它们。

如何找到引用循环

很难在几段中解释引用周期检测算法。基本上,GC 迭代每个容器对象并临时删除对其引用的所有容器对象的所有引用。完全迭代后,所有引用计数低于 2 的对象都无法从 Python 的代码中访问,因此可以收集。

要完全理解循环查找算法,我建议您阅读Neil Schemenauer原始提案并从 CPython 的源代码中收集函数。此外,Quora 的回答垃圾收集器博客文章可能会有所帮助。

请注意,原始提案中描述的终结器问题自 Python 3.4 以来已得到修复。你可以在PEP 442 中阅读它。

性能提示

循环在现实生活中很容易发生。通常,您会在图形、链表或结构中遇到它们,您需要在其中跟踪对象之间的关系。如果您的程序有密集的工作负载并且需要低延迟,则需要尽可能避免引用周期。

Python清除变量内存:为了避免代码中的循环引用,您可以使用在weakref模块中实现的弱引用。与通常的引用不同,如果对象被销毁,weakref.ref则不会增加引用计数并返回None

在某些情况下,禁用 GC 并手动使用它很有用。可以通过调用禁用自动收集gc.disable()。要手动运行收集过程,您需要使用gc.collect().

如何查找和调试引用循环

调试引用周期可能会非常令人沮丧,尤其是当您使用大量第三方库时。

如何释放Python内存?标准的gc 模块提供了许多有助于调试的有用帮助程序。如果将调试标志设置为DEBUG_SAVEALL,则找到的所有无法访问的对象都将附加到gc.garbage列表中。

Python释放内存代码如下:

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直观地探索对象的关系。Python 引用计数图

结论

Python如何释放内存?大多数Python垃圾收集是通过引用计数算法完成的,我们根本无法调整。因此,请注意实现细节,但不要过早地担心潜在的 GC 问题。

希望你已经学到了一些新东西。如果您还有任何问题,我很乐意在下面的评论中回答。

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: