Python中的内存管理方法介绍

217 阅读25分钟

内存管理是有效管理计算机内存(RAM)的过程。它包括在程序要求时,在运行时为程序分配一块内存,并在程序不再需要时释放所分配的内存以便重新使用。

在C或Rust等语言中,内存管理是程序员的责任。程序员必须在程序使用前手动分配内存,并在程序不再需要时释放它。在 Python 中,内存管理是自动的!Python 自动处理内存的分配和删除。

在这篇文章中,我们将讨论 Python 中内存管理的内部情况。我们还将介绍基本单元,如对象,如何存储在内存中,Python中不同类型的内存分配器,以及Python的内存管理器如何有效地管理内存。

了解Python中内存管理的内部结构有助于设计内存效率高的应用程序。它也使调试应用程序中的内存问题变得更加容易。

目录

让我们从了解Python作为一种语言规范开始,然后深入了解CPython!

作为一种语言规范的Python

一种编程语言是一组规则和规范,在一个参考文件中定义。

Python 是一种编程语言,而是 Python 的参考文件,它陈述了 Python 语言的规则和规范。

例如,Python语言规范指出,要定义一个函数,我们必须使用def 关键字。这只是一个规范,我们必须让计算机理解,通过使用def function_name ,我们打算定义一个名字为function_name 的函数。我们必须写一个程序来实现这些规则和规范。

Python语言的规则和规范由各种编程语言来实现,如C、Java和C#。Python语言在C语言中的实现被称为CPython,而Python语言在Java和C#中的实现分别被称为JythonIronPython

什么是CPython?

CPython是Python语言的默认和最广泛使用的实现。当我们说Python时,实质上就是指CPython。当你从python.org 下载 Python 时,基本上就是下载了 CPython 代码。因此,CPython 是一个用 C 语言编写的程序,它实现了所有由 Python 语言定义的规则和规范。

CPython是Python编程语言的参考实现。CPython可以被定义为解释器和编译器,因为它在解释Python代码之前将其编译为字节码。-维基百科

由于CPython是参考实现,所有Python语言的新规则和规范都首先由CPython实现。

在这篇文章中,我们将讨论CPython的内存管理的内部情况。

请注意:其他的实现,如JythonIronPython,可能以不同的方式实现内存管理。

由于CPython是用C语言实现的,我们首先来了解一下C语言中与内存管理相关的两个重要函数:mallocfree!

什么是C语言中的mallocfree 函数?

首先,malloc 是C编程语言中用来在运行时向操作系统请求一块内存的方法。当程序在运行时需要内存时,它会调用malloc 方法来获得所需的内存。

其次,free 是C语言编程中的一个方法,当程序不再需要内存时,将分配给程序的内存释放或放回给操作系统。

当Python(CPython)程序需要内存时,CPython内部调用malloc 方法来分配内存。当程序不再需要内存时,CPython调用free 方法来释放它。

接下来,让我们看看内存是如何分配给Python中的不同对象的!

Python中的对象

在Python中所有的东西都是一个对象。函数,甚至简单的数据类型,如整数浮点数字符串,都是Python中的对象。当我们在Python中定义一个整数时,CPython内部会创建一个整数类型的对象。这些对象被存储在堆内存中。

每个 Python 对象由三个字段组成。

  • 类型
  • 参考数

让我们考虑一个简单的例子。

a = 100

当上面的代码被执行时,CPython创建了一个类型为integer 的对象,并在堆内存中为这个对象分配了内存。

type 表示该对象在CPython中的类型,而value 字段,顾名思义,存储该对象的值(本例中为100 )。我们将在文章的后面讨论ref_count 字段。

Python中的变量

Python 中的变量只是对内存中实际对象的引用。它们就像名字或标签,指向内存中的实际对象。它们不存储任何值。

考虑一下下面的例子。

a = 100

正如前面所讨论的,当上面的代码被执行时,CPython内部创建了一个整数类型的对象。变量a 指向这个整数对象,如下图所示。

Variable a points to the integer object

我们可以在Python程序中使用变量a 来访问这个整数对象。

让我们把这个整数对象分配给另一个变量b

b = a

当上面的代码被执行时,变量ab 都指向同一个整数对象,如下图所示。

Variable a and b points to the integer object

现在让我们把这个整数对象的值增加1。

# Increment a by 1
a = a + 1

当上述代码被执行时,CPython创建了一个新的整数对象,其值为101 ,并使变量a 指向这个新的整数对象。变量b 将继续指向数值为100 的整数对象,如下图所示。

Increment variable a

在这里,我们可以看到,CPython 没有用101 覆盖100 的值,而是创建了一个新的对象,其值为101 ,因为 Python 中的整数是不可变的。一旦创建,它们就不能被修改。请注意,浮点数和字符串数据类型在Python中也是不可变的。

让我们考虑一个简单的 Python 程序来进一步解释这个概念。

i = 0

while i < 100:
    i = i + 1

上面的代码定义了一个简单的while 循环,它增加变量i 的值,直到它小于100 。当这段代码被执行时,对于变量i 的每一次增量,CPython 将用增量的值创建一个新的整数对象,而旧的整数对象将被从内存中删除(更准确地说,这个对象将变得有资格被删除)。

CPython为每个新对象调用malloc 方法,为该对象分配内存。它调用free 方法来从内存中删除旧对象。

让我们用mallocfree 来转换上述代码。

i = 0  # malloc(i)

while i < 100:
    # malloc(i + 1)
    # free(i)
    i = i + 1

我们可以看到,CPython创建和删除了大量的对象,即使对于这个简单的程序。如果我们为每个对象的创建和删除都调用mallocfree 方法,就会降低程序的执行性能,使程序变得缓慢。

因此,CPython引入了各种技术来减少我们为每个小对象的创建和删除而调用mallocfree 的次数。现在让我们来了解一下CPython是如何管理内存的!

CPython中的内存管理

Python中的内存管理涉及到对私有堆的管理。私有堆是专属于Python进程的一部分内存。所有的Python对象和数据结构都存储在私有堆中。

操作系统不能将这部分内存分配给另一个进程。私有堆的大小可以根据 Python 进程的内存需求而增长和缩小。私有堆由定义在 CPython 代码中的 Python 内存管理器管理。

为了便于表述,CPython中的私有堆可以被分为多个部分,如下图所示。

Heap memory partition

请注意,这些部分的边界不是固定的,可以根据需要增加或减少。

  1. Python Core 非对象内存。分配给Python核心非对象数据的那部分内存。
  2. 内部缓冲区。分配给内部缓冲区的那部分内存。
  3. 特定对象内存- 分配给具有特定对象内存分配器的对象的那部分内存。
  4. 对象内存。分配给对象的那部分内存。

当程序请求内存时,CPython使用malloc 方法向操作系统请求该内存,私有堆的大小就会增长。

为了避免在每个小对象的创建和删除中调用mallocfree ,CPython为不同的目的定义了多个分配器和去分配器。我们将在下一节中详细讨论它们中的每一个!

内存分配器

为了避免频繁调用mallocfree 方法,CPython定义了一个分配器的层次结构,如下图所示。

Memory allocators hierarchy

下面是内存分配器的层次结构,从基础层开始:

  • 通用分配器(CPython的malloc 方法)
  • 原始内存分配器(用于大于512字节的对象)
  • 对象分配器(用于小于或等于512字节的对象)
  • 特定对象分配器(针对特定数据类型的特定内存分配器)

在基础层是general-purpose 分配器。general-purpose 分配器是CPython的C语言的malloc 方法。它负责与操作系统的虚拟内存管理器进行交互,并将所需的内存分配给Python进程。这是唯一一个与操作系统的虚拟内存管理器进行通信的分配器。

general-purpose 分配器的顶部是 Python 的raw memory 分配器。raw memory 分配器为general-purpose 分配器 (即malloc 方法) 提供了一个抽象的概念。当一个 Python 进程需要内存时,raw memory 分配器与general-purpose 分配器交互,以提供所需的内存。它确保有足够的内存来存储 Python 进程的所有数据。

raw memory 分配器之上,我们有对象分配器。这个分配器用于为小对象(小于或等于512字节)分配内存。如果一个对象需要超过512字节的内存,Python的内存管理器直接调用raw memory 分配器。

从上面的表述中可以看出,我们在对象分配器之上还有特定对象的分配器。简单的数据类型,如整数浮点数字符串列表,都有各自的特定对象分配器。这些特定对象的分配器根据对象的要求实现内存管理策略。例如,整数的特定对象分配器与浮点数的特定对象分配器有不同的实现。

特定对象分配器和对象分配器都在已经由原始内存分配器分配给Python进程的内存上工作。这些分配器从不向操作系统请求内存。它们在私有堆上操作。如果对象分配器或特定对象分配器需要更多的内存,Python 的原始内存分配器通过与通用分配器的交互来提供。

Python中的内存分配器的层次结构

Allocator hierarchy state diagram

当一个对象请求内存,并且该对象有定义的特定对象分配器时,特定对象分配器被用来分配内存。

如果对象没有特定对象的分配器,并且请求的内存超过512字节,Python内存管理器直接调用原始内存分配器来分配内存。

如果请求的内存大小小于512字节,则使用对象分配器来分配。

对象分配器

对象分配器也被称为pymalloc 。它用于为小于512字节大小的小对象分配内存。

CPython代码库将对象分配器描述为

一个针对小块的快速、特殊用途的内存分配器,用于通用的malloc之上。

除非特定对象的分配器实现了专有的分配方案(例如:ints使用一个简单的free list),否则每个对象的分配和删除(PyObject_New/Del)都会调用它。

这也是周期性垃圾收集器对容器对象进行选择性操作的地方。

当一个小对象请求内存时,对象分配器并不只是为该对象分配内存,而是向操作系统请求一大块内存。这个大的内存块随后被用来为其他小对象分配内存。

这样一来,对象分配器就避免了为每个小对象调用malloc

对象分配器分配的大块内存被称为Arena 。Arenas的大小为256KB。

为了有效地使用Arenas ,CPython将Arena 划分为Pools 。池的大小为4KB。因此,一个arena 可以由64个(256KB / 4KB)池组成。

Arenas, pools, and blocks

池被进一步划分为Blocks

接下来,我们将讨论这些组件中的每一个!

区块

区块是对象分配器可以分配给一个对象的最小的内存单位。一个块只能分配给一个对象,而一个对象只能分配给一个块。不可能将一个对象的一部分放在两个或多个独立的块中。

区块有不同的大小。一个块的最小尺寸是8字节,而一个块的最大尺寸是512字节。一个块的大小是8的倍数,因此,块的大小可以是8、16、32、...、504、或512字节。每个区块大小都被称为大小类。一共有64个大小类,如下图所示。

Block size class

从上表中可以看出,大小类0的块的大小为8字节,而大小类1的块的大小为16字节,以此类推。

程序总是被分配到一个完整的块,或者根本没有块。因此,如果一个程序请求14字节的内存,它将被分配一个16字节的块。同样,如果一个程序请求35字节的内存,就会分配一个40字节的块。

池子

一个池由只有一个大小级别的块组成。例如,一个池子里有一个大小为0的块,就不能有任何其他大小的块。

池的大小等于虚拟内存页的大小。这里'虚拟内存页这个术语的意思。

页,内存页,或虚拟页是一个固定长度的连续的虚拟内存块。它是虚拟内存操作系统中内存管理的最小的数据单位。-维基百科

在大多数情况下,池的大小是4KB。

只有当没有其他可用的池子拥有所要求的大小类的块时,池子才会从Arenas中分割出来。

一个池子可以处于三种状态之一:

  1. 已使用:如果一个池子有可供分配的块,就说它处于used 状态。

  2. :如果一个池子的所有区块都被分配了,则表示该池子处于full 状态。

  3. :如果一个池子的所有块都可以分配,则表示该池子处于empty 状态。空池没有与之相关的大小类别。它可以被用来分配任何大小类的块。

池在CPython代码中的定义如下所示。

/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* number of allocated blocks    */
    block *freeblock;                   /* pool's free list head         */
    struct pool_header *nextpool;       /* next pool of this size class  */
    struct pool_header *prevpool;       /* previous pool       ""        */
    uint arenaindex;                    /* index into arenas of base adr */
    uint szidx;                         /* block size class index        */
    uint nextoffset;                    /* bytes to virgin block         */
    uint maxnextoffset;                 /* largest valid nextoffset      */
};

术语szidx 表示池的大小类别。如果szidx ,它将只拥有大小等级为0的块(即8字节的块)。

术语arenaindex 表示池子所属的竞技场。

相同大小级别的池子用双链表互相链接。nextpool 指针指向下一个相同大小等级的池,而prevpool 指针指向上一个相同大小等级的池。

下面是相同大小类的池子是如何连接的:

Pools double linked list

freeblock 指针指向池内自由块的单链列表的开始。当一个分配的块被释放时,它被插入到freeblock 指针的前面。

Free block pointer

如上图所示,allocated 块被分配给对象,而free 块被分配给对象,但现在是自由的,可以分配给新的对象。

当对内存提出请求时,如果没有可用的具有所请求大小类别的块的池,CPython会从Arena中划分出一个新的池。

当一个新的内存池被分割出来时,整个内存池不会立即被分割成块状。块是在需要的时候从池中分割出来的。上图中水池的Blue 阴影区域表示水池的这些部分还没有被分割成块。

CPython代码库中的一段摘录提到了从池中分割块的情况,如下所示。

当一个池子被初始化时,池子中的可用块并没有被全部链接在一起。相反,只有 "前两个"(最低地址)区块被设置,返回第一个这样的区块,并将pool->freeblock设置为持有第二个这样的区块的单块列表。这与pymalloc在所有级别(场、池和块)上的努力是一致的,即在真正需要之前绝不碰一块内存。

Free block pointer of a new pool

在这里,我们可以看到,当一个新的池子从竞技场被分割出来时,只有前两个块从池子里被分割出来。一个区块被分配给申请内存的对象,而另一个区块是空闲的或未被触及的,freeblock 指针指向这个区块。

CPython维护了一个名为usedpools 的数组,以跟踪所有大小类的used 状态的池(可供分配的池)。

usedpools 数组的索引等于池的大小类。对于usedpools 数组的每个索引iusedpools[i] 指向大小类池的标题i 。例如,usedpools[0] 指向大小类池的标题0usedpools[1] 指向大小类池的标题1

下面的图应该更容易理解:

Used pools array

由于同一大小类的池子是使用双链表相互链接的,因此可以使用usedpools 数组来遍历每个大小类的used 状态下的所有池子。

如果usedpools[i] 指向null ,这意味着在used 状态下没有大小类的池子i 。如果一个对象请求一个大小类的块i ,CPython将划分一个新的大小类池i ,并更新usedpools[i] ,以指向这个新池。

如果一个块从full 状态的池中释放出来,则该池的状态会从full 变成used 。CPython将这个池子添加到其大小类的池子的双链列表的前面。

Full pool moved to the usedpools array

如上图所示,poolX 属于大小类0 ,它处于full 状态。当一个块从poolX ,它的状态就会从full 变成used 。一旦poolX 的状态变成了used ,CPython就会把这个池子添加到大小类0 的池子的双链列表的前面,并且usedpools[0] 将开始指向这个池子。

竞技场

缓冲区是用于为小对象分配内存的大块内存。它们是由原始内存分配器分配的,大小为256KB。

当一个小对象请求内存,但没有现有的区域来处理这个请求时,原始内存分配器不是只为这个小对象请求内存,而是从操作系统中请求一个大的内存块(256KB)。这些大的内存块被称为竞技场。

当需要时,池(4KB大小)从竞技场中分割出来。

考虑一下arena_object ,定义在CPython代码库中

struct arena_object {
    /* The address of the arena, as returned by malloc */ 
    uintptr_t address;

    /* Pool-aligned pointer to the next pool to be carved off. */
    block* pool_address;

    /* The number of available pools in the arena:  free pools + never-
     * allocated pools.
     */
    uint nfreepools;

    /* The total number of pools in the arena, whether or not available. */
    uint ntotalpools;

    /* Singly-linked list of available pools. */
    struct pool_header* freepools;

    struct arena_object* nextarena;
    struct arena_object* prevarena;
};

freepools 指针指向空闲池的列表,空闲池没有分配它们的任何块。

nfreepools 项表示竞技场中空闲池的数量。

CPython维护了一个名为usable_arenas 的双链表,以跟踪所有具有available 池的竞技场。

Available 池处于 或 状态。empty used

nextarena 指针指向下一个可用的竞技场,而prevarena 指针指向usable_arenas 双链表中的上一个可用的竞技场。双链表是按照nfreepools 值的递增顺序排序的。

usable_arenas doubly linked list

如上图所示,usable_arenas 是根据nfreepools 进行排序的。有0个空闲池的场馆是第一项,其次是有1个空闲池的场馆,以此类推。这意味着列表的排序是以分配最多的竞技场为先。我们将在文章的下一节解释为什么这种排序方式是进口的。

可用的竞技场列表是根据哪些竞技场有最多的分配来排序的,所以当有内存分配的请求时,它将从有最多分配的竞技场中提供。

Python进程会释放内存吗?

当池中分配的块被释放时,CPython不会将内存返回给操作系统。这个内存继续属于Python进程,CPython使用这个块来为新的对象分配内存。

即使一个池中的所有块都被释放,CPython也不会将池中的任何内存返回给操作系统。CPython将整个内存池的内存保留给自己使用。

CPython在竞技场层面上释放内存给操作系统,而不是在块或池层面上。另外,请注意,CPython是一次性释放整个竞技场的内存。

由于内存只能在竞技场级别释放,CPython只有在绝对必要时才会从新创建竞技场。它总是试图从先前划分的区块和池中分配内存。

这就是为什么usable_arenas ,以递减的方式排序nfreepools 。下一次对内存的请求将从分配最多的竞技场分配。这样,如果包含的对象被删除,数据最少的竞技场就会变成空闲,这些竞技场占用的内存可以释放给操作系统。

Python中的垃圾收集

垃圾收集被定义为当程序不再需要分配的内存时,对其进行回收或释放的过程。

垃圾收集(GC)是一种自动内存管理的形式。垃圾收集器试图回收由程序分配的、但不再被引用的内存--也称为垃圾。-维基百科

在像C语言中,程序员必须手动释放未使用对象的内存(未使用对象的垃圾收集),而Python中的垃圾收集是由语言本身自动处理的。

Python使用两种方法进行自动垃圾收集:

  1. 基于引用计数的垃圾收集。
  2. 生成式垃圾收集。

让我们首先解释一下什么是引用计数,然后我们将了解更多关于基于引用计数的垃圾收集!

引用计数

正如我们前面看到的,CPython在内部为每个对象创建了属性typeref count

让我们考虑一个例子来更好地理解ref count 这个属性。

a = "memory"

当上述代码被执行时,CPython创建了一个类型为string 的对象memory ,如下图所示。

String object pointed by variable a

字段ref count 表示对该对象的引用数量。我们知道,在Python中,变量只是对对象的引用。在上面的例子中,变量a 是对字符串对象memory 的唯一引用。因此,字符串对象memoryref count 值是1

我们可以使用getrefcount 方法得到Python中任何对象的引用计数。

让我们得到字符串对象的引用计数memory

import sys

ref_count = sys.getrefcount(a)

print(ref_count)   # Output: 2

上述代码的输出是2 。这表明字符串对象memory 被两个变量所引用。然而,我们在前面看到,memory 对象只被变量a 引用。

为什么在使用getrefcount 方法时,字符串对象memory 的引用计数值是2

为了理解这个问题,让我们考虑一下getrefcount 方法的定义。

def getrefcount(var):
    ...

注意:上面的getrefcount 定义只是为了解释。

这里,当我们把变量a 传递给方法getrefcountmemory 对象也被getrefcount 方法的参数var 所引用。因此,memory 对象的引用次数是2

String object pointed by variables a and var

因此,每当我们使用getrefcount 方法来获取一个对象的引用计数时,引用计数总是比该对象的实际引用计数多1。

让我们创建另一个变量,b ,它指向同一个字符串对象memory

b = a

Variables a, b, and var pointing to a string object

变量ab 都指向字符串对象memory 。因此,字符串对象memory 的引用计数将是2,而getrefcount 方法的输出将是3。

import sys

ref_count = getrefcount(a)

print(ref_count) # Output: 3

减少引用次数

为了减少引用次数,我们必须删除对变量的引用。这里有一个例子。

b = None

变量b 不再指向字符串对象memory 。因此,字符串对象memory 的引用计数也将减少。

import sys

ref_count = getrefcount(a)

print(ref_count) # Output: 2

使用del 关键字减少引用计数

我们也可以使用del 关键字来减少一个对象的引用次数。

如果我们将None 赋值给变量b (b = None),b 不再指向字符串对象memorydel 关键字的作用与此相同,它被用来删除对象的引用,从而减少其引用计数。

请注意,del 关键字不会删除该对象。

考虑一下下面的例子:

del b

String object pointed to by variables a and var

这里,我们只是删除了b 的引用。字符串对象memory 没有被删除。

现在让我们得到字符串对象memory 的引用计数。

import sys

ref_count = getrefcount(b)

print(ref_count) # Output: 2

很好!b 的引用计数现在是2。

让我们回顾一下我们学到的关于引用计数的知识!

如果我们把同一个对象分配给一个新的变量,那么该对象的引用次数就会增加。当我们通过使用del 关键字或使其指向None 来取消引用对象时,该对象的引用计数就会减少。

现在我们对Python中引用计数的概念有了更好的理解,让我们来学习垃圾收集是如何在引用计数的基础上工作的。

基于引用计数的垃圾回收

基于引用计数的垃圾收集使用对象的引用计数来释放或回收内存。当对象的引用计数为零时,Python的垃圾收集启动并从内存中删除该对象。

当一个对象从内存中被删除时,可能会引发其他对象的删除。

考虑一下下面的例子。

import sys

x = "garbage"
b = [x, 20]

ref_count_x = getrefcount(x)
print(ref_count_x) # Output: 3

ref_count_b = getrefcount(b)
print(ref_count_b) # Output: 1

忽略由于getrefcount 方法导致的引用计数的增加,字符串对象garbage 的引用计数是 2 (被变量x 引用和从列表b 引用),而数组对象的引用计数是1

接下来,让我们删除变量x

del x

由于数组对象[x, 20] ,字符串对象garbage 的引用计数为1。

我们仍然可以在变量b 的帮助下访问字符串对象garbage

b[0]

# Output: garbage

让我们删除变量b

del b

一旦上面的代码被执行,数组对象的引用计数就变成了0,垃圾收集器将删除数组对象。

删除数组对象[x, 20] ,也将删除数组对象中字符串对象x 的引用garbage 。这将使garbage 对象的引用计数为0,因此,garbage 对象也将被收集。因此,一个对象的垃圾收集也可能触发该对象所引用的对象的垃圾收集。

基于引用计数的垃圾收集是实时的!

一旦对象的引用计数变为0,垃圾收集就会被触发。这是Python的主要垃圾收集算法,因此,它不能被禁用。

如果存在循环引用,基于引用计数的垃圾收集就不起作用。为了释放或回收有循环引用的对象的内存,Python 使用了一种生成性垃圾收集算法。

接下来,我们将讨论循环引用,然后深入了解世代垃圾收集算法的更多内容

Python 中的循环引用

循环引用或循环引用是指一个对象引用自己或两个不同的对象相互引用的情况。

循环引用只适用于容器对象,如listdict 和用户定义的对象。对于不可变的数据类型,如整数浮点数字符串,它是不可能的。

考虑一下下面的例子。

import sys

b = list()

ref_count = sys.getrefcount(b)  # Output: 2

b.append(b)

ref_count = sys.getrefcount(b)  # Output: 3

从上面的表述中可以看出,数组对象b 正在引用自己。

让我们删除引用b

del b

Circular reference delete reference var b

一旦我们删除引用b ,这个数组对象将不能从 Python 代码中访问,但是它将继续存在于内存中,如上图所示。

由于数组对象在引用自己,数组对象的引用计数永远不会变成0,它也不会被引用计数垃圾收集器收集。

让我们考虑另一个例子。


class ClassA:
    pass

class ClassB:
    pass


A = ClassA()
B = ClassB()

# Reference b from a
A.ref_b = B

# Refer a from b
B.ref_a = A

Circular reference example with two objects

在这里,我们定义了两个对象,object_aobject_b ,分别属于ClassAClassB 类。object_a 是由变量A 来引用的,object_b 是由变量B 来引用。

让我们删除变量AB

del A
del B

一旦我们删除了变量ABobject_aobject_b 将不能从 Python 代码库中访问,但它们将继续存在于内存中。

这些对象的引用计数永远不会变成零,因为它们互相指向对方(通过属性ref_aref_b )。因此,这些对象将永远不会被引用计数垃圾收集器收集。

由于具有循环引用的对象的引用计数永远不会变成0,引用计数垃圾收集方法永远不会清理这些对象。对于这种情况,Python提供了另一种垃圾收集算法,叫做生成式垃圾收集

Python中的生成式垃圾收集

生成式垃圾收集算法解决了有循环引用的对象的垃圾收集问题。由于循环引用只有在容器对象中才可能发生,它扫描所有的容器对象,检测具有循环引用的对象,如果它们可以被垃圾收集,就将它们删除。

为了减少扫描对象的数量,代际垃圾收集还忽略了只包含不可变类型(如int和strings)的图元。

由于扫描对象和检测周期是一个耗时的过程,生成式垃圾收集算法不是实时的。当生成垃圾收集算法被触发时,其他一切都会停止。因此,为了减少垃圾收集算法被触发的次数,CPython将容器对象分为多代,并为每代定义了阈值。如果某一代的对象的数量超过了定义的阈值,就会触发一代的垃圾收集。

CPython将对象分为三代(0、1、2代)。当一个新对象被创建时,它属于第一代。如果这个对象在CPython运行代际垃圾收集算法时没有被收集,它就会被移到第二代。如果这个对象在CPython再次运行代际垃圾收集算法时没有被收集,它就会被移到第三代。这是最后一代,该对象将留在那里。

一般来说,大多数对象会在第一代被收集。

当某一代的垃圾收集被触发时,它也会收集所有年轻的一代。例如,如果第1代的垃圾收集被触发,它也会收集存在于第0代的对象。

所有三代(0、1、2)的垃圾都被收集的情况被称为完全收集。由于full 收集涉及扫描和检测大量对象的周期,CPython尽量避免full 收集。

我们可以使用Python提供的gc 模块来改变生成式垃圾收集器的行为。可以使用gc 模块来禁用生成性垃圾收集。因此,它也被称为可选的垃圾收集。

接下来,我们将解释gc 模块的一些重要方法。

gc Python中的模块

我们可以使用下面的方法来获得为每一代定义的阈值。

import gc
gc.get_threshold()

# Output: (700, 10, 10)

上面的输出表明,第一代的阈值是700,而第二代和第三代的阈值是10

如果第一代中的对象超过700个,那么将对第一代中的所有对象触发代际垃圾收集。如果第二代中的对象超过10个,那么将对第1代和第0代都触发垃圾回收。

我们还可以检查每一代中的对象数量,如下图所示。

import gc
gc.get_count()

# Output: (679, 8, 0)   # Note: Output will be different on different machines

这里,第一代的对象数量是679,而第二代的对象数量是8,第三代的对象数量是0。

我们也可以手动运行代际垃圾收集算法,如下图所示。

import gc
gc.collect()

gc.collect() 将触发代际垃圾收集。默认情况下,它运行一个完整的收集。为了运行第一代的垃圾收集,我们可以调用这个方法,如下图所示。

import gc
gc.collect(generation=1)

让我们检查一下收集过程后仍然活着的对象的数量。

import gc

gc.get_count()

# Output: (4, 0, 0)   # Note: Output will be different on different machines

我们还可以更新每一代的代际阈值,如下图所示。

import gc
gc.get_threshold()

gc.set_threshold(800, 12, 12)

gc.get_threshold()

# Output: (800, 12, 12)   # Note: Output will be different on different machines

我们可以用下面的命令来禁用世代垃圾收集。

gc.disable()

反之,我们可以用下面的命令再次启用它。

gc.enable()

总结

在这篇文章中,我们了解到Python是一套规则和规范,而CPython是Python在C语言中的参考实现。我们还了解了各种内存分配器,例如 Object AllocatorObject-specific AllocatorRaw memory AllocatorGeneral purpose Allocator,Python使用它们来有效管理内存。然后我们了解了Python内存管理器用来优化程序中小对象(大小小于或等于512字节)的内存分配/取消分配的Arenas、Pools和Blocks。我们还学习了垃圾收集算法,如引用计数和生成垃圾收集。