您是否曾经想过,您使用的数据在Python的肠子里看起来如何? 关于如何创建变量并将其存储在内存中? 如何以及何时将它们移除? 该材料(我们将其翻译发行)专门用于研究Python的深度,在此期间,我们将尝试找出这种语言的内存管理功能。 学习本文之后,您将了解计算机的低级机制如何工作,尤其是与内存有关的机制。 您将了解Python如何抽象低级操作以及如何管理内存。

知道Python中发生了什么,将使您更好地理解该语言的某些行为。 我希望,这将使您有机会欣赏在使用所用语言的实现过程中正在进行的大量工作,从而使程序以所需的方式工作。
记忆是一本空书
在使用计算机存储器的一开始,它可以以一本用于短篇小说的空书的形式表示。 尽管页面上没有任何内容,但很快就会出现故事的作者,每个人都希望在本书中写下自己的故事。
由于一个故事不能写在另一个故事的顶部,因此作者需要注意写在哪本书上。 在编写任何内容之前,他们会咨询总编辑。 他决定作者可以在哪里确切记录故事。
由于我们正在谈论的书已经存在了一段时间,因此其中的许多故事已经过时了。 如果没有人阅读或在他们的作品中提及故事,则该故事将从书中删除,从而为新故事腾出空间。
通常,我们可以说计算机内存与此类书籍非常相似。 实际上,固定长度的连续内存块甚至被称为页面,因此我们认为将内存与书本进行比较是非常成功的。
将故事写在书中的作者是需要将数据存储在内存中的不同应用程序或过程。 决定作者可以在书的哪几页上写的主编是处理内存管理的机制。 可以从书中删除旧故事,为新故事腾出空间的人可以与垃圾收集机制进行比较。
内存管理:从铁到程序的方式
内存管理是一个过程,在此过程中,程序将数据写入内存并从中读取数据。 内存管理器是一个实体,它确定应用程序可以将其数据确切地放置在内存中的位置。 由于可以分配给应用程序的内存碎片的数量不是无限的,就像任何书中的页数不是无限的一样,服务于应用程序的内存管理器需要查找可用的内存碎片并将其提供给应用程序。 将内存分配给应用程序的过程称为内存分配。
另一方面,当不再需要某些数据时,可以将其删除,或者换句话说,释放它所占用的内存。 但是当谈到记忆时,它们究竟是“隔离”和“解放”了什么呢?
您计算机中某处有一个物理设备,用于存储Python程序运行时使用的数据。 在Python对象出现在物理内存中之前,代码必须经过许多抽象层。
位于硬件(例如RAM或硬盘)顶部的主要此类层之一是操作系统(OS)。 它执行(或拒绝执行)从内存中读取数据并将数据写入内存的请求。
在我们的例子中,操作系统的顶部是一个应用程序,是Python的一种实现(它可以是操作系统的一部分或从
python.org下载的
软件包 )。 正是该软件包用于内存管理,以确保Python代码的运行。 本文的重点是Python用于管理内存的算法和数据结构。
Python参考实现
参考Python实现称为CPython。 它是用C语言编写的。当我第一次听说它时,它确实使我感到不安。 用另一种语言编写的编程语言? 好吧,实际上,这并非完全正确。
Python规范在
本文档中以普通英语描述。 但是,仅凭此规范,用Python编写的代码当然无法执行。 为此,您需要遵循本规范的规则,才能解释用Python编写的代码。
此外,您还需要一些可以在计算机上执行解释后代码的工具。 参考Python实现解决了这两个任务。 它将代码转换为指令,然后在虚拟机上执行这些指令。
虚拟机类似于由硅,金属和其他材料制成的普通计算机,但是它们是通过软件实现的。 他们通常忙于处理基本指令,类似于在
Assembler中编写的指令。
Python是一种解释型语言。 用Python编写的代码以所谓的
字节码编译成一组便于计算机使用的
指令 。 在运行程序时,虚拟机将解释这些说明。
您是否看过扩展名为
.pyc
或
__pycache__
文件夹的文件? 它们包含由虚拟机解释的相同字节码。
请务必注意,除了CPython外,还有其他Python实现。 例如,当使用
IronPython时, Python代码被编译为Microsoft CLR语句。 在
Jython中,代码被编译为Java字节码并在Java虚拟机中执行。 在Python世界中,有诸如
PyPy之类的东西,但值得单独写一篇文章,因此在这里我们只提及它。
出于本文的目的,我将重点介绍内存管理机制如何在Python参考实现-CPython中工作。
应该注意的是,尽管我们在此要讨论的大多数内容对于新版本的Python都是正确的,但将来可能会发生变化。 因此,请注意以下事实:在本文中,我专注于撰写本文时的最新版本的Python-
3.7 。
因此,CPython软件包是用C编写的,它解释了Python字节码。 这与内存管理有什么关系? 事实是,用于内存管理的算法和数据结构存在于C语言代码中,正如已经提到的那样,它是用C语言编写的。为了了解内存管理在Python中的工作方式,您首先需要对CPython有所了解。
编写CPython的C语言没有对面向对象编程的内置支持。 因此,CPython代码中使用了许多有趣的体系结构解决方案。
您可能已经听说Python中的所有内容都是对象,甚至是
int
和
str
类的原始数据类型。 在CPython的语言实现级别上确实如此。 有一个称为
PyObject
的结构,供CPython中创建的对象使用。
结构是可以对不同类型的数据进行分组的复合数据类型。 如果将此与面向对象的编程进行比较,则其结构类似于具有属性但没有方法的类。
PyObject
是所有Python对象的祖先。 此结构仅包含两个字段:
ob_refcnt
参考计数器。ob_type
指向另一种类型的指针。
参考计数器用于实现垃圾收集机制。 另一个
PyObject
字段是指向特定对象类型的指针。 此类型由描述Python对象的另一种结构表示(例如,它可以是
dict
或
int
)。
每个对象都有其自己的,对于此类对象而言唯一的内存分配机制,该机制知道如何获取存储此对象所需的内存。 另外,每个对象都有其自己的释放内存的机制,该机制在不再需要内存时“释放”内存。
但是,应该注意的是,在所有这些有关内存分配和释放的对话中,都有一个重要因素。 事实是计算机内存是共享资源。 如果同时有两个不同的进程尝试将某些内容写入同一存储区,则可能会发生不良情况。
口译员全局锁定
全局解释器锁定(GIL)是使用共享计算机资源(如内存)时发生的常见问题的解决方案。 当两个线程尝试同时修改同一资源时,它们可以彼此“冲突”。 结果将是一团糟,没有一个流能够实现它所追求的目标。
让我们再次回到这本书的比喻。 想象一下,有两位作者任意决定,现在轮到他们做笔记了。 但是他们也决定在同一页面上同时做笔记。
他们每个人都没有注意另一个人正在尝试写他的故事的事实。 他们一起开始在页面上写文本。 结果,将在此处记录两个故事,一个故事在另一个故事的顶部,这将使该页面完全不可读。
解决此问题的方法之一是使用单个全局解释器机制来阻止某个线程正在使用的共享资源。 在我们的示例中,这是一种“机制”,“阻塞”了书的页面。 这种机制消除了上述情况,即两个作者同时在同一页面上写文本。
Python中的GIL机制通过阻止整个解释器来实现这一点。 结果,没有什么可以干扰当前线程的操作。 当CPython处理内存时,它使用GIL来确保安全有效地完成这项工作。
这种方法各有利弊,GIL是Python社区中激烈争论的主题。 要了解有关GIL的更多信息,您可以看一下
该材料 。
垃圾收集
让我们回到这本书的类比,并想象本书中记录的一些故事已经过时了。 没有人阅读它们,没人在任何地方提及它们。 如果没有人阅读或提及其作品中的某些材料,则可以丢弃该材料,为新文本腾出空间。
这些古老的,被遗忘的故事可以与引用计数为零的Python对象进行比较。 这些与我们讨论
PyObject
结构时所讨论的计数器相同。
链接计数器的增加有几个原因。 例如,如果存储在一个变量中的对象被写入另一个变量,则计数器增加:
numbers = [1, 2, 3]
当对象作为参数传递给某些函数时,它会增加:
total = sum(numbers)
这是参考计数器中数字增加的情况的另一个示例。 如果对象包含在列表中,则会发生这种情况:
matrix = [numbers, numbers, numbers]
Python允许程序员使用
sys
模块找出某个对象的引用计数的当前值。 为此,使用以下构造:
sys.getrefcount(numbers)
getfefcount()
它时,您需要记住,将对象传递给
getfefcount()
方法会使计数器值增加1。
无论如何,如果对象仍在代码中的某处使用,则其引用计数器将大于0。当计数器值降至0时,将发挥特殊功能,“释放”对象占用的内存。 然后,其他对象可以使用此内存。
现在,我们问自己有关什么是“释放内存”以及其他对象如何使用此内存的问题。 为了回答这些问题,让我们谈谈CPython中的内存管理机制。
CPython中的内存管理机制
现在,我们将讨论CPython如何具有内存架构以及如何在其中进行内存管理。
如前所述,CPython和物理内存之间有几层抽象。 操作系统提取物理内存并创建应用程序可以使用的虚拟内存层(这也适用于Python)。
特定操作系统的虚拟内存管理器为Python进程分配一块内存。 下图中的深灰色区域是属于Python进程的内存。
CPython使用的内存区域Python使用一定量的内存供内部使用,以及用于与为对象分配内存无关的需求。 另一块内存用于存储对象(这些是
int
,
dict
等类型的值)。 请注意,这是简化图。 如果您想看完整图,请看一下
CPython的源代码,我们正在谈论的一切都在这里发生。
CPython具有为对象分配内存的功能,该功能负责在旨在存储对象的区域中分配内存。 当这种机制起作用时,最有趣的事情发生了。 当对象需要内存时,或者在需要释放内存的情况下,将调用该方法。
通常,向Python对象(如
list
和
int
添加或删除数据不涉及同时处理大量信息。 因此,构建内存分配工具的架构时要着眼于处理少量数据。 此外,该工具在明确表明它绝对必要之前,将不分配内存。
源代码中的注释将内存分配工具描述为“一种用于小型块的快速专用内存分配工具,旨在用于通用malloc之上。” 在这种情况下,
malloc
是旨在分配内存的C库函数。
让我们讨论一下CPython使用的内存分配策略。 首先,我们将讨论三个实体-所谓的块(block),池(pool)和竞技场(arena)(竞技场),以及它们如何相互关联。
Arenas是最大的内存碎片。 它们在内存页面的边界上对齐。 页面边界是操作系统使用固定长度内存的连续块结束的位置。 Python在使用内存时,假设系统内存页面的大小为256 KB。
竞技场,泳池和街区池位于竞技场上,竞技场是4 KB虚拟内存页面。 它们与我们示例中的书页相似。 池分为小块内存。
同一池中的所有块都属于相同的大小类。 块所属的大小类决定了此块的大小,要在考虑请求的内存大小的情况下进行选择。 这是从源代码中获取的一张表,该表演示了系统请求在内存中存储的数据量,分配的块的大小以及大小类的标识符。
数据量(以字节为单位)
| 块大小
| idx类大小
|
1-8
| 8
| 0
|
9-16
| 16
| 1个
|
17-24
| 24
| 2
|
25-32
| 32
| 3
|
33-40
| 40
| 4
|
41-48
| 48
| 5
|
49-56
| 56
| 6
|
57-64
| 64
| 7
|
65-72
| 72
| 8
|
...
| ...
| ...
|
497-504
| 504
| 62
|
505-512
| 512
| 63
|
例如,如果请求存储42个字节,则数据将放置在48个字节的块中。
泳池
池由属于相同大小类的块组成。 使用双向链接列表机制,每个池与包含相同大小类块的其他池关联。 使用这种方法,即使要在不同的池中查找可用空间,内存分配算法也可以轻松找到给定大小的块的可用空间。
usedpools
列表使您可以跟踪其中有空间容纳属于特定大小类的数据的所有池。 当请求保存某个大小的块时,算法会在此列表中检查存储所需大小的块的池的列表。
池本身必须处于三种状态之一。 即,可以使用它们(使用状态),可以将它们填充(
full
)或空(
empty
)。 使用的池具有可用的块,可以在其中存储适当大小的数据。 填充池的所有块都分配有数据。 空池不包含任何数据,并且如有必要,可以将其分配为存储属于任何大小类的块。
freepools
池列表存储有关所有处于
empty
状态的池的信息。 例如,如果在
usedpools
列表中没有关于存储8字节块(idx 0的类)的池的条目,则将初始化一个新的池,该池处于
empty
状态,用于存储此类块。 这个新池被添加到
usedpools
列表中,它可以用来满足保存创建后接收到的数据的请求。
假设在处于
full
状态的池中,释放了一些块。 这是因为不再需要存储在其中的数据。 该池将再次出现在
usedpools
列表中,并且可用于相应大小级别的数据。
有了此算法,我们就可以了解池的状态在操作过程中如何变化(以及大小类如何变化,属于它们的块可以存储在其中)。
积木
已用,满和空池从上图中可以看到,池包含指向它们所包含的“空闲”内存块的指针。 关于使用块,应注意一个小功能,该功能在源代码中指出。 在CPython中,所有级别(区域,池,块)中使用的内存管理系统都仅在绝对必要时才努力分配内存。
这意味着池可以包含处于以下三种状态之一的块:
untouched
是尚未分配的内存部分。free
已分配的内存部分,但后来由CPython使其“空闲”,不再包含任何有价值的数据。allocated
是包含有价值数据的内存部分。
freeblock
指针指向空闲存储块的单链接列表。 换句话说,这是可以放置数据的位置的列表。 如果需要多个空闲块来放置数据,则内存分配工具将从未占用状态的池中获取几个块。
当内存管理工具使块变为“空闲”时,它们在获得
free
状态时将
freeblock
列表的顶部。 此列表中包含的块不一定代表类似于上图中所示的连续存储区域。 它们实际上可能看起来像下面的那个。
单链接的freeblock列表竞技场
竞技场内有游泳池。 如前所述,这些池可以驻留在
used
,
full
或
empty
状态。 应该注意的是,竞技场的状态与池状态不同。
Arenas被组织成一个名为
usable_arenas
的双向链接列表。 该列表按可用空闲池的数量排序。 竞技场中的免费池越少,竞技场就越靠近列表的顶部。
Usable_arenas列表这意味着将选择比填充数据的区域更强大的竞技场,以在其中放置新数据。 反之亦然? 为什么不以可用空间最大的方式在舞台上发布新数据?
实际上,此功能使我们想到了真正释放内存的想法。 您可能已经注意到,我们在这里经常使用“释放内存”的概念,将其用引号引起来。 这样做的原因是,尽管可以将该块视为“空闲”,但它所代表的内存实际上并没有返回给操作系统。 Python进程保留了这部分内存,以后使用它来存储新数据。 内存的真正释放是操作系统的返回,可以利用它。
Arenas是此处考虑的方案中的唯一实体,可以真正释放所代表的内存。 常识表明,上述与竞技场合作的方案旨在让几乎空着的那些竞技场完全清空。 通过这种方法,可以完全释放由完全空的竞技场表示的那部分内存,这将减少Python消耗的内存量。
总结
这是您通过阅读此材料中学到的:
- 什么是内存管理及其重要意义。
- 如何安排用C编程语言编写的Python Cpython的参考实现。
- CPython在内存管理中使用了哪些数据结构和算法。
内存管理是计算机程序工作不可或缺的一部分。 Python解决了程序员未注意到的几乎所有内存管理任务。 Python使使用这种语言编写的任何人都可以忽略与使用计算机有关的许多小细节。 这使程序员有机会在更高层次上工作,以创建自己的代码,而不必担心数据存储在何处。
亲爱的读者们! 如果您有Python开发经验,请告诉我们如何处理程序中的内存使用情况。 例如,您是否要保存它?
