大家好! 这样漫长的三月周末结束了。 我们希望将假日后的第一本书献给许多课程所钟爱的
“ Python开发人员” ,该课程在不到2周的时间里开始。 走吧
目录内容- 记忆是一本空书
- 内存管理:从硬件到软件
- Python基本实现
- 全球口译员锁(GIL)概念
- 垃圾收集器
- CPython中的内存管理:
- 结论

您是否想过Python后台如何处理您的数据? 您的变量如何存储在内存中? 在什么时候将它们移走?
在本文中,我们将更深入地研究Python的内部结构,以了解内存管理的工作原理。
阅读本文后,您将:
- 了解有关低级操作(尤其是内存)的更多信息。
- 了解Python如何抽象底层操作。
- 了解Python中的内存管理算法。
了解Python的内部结构将更好地理解其行为原理。 希望您可以从一个新的角度看待Python。 在幕后,有太多逻辑操作使程序正常运行。
记忆是一本空书您可以将计算机的内存想象成一本空的书,等待它写很多简短的故事。 它的页面上还没有任何内容,但是很快就会出现想要在其中写故事的作者。 为此,他们将需要一个位置。
由于他们不能在一个故事上写一个故事,因此他们在写页面时要非常小心。 在您开始写作之前,他们会咨询图书管理员。 经理决定作者可以在书中的何处写下他们的故事。
由于这本书已经存在多年了,所以其中的许多故事已经过时了。 当没有人阅读或讲述历史时,他们会将其删除以为新故事腾出空间。
从本质上讲,计算机内存就像一本空书。 固定长度的连续内存块通常称为页面,因此这种类比派上用场了。
作者可以是需要在内存中存储数据的各种应用程序或过程。 决定作者可以在哪里写故事的经理扮演着内存管理器的角色-排序器。 那些抹掉旧故事的人就是垃圾收集器。
内存管理:从硬件到软件内存管理是软件应用程序读取和写入数据的过程。 内存管理器确定将程序数据放在何处。 当然,由于内存量与书中的页数一样,因此,管理者需要找到可用空间以供应用程序使用。 此过程称为“内存分配”。
另一方面,当不再需要数据时,可以将其删除。 在这种情况下,他们谈论释放内存。 但是,它释放了什么,它从何而来?
当您运行Python程序时,在计算机内部的某个位置存在一个物理设备来存储数据。 在进入此设备之前,Python代码会经历许多抽象级别。
操作系统(位于设备,RAM,硬盘等上方)的主要层次之一。 它管理对内存的读写请求。
在操作系统上方,有一个应用程序层,在该层上有一个Python实现(连接到您的OS或从python.org下载)。 此编程语言中的代码的内存管理由特殊的Python工具进行管理。 Python用来管理内存的算法和结构是本文的主题。
Python基本实现Python或“纯Python”的基本实现是用C编写的CPython。
当我第一次听说这件事时,我感到非常惊讶。 一种语言如何用另一种语言书写? 好吧,当然不是字面上的意思,但是这个主意是这样的。
在
特殊的英语参考手册中描述了Python语言。 但是,仅此指南不是很有用。 您仍然需要一个工具来解释由目录规则编写的代码。
您还需要一些东西来在计算机上执行代码。 基本的Python实现提供了两种条件。 它将Python代码转换为在虚拟机中执行的指令。
注意:虚拟机类似于物理计算机,但是它们嵌入在软件中。 它们处理类似于汇编代码的基本指令。
Python是一种解释型编程语言。 您的Python代码是使用计算机更容易理解的指令(
字节码)编译的 。 当您运行代码时,虚拟机将解释这些指令。
您是否看过扩展名为
.pyc或
__pycache__文件夹的文件? 这是虚拟机解释的相同字节码。
重要的是要了解,除了CPython之外,还有其他实现,例如
IronPython ,它可以在Microsoft公共语言运行时(CLR)中编译和运行。
Jython编译为Java字节码以在Java虚拟机中运行。 还有一个关于
PyPy的文章,您可以撰写一篇单独的文章,因此,我只会顺便提及一下。
在本文中,我们将重点介绍使用CPython工具进行内存管理。
注意:Python版本已更新,以后可能会发生任何事情。 在撰写本文时,最新版本是
Python 3.7 。
好的,我们已经用C语言编写了CPython来解释Python字节码。 这与内存管理有什么关系? 首先,C语言中存在C语言中用于管理内存的算法和结构。要了解Python中的这些原理,您需要对CPython有基本的了解。
CPython是用C编写的,而C不支持面向对象的编程。 因此,CPython代码具有相当有趣的结构。
您一定已经听说Python中的所有内容都是一个对象,例如,甚至是int和str之类的类型。 在CPython实现级别上确实如此。 CPython中的每个对象都有一个称为PyObject的结构。
注意:C语言中的结构是用户定义的数据类型,它本身将不同类型的数据分组。 我们可以用面向对象的语言进行类比,并说结构是具有属性但没有方法的类。
PyObject是Python中所有对象的祖先,仅包含两件事:
- ob_refcnt :参考计数器;
- ob_type :指向另一种类型的指针。
垃圾收集需要一个参考计数器。 我们还有一个指向特定对象类型的指针。 对象类型只是描述Python中对象(例如dict或int)的另一种结构。
每个对象都有一个面向对象的内存分配器,它知道如何分配内存和存储对象。 每个对象还具有面向对象的资源释放器,如果不再需要其内容,则该清理器将清理内存。
谈论内存分配及其清理有一个重要因素。 内存是计算机的共享资源,如果两个进程试图同时将数据写入同一内存位置,则可能会发生不愉快的事情。
全局解释锁(GIL)GIL是解决共享资源(例如计算机内存)之间共享内存这一普遍问题的解决方案。 当两个线程试图同时更改同一资源时,它们会互相踩脚。 结果,在存储器中形成了一个完整的混乱,没有任何进程可以完成其工作并获得所需的结果。
回到这本书的类比,假设两位作者各自决定在这个特定时刻在当前页面上写他的故事。 他们每个人都忽略了对方写故事的企图,并开始顽固地写在页面上。 结果,我们有两个故事,一个故事在另一个故事之上,以及一个绝对不可读的页面。
解决这个问题的方法之一就是GIL,它在线程与分配的资源交互时阻止解释器,从而允许一个线程和只有一个线程写入分配的内存区域。 当CPython分配内存时,它使用GIL来确保正确执行。
这种方法既有很多优点,也有很多缺点,这就是为什么GIL在Python社区引起冲突的原因。 要了解有关GIL的更多信息,建议阅读以下
文章 。
垃圾收集器让我们回到对这本书的类比,并想象其中的一些故事已经过时了。 没有人阅读并解决它们。 在这种情况下,自然的解决方案是将它们不必要地去除,从而为新故事腾出空间。
可以将此类未使用的旧故事与Python中引用计数已降至0的对象进行比较。请记住,Python中的每个对象都具有引用计数和指向类型的指针。
参考计数可能由于多种原因而增加。 例如,如果将一个变量分配给另一个变量,它将增加。

如果将对象作为参数传递,它也会增加。

在最后一个示例中,如果将对象包括在列表中,则引用计数将增加。

Python使用sys模块让您知道引用计数器的当前值。 您可以使用
sys.getrefcount(numbers)
,但是请记住,调用
getrefcount()
会使引用计数增加另一个。
在任何情况下,如果仍然需要代码中的对象,则其引用计数器的值将大于0。当其降至零时,将启动特殊功能以清除内存,从而释放该内存并将其用于其他对象。
但是“释放内存”是什么意思,其他对象如何使用它? 让我们直接研究CPython中的内存管理。
CPython中的内存管理在这一部分中,我们将深入探讨CPython内存体系结构及其运行算法。
如前所述,物理设备和CPython之间存在抽象层次。 操作系统(OS)提取物理内存,并创建包括Python在内的应用程序可以访问的虚拟内存级别。
面向OS的虚拟内存管理器为Python进程分配了特定的内存区域。 在图中,深灰色区域是Python进程占用的空间。

Python使用部分内存供内部使用,并使用非对象内存。 另一部分分为对象的存储(您的
int,dict等)。我现在用一种非常简单的语言讲,但是您可以直接在幕后看,也就是
CPython的
源代码,并从实际的角度看这一切是如何发生的。 。
在CPython中,有一个对象分配器负责在对象内存区域内分配内存。 正是在这种物体的分配器中,所有的魔法都得以执行。 每当每个新对象需要占用或释放内存时,都会调用该函数。
通常,在Python中添加和删除数据(例如int或list)一次不会使用大量数据。 这就是分配器的体系结构专注于每单位时间处理少量数据的原因。 而且,他不会提前分配内存,即直到那一刻直到绝对必要为止。
源代码中的注释将分配器定义为“像通用malloc函数一样工作的专用快速内存分配器”。 因此,在C语言中,malloc用于分配内存。
现在让我们看一下CPython的内存分配策略。 首先,让我们谈谈三个主要部分以及它们之间的关系。
Arenas是最大的内存区域,占用的空间一直到内存中页面的边界。 页面边界(页面扩展)是操作系统使用的固定长度的连续内存块的端点。 Python将系统页面边框设置为256 KB。

竞技场内部是池(池),它们被视为一个虚拟内存页面(4 Kb)。 在我们的类比中,它们看起来像页面。 池被划分为更小的内存块。
池中的所有块都位于一个“大小类”中。 size类确定具有一定数量的请求数据的块的大小。 下表中的等级直接取自源代码中的注释:

例如,如果需要42个字节,则数据将放置在48个字节的块中。
泳池池由相同大小级别的块组成。 每个池都基于与相同大小类的其他池的双向链表的原理。 因此,即使在许多池中,该算法也可以轻松找到所需块大小的必要位置。
usedpools list
会跟踪所有具有某种可用空间的池,这些可用空间可用于每个大小类的数据。 当请求所需的块大小时,该算法将检查已用池的列表,以找到合适的池。
池处于三种状态:已使用,已满,为空。 使用的池包含可以在其中写入一些信息的块。 完整池的所有块均已分发,并且已经包含数据。 空池不包含任何数据,可以根据需要细分为适合的大小级别。
空池
freepools list
(
freepools list
)分别包含所有处于空状态的池。 但是在什么时候使用它们?
假设您的代码需要8个字节的存储区。 如果已用池列表中没有类大小为8字节的池,则将初始化一个新的空池以存储8字节的块。 然后将空池添加到已用池列表中,并可以在以下查询中使用。
当不再需要这些块中的信息时,完整的池将释放一些块。 该池将根据其大小级别添加到使用的列表中。 您可以观察池如何根据算法更改其状态甚至大小类。
积木
从图中可以看出,这些池包含指向空闲内存块的指针。 他们的工作有些细微差别。 根据源代码中的注释,分发服务器“力争在需要之前不要接触任何级别(区域,池,块)的任何内存区域”。
这意味着一个块可以具有三种状态。 它们可以定义如下:
- 未触及 :尚未分配的内存区域;
- 空闲 :已分配但后来由CPython释放的内存区域,因为它们不包含相关信息;
- 分布式 :当前包含当前信息的内存区域。
freeblock指针是空闲存储块的单链接列表。 换句话说,这是您可以在其中写信息的自由场所的列表。 如果需要的内存多于可用块中的内存,则分配器将使用池中未触及的块。
内存管理器释放块后,这些块将立即添加到空闲块列表的顶部。 实际列表可能不包含连续的存储块序列,如第一个“成功”图中所示。
竞技场竞技场内有游泳池。 与池不同,Arenas没有明确的州划分。
它们本身被组织成一个双向链接列表,称为可用竞技场列表(usable_arenas)。 该列表按空闲池数排序。 可用池越少,竞技场就越靠近列表的顶部。

这意味着将选择最完整的竞技场来记录更多数据。 但是为什么呢? 为什么不将数据写入最大可用空间呢?
这使我们想到了完全释放内存的想法。 事实是,在某些情况下,释放内存后,操作系统仍然无法访问。 Python进程将其保持分发状态,并在以后用于新数据。 完全内存重新分配会将内存返回给操作系统。
竞技场并不是唯一可以完全腾空的区域。 因此,我们知道应该释放“更接近空虚”列表中的那些区域。 在这种情况下,实际上可以完全释放内存区域,因此,Python程序的总内存容量会减少。
结论内存管理是使用计算机时最重要的部分之一。 Python以一种或另一种方式几乎以隐身模式执行其所有操作。
从本文中您了解到:
- 什么是内存管理,为什么重要?
- 什么是CPython,基本的Python实现;
- 数据结构和算法如何在CPython的内存管理中工作并存储数据。
Python提取了使用计算机的许多细微差别。 这样就可以进行更高级别的工作,而不必再为程序字节的存储位置和存储方式所困扰。
因此,我们了解了Python中的内存管理。 按照传统,我们会等待您的评论,我们还会邀请您参加将于3月13
日举行的Python开发人员课程的
开放日 。