Python中的垃圾回收(Garbage Collection,简称GC)机制是一个自动内存管理过程,它负责在对象不再被使用时释放内存资源。Python的垃圾回收主要依赖于引用计数(Reference Counting)来跟踪和回收不再使用的对象。除了引用计数,Python的垃圾回收器还包括一个循环检测器,用于检测并回收循环引用中涉及的对象。
对象的底层C语言结构体
在Python中,我们知道一切皆对象,那么在在CPython实现中(CPython是Python的官方和最广泛使用的实现),每个对象确实都是由一个C语言结构体表示的。包括数字、字符串、函数等。这些对象在底层都对应着一个C语言结构体——PyObject
。
PyObject
PyObject
是所有Python对象共有的部分,它定义了对象的引用计数和类型指针。引用计数用于垃圾回收,而类型指针则指向一个表示该对象类型信息的结构体。
typedef struct _object {_PyObject_HEAD_EXTRAPy_ssize_t ob_refcnt;struct _typeobject *ob_type;
} PyObject;
ob_refcnt
是一个引用计数器,它帮助Python进行自动内存管理。ob_type
指向一个表示该对象所属类型的结构体。
PyVarObject
对于那些大小可变的数据类型(如列表、字典等),Python使用了另一种结构体——PyVarObject
。这个结构体继承自 PyObject
, 并添加了额外的成员来记录变量长度。
typedef struct {PyObject_VAR_HEADPy_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
ob_size
表示容器中元素数量。
类型特定结构体
除了通用部分之外,每种具体数据类型还有自己特定信息需要存储。例如:
-
对于整型(
PyLongObject
):typedef struct {PyObject_VAR_HEADdigit ob_digit[1]; } PyLongObject;
-
对于字符串(
PyUnicodeObject
):typedef struct {// ... 其他成员 ...Py_ssize_t length; /* Length of raw Unicode data */wchar_t *data; /* Raw Unicode buffer */// ... 其他成员 ... } PyUnicodeObject;
每种数据类型都会有其专门设计的C语言结构来存储其特定信息和行为方法(如整型数字具有不同大小和符号性质;字符串则需要记录字符序列及长度等)。通过这样丰富多样化地设计内部数据结构与算法实现细节,Python能够以统一且高效率地方式处理各类数据操作与交互逻辑。
小结
-
ob_refcnt:这是一个整数,表示对象的引用计数。当有新的引用指向对象时,这个计数器会增加;当引用被删除时,计数器会减少。当引用计数降到0时,对象将被垃圾回收器回收。
-
ob_type:这是一个指向
PyTypeObject
结构体的指针,它包含了对象的类型信息。PyTypeObject
结构体包含了与对象类型相关的数据,如类型名称、大小、类型方法、基类等。 -
ob_size:这是一个用于跟踪对象中元素数量的字段,它主要用于序列类型(如列表和元组)。对于非序列类型的对象,这个字段可能没有实际用途。
-
数据:这是对象实际数据存储的地方。具体布局取决于对象的类型。例如,对于整数对象,数据可能直接存储为整数值;而对于复合类型,数据可能包括指向其他对象的指针。
引用计数
基本概念
引用计数是一种简单直观的内存管理技术,它通过跟踪每个对象被引用的次数来决定对象是否可以被垃圾回收。引用计数是Python垃圾回收机制的核心。
通过上面的结构体我们知道每个对象都有一个引用计数,用于记录有多少个引用指向该对象。当对象被创建时,引用计数初始化为1。如果有新的引用指向该对象,引用计数增加;如果引用被删除,引用计数减少。当对象的引用计数降为0时,意味着没有任何引用指向该对象,对象将立即被回收,其占用的内存被释放。
- 创建对象时:当你创建一个新对象时(比如
a = 3
),Python会为这个对象分配内存,并将其引用计数设置为1。 - 增加引用:如果有其他变量被赋值为这个对象(例如
b = a
),该对象的引用计数会增加。 - 减少引用:如果对该对象的一个引用被销毁或者被重新赋值(例如
a = 5
),则原来指向那个对象的引用计数会减少。 - 回收内存:一旦某个对象的引用计数变为0,意味着没有任何变量或者数据结构再指向它,Python就会自动释放掉这块内存。
引用计数机制不能自动处理循环引用的情况,即两个或多个对象相互引用,导致它们的引用计数永远不会降到0。为了解决这个问题,CPython使用了一个循环垃圾收集器。这个收集器会定期运行,通过一个标记-清除算法来识别和回收循环引用的对象。
查看引用计数
在Python中,你可以通过标准库中的sys
模块来查看和操作一个对象的实际参考次数。以下是一些例子:
import sys# 创建一个列表
a = []# 查看列表a 的当前参考次数
print(sys.getrefcount(a)) # 注意: 调getrefcount本身也会临时增加一次参考, 所以结果比预期多1# 创建另外两个别名指向同一列表
b = a
c = a# 再次检查参考次数量
print(sys.getrefcount(a)) # 现在应该是4了: a, b, c 和 getrefcount 的临时调用# 删除其中一个别名减少参考数量
del b
print(sys.getrefcount(a)) # 参考数量减少到3了: a, c 和 getrefcount 的临时调用
循环垃圾收集器(Cycle-GC)
为了解决引用计数无法处理循环引用的问题,Python引入了循环垃圾收集器。这个收集器使用一种称为“标记-清除”(Mark-Sweep)的算法来检测并回收循环引用的对象。这个收集器主要针对容器对象,如列表、字典、类实例等,因为这些类型的对象更可能形成循环引用。
算法
循环垃圾收集器基于“标记-清除”(Mark-Sweep)算法
- 标记阶段:从一组根对象开始(如全局变量、活动栈帧等),遍历所有可达对象。"可达"意味着从根对象出发可以通过某种方式访问到该对象。在遍历过程中,每访问到一个对象就将其标记为活动的。注意这一阶段,并不是将“标记”的对象放入某个容器中等待回收。
- 清除阶段:遍历所有对象,将未标记的对象视为垃圾进行回收。未标记的对象即为那些在标记阶段没有从根对象可达的对象,即不再被任何活动引用所引用。这一步骤并不涉及先存放在某个容器或列表中;相反,一旦确定为垃圾,就会立刻进行资源回收处理。
触发时机
循环垃圾收集(Cyclic Garbage Collector)在Python中并不是以一个独立的监控线程的形式存在。它是Python内存管理机制的一部分,特别是在CPython实现中,它与主程序运行在同一个线程中。循环垃圾收集器会根据特定条件被触发执行,例如当分配操作计数达到某个阈值时。
- 循环垃圾收集器通常在分配了一定数量新对象或者释放了一定数量旧对象之后自动触发。
- Python也提供了手动触发GC的接口
gc.collect()
,允许开发者根据需要控制GC执行时间点。
分代回收(Generational GC)
Python的垃圾回收器还采用了分代回收策略,将对象分为三代:0代、1代和2代。新创建的对象属于0代,如果它们经过一次垃圾回收仍然存活,将被移动到1代,再经过一次回收仍然存活的对象则被移动到2代。分代回收的思想是,存活时间越长的对象,被回收的可能性越小。因此,垃圾回收器会更频繁地回收低代对象,而较少地回收高代对象。
尽管引用计数很高效,但它无法解决循环引用问题。例如两个对象相互引用,即使它们已经不再使用了,由于彼此持有对方的一个有效引用导致其引用计数永远不会降到0。为了解决这一问题,在基于引用计数之上,Python还采取了分代收集策略。
- 第0代(Generation 0):这里包含所有新创建的对象。由于许多对象会很快变得不可达,因此第0代会频繁进行垃圾回收。
- 第1代(Generation 1):当一个对象在第0代经历过一次垃圾回收后仍然存活,它会被移动到第1代。相比于第0代,这里的垃圾回收频率较低。
- 第2代(Generation 2):类似地,从第1代存活下来的对象会被移动到更稳定、更少进行垃圾回收操作的第2 。
- 如果在第一代中进行了垃圾回收仍然存活下来,则将其移动到第二代。同样地,在第二代中存活下来后会移动到第三代。随着世代等级增加, 对象在内存中存在时间越长。
- 分代算法假设生命周期短暂(如局部变量)或非常长久(如全局配置信息) 的数据较多, 因此通过调整各世代触发GC(Garbage Collection) 的阈值可以有效提高GC性能。
- 当执行分代回收时, Python会检查较年轻一些世界里面是否存在循环应用并清除那些无法访问到达外部状态 (unreachable) 的循环。
分层次进行GC
通过将新创建和短命命期预期高、以及长寿命预期高但数量相对较少且稳定性强等特点区别开来处理和管理内存资源使用情况:
- 在每一次GC执行时,并非检查所有三个世界级别上所有容器型数据结构实例状态;而是首先针对最年轻那一带(即最可能出现大量无效引用关系链条需要清理掉以释放占用资源空间情况)进行检查处理。
- 只有当年轻带上执行了足够多次GC操作之后才考虑向上逐级提升检查范围至更老旧带级别;并且随着带级别增加其检查频率也随之降低。
使用分层次方法可以显著减少必须要遍历和检查整体数据结构实例状态所需时间和计算资源消耗量——因为大部分新创建实例都可能很快就变成无效引用状态并需要清理掉;同时保证那些确实需要长时间保持有效状态直至程序运行结束阶段才释放掉占用资源空间情况下能够尽可能减少干扰影响。
背后的数据结构
每一代(generation)都有自己的链表来跟踪和管理属于该代的所有对象。这些链表使得垃圾收集器能够有效地遍历特定代中的对象,以执行垃圾回收。
在CPython中,分代回收是通过以下几个关键数据结构来实现的:
- PyObject: 这是所有Python对象共有的基础结构体。它包含了引用计数和指向其类型描述符的指针。
- gc模块: Python提供了一个内置模块
gc
,它暴露了与垃圾收集相关的功能和接口,包括手动触发垃圾回收、调整阈值、查看各代对象列表等。 - Generation链表: 每一代都有自己独立的链表来跟踪该代中所有活动(即未被回收)对象。当进行垃圾回收时,GC会根据这些链表遍历并检查每一代。每个代的
gc_generation
结构体还包含两个其他重要的字段:count
和threshold
。count
字段用于跟踪自上次垃圾回收以来分配和释放的对象数量。threshold
字段是一个阈值,当count
超过这个阈值时,就会触发该代的垃圾回收。
默认阈值
阈值分为三代(Generation 0, 1, 2),每一代都有自己的阈值。这些阈值可以通过gc.get_threshold()
函数获取,并且可以通过gc.set_threshold()
函数进行设置。
- 第0代(Generation 0)的阈值通常设置为700。
- 第1代(Generation 1)的阈值通常设置为0代 10次回收后。
- 第2代(Generation 2)的阈值通常设置为1代 10次回收后。
调整阈值
开发者可以根据应用程序的需要调整这些阈值。例如,如果你认为应用程序中的对象生命周期更长,你可能希望减少垃圾回收的频率以避免性能开销。在这种情况下,你可以增加阈值。相反,如果你希望更积极地回收不再使用的对象,你可以减少阈值。
import gc# 获取当前的阈值设置
thresholds = gc.get_threshold()
print("当前的gc分代阈值分别是:", thresholds)# 设置新的阈值
new_thresholds = (600, 10, 5) # 分别为第0、1、2代的阈值, 0代为对象个数,1代和2代为对应前一代的gc次数
gc.set_threshold(*new_thresholds)
thresholds = gc.get_threshold()
print("最新的gc分代阈值分别是:", thresholds)
GC优化
Python的内存管理和垃圾回收机制包括多种优化策略,以提高性能和减少内存使用。其中,对象池(Object Pools)和空闲列表(Free Lists)是两个重要的优化手段。这些技术主要用于管理小对象的分配和回收,因为频繁地为小对象分配和释放内存会导致大量的性能开销。
对象池
前面的文章提到过:https://blog.csdn.net/weixin_39743356/article/details/136077846
Python中最著名的对象池是针对小整数(Small Integers)和短字符串(Short Strings)的优化。
-
小整数池:Python解释器启动时会创建一个范围在
[-5, 256]
之间的整数对象池。当程序需要这个范围内的任何整数时,Python都会从这个池中返回相应的引用而不是新建一个对象。这样做可以避免频繁创建和销毁常用数字对象。 -
短字符串驻留:对于一些内容相同且长度较短的字符串,在程序运行期间只会创建一次并被重复使用。这种机制称为字符串驻留(String Interning),它可以帮助节省内存并加速字典类型键值对查找。
空闲列表
空闲列表是另一种优化技术,主要用于快速分配与回收特定类型对象所占用的内存空间。
-
列表、字典、集合等容器类型:当这些容器被销毁时,它们占据的内存不会立即返回给操作系统,而是保留在一个空闲列表中。下次再创建同类型容器时,就可以直接重用这块内存区域。
-
元组: Python还维护了一个专门针对元组大小不同情况下可复用数据块状态记录表——每当有新元组需要创建且其大小符合某个已存在记录项所描述状态时,则直接从该记录项关联空闲列表中取出一块数据区域进行初始化使用;反之则将不再需要且大小符合某记录项描述状态得到旧元组数据区域归还至相关联空闲列表备份后续可能需求。
内部碎片处理
在Python中,内部碎片(Internal Fragmentation)是指在内存分配过程中,由于分配的内存块不能完全被使用而导致的内存空间浪费现象。这种情况通常发生在使用固定大小的内存块分配策略时,例如,当一个对象所需的内存大小不是内存块大小的整数倍时,就会在分配的内存块中留下未使用的部分,这就是内部碎片。
除了上述策略外,CPython还通过“块”(block)管理来减少因小规模分配造成的外部碎片问题:
-
PyMalloc是Python内存管理系统中的一个组件,它负责处理对象的内存分配请求。PyMalloc的设计目的是为了优化小对象的内存分配,减少内存碎片,提高内存分配的效率。
在Python的内存管理中,对象根据大小被分为不同的类别。PyMalloc主要负责分配小于某个阈值(通常是256字节)的小对象。它使用了一个称为“池”(pool)的数据结构来管理这些小对象的内存分配。这些池被组织成多个“块”(block),每个块包含多个大小相同的对象。
当Python代码创建一个小对象时,PyMalloc会尝试在一个已有的块中找到一个足够大的空闲空间来放置这个对象。如果没有合适的空闲空间,PyMalloc会分配一个新的块来满足请求。这种分配策略有助于减少内存碎片,并提高内存分配的速度。