目录
- 内存就像是一本空白的书
- 内存管理:从硬件到软件
- 默认的Python实现(CPython)
- 全局解释器锁 (GIL)
- 垃圾收集
- CPython的内存管理
- Pools
- Blocks
- Arenas
- 结论
前言:
- 你是否曾经想弄明白
Python
是如何在幕后处理你的数据的?你的变量在内存中是如何存储的?这些变量什么时候会被删除? - 这篇文章中,我们将深入
Python
内部,了解Python
是如何进行内存管理。 - 读完这篇文章以后,你将:
- 学习到更低层次的计算机运作原理,例如:内存
- 了解
Python
如何抽象更低层次的操作 - 学习到
Python
内部的内存管理算法
- 理解
Python
内部的一些运行机制,将帮助你更好的理解Python
的某些行为的内部原理是什么。
内存就像是一本空白的书
你可以想象,内存就像是一本空白的书(内存),书上的所有页(page
)还没有写任何东西。但是最终,会有不同的作者(进程)在书上写自己的文章。
作者(进程)之间都只能在自己的页(page
)写文章,不能越界到其他页(page
),所以作者(进程)需要知道自己在哪一页(page
)写文章。在写之前,作者(进程)需要询问书(内存)的管理者。让管理者告诉自己可以在书的哪一页(page
)写文章。
等到过一段时间,书上已经写了很多内容了,当没有人读或者引用一些文章的时候,我们会擦掉这些内容,腾出空间来写新的文章。
书和内存都是使用固定长度并且相邻的页来操作的。所以这个比喻非常恰当。
作者就是不同的应用程序进程,它们需要在内存中存储数据;管理者就是一个内存管理器;擦掉旧文章的人就是垃圾收集器。
内存管理:从硬件到软件
内存管理其实就是管理应用程序读写数据的整个过程。内存管理器决定应用程序的数据往哪放。因为内存是有限的,就像书的页数是有限的,所以内存管理器必须找到足够的内存空间提供给应用程序。提供内存给应用程序的过程一般叫做memory allocation
。
另一方面,当内存中的某个数据不再需要,它会被删除或者释放。但是这块内存来自哪里?它将被释放到哪?
当你运行Python
程序的时候,计算机的某处物理设备上存储着你的数据。Python
中的对象在实际到达硬件之前,已经做了好几层的抽象。
在硬件(例如:RAM
或硬盘)层上面最主要的一层就是操作系统(operating system
)。操作系统执行(或者拒绝)你读写内存的请求。
操作系统的上层就是应用程序,例如CPython
(默认的Python
实现)。此时Python
代码的内存管理由Python
应用程序处理。Python
应用程序在内存管理中使用的算法和数据结构是本文的重点。
默认的Python实现(CPython)
默认的Python
实现(即CPython
),是由C
语言编写的。
比如你写了一段Python
代码,此时你还需要一些东西来解释并执行你的Python
代码。默认的Python
实现(CPython
)就是来完成解释和执行动作的,然后将你的Python
代码转换成指令,最后在一个虚拟机中执行。
注意:
虚拟机就像是物理计算机一样,但是它们是用软件来实现的。通常虚拟机执行基本指令就和汇编指令类似。
Python
是一门解释型语言。实际上,你的Python
代码会被编译成计算机更容易读的字节码指令。当你执行你的代码时,这些指令会被Python
虚拟机解释执行。
你是否见过.py
文件或者__pycache__
文件夹?它们是会被Python
虚拟机解释的字节码文件。
需要注意的是,除了CPython
(默认Python
实现)还有其他实现方式。IronPython
编译后的代码运行在微软的公共语言运行时环境中(Microsoft’s Common Language Runtime
)。Jython
编译生成Java
字节码,然后运行在Java
虚拟机环境中。还有PyPy
,它很复杂,需要用一整片文章才能讲清楚,所以这里只是提一下。
我们这篇文章的目的是讲内存管理,所以我们将着重讲默认Python
实现(CPython
)的内存管理,其他的实现有兴趣的读者可以自行深入了解。
声明:
本片文章使用的Python
版本是Python 3.7
。一般情况下Python
版本都是向后兼容的,但是还会有一些东西会被修改,这些差异读者可以忽略。
到这里我们已经很清楚:CPython
是用C
语言实现的,它用来解释Python
字节码。但是这与内存管理有什么关系呢?其实,内存管理的算法和数据结构都是在CPython
中用C
语言实现的。要理解Python
的内存管理,你需要先对CPython
有基础的了解。
众所周知,C
语言本身是不支持面向对象编程的。所以,在CPython
的代码中,需要进行一些特殊的设计才能使Python
达到面向对象编程的效果。
你可能听说过:在Python
中所有的东西都是对象,甚至包括类型(例如,int
、str
等)。是的,在CPython
的代码中确实是这样实现的。在CPython
中有一个struct
结构叫做PyObject
,CPython
中的所有对象都使用这个struct
结构。
注意:
在C
语言中,struct
是一个自定义数据类型,它将不同数据类型组织在一起。与面向对象语言相比,它就像是一个只有属性没有方法的类。
Python
中的所有对象都包含了PyObject
结构,这个结构里边只有两个域:
-
ob_refcnt
: 引用计数 -
ob_type
: 指向另一种类型的指针
引用计数(ob_refcnt
)用作垃圾收集( garbage collection
)。 指针(ob_type
)指向当前对象的类型。对象类型是另外一种结构,这种结构用来描述dict/int等Python
中的类型对象。
每一个对象都有特定的内存分配器,这个内存分配器知道如何获取内存来存储字节的对象。除此之外,每一个对象也都有特定的内存释放器,它会释放不再需要的内存。
然鹅,在我们讨论的内存申请与释放中有一个重要的因素:内存是计算机上的共享资源,当两个不同的程序同一时间尝试去写同一块内存时,就会有大问题出现。
全局解释器锁 (GIL)
GIL
是解决共享资源(例如计算机上的内存等)分配的一种解决方案。可以想象,当两个线程同一时间尝试去修改相同的资源,最后的结果是不可预期的,它们随机的执行导致最后两个线程得到的结果都不是自己想要的。
类比之前书的例子,假设两个作者都认为该自己写文章了,不仅如此,他们还需要相同的时间在相同的页上内容。
两个作者之间都忽略彼此的动作,都开始自顾自的写着文章。最后的结果就是两篇文章都叠在了一起,导致这个页都无法阅读了。
这个问题的一个解决方案就是当一个线程准备访问共享资源(书中的页)的时候,用一个独立的全局的锁将解释器锁住。换句话说,就是同一时间只能有一个作者写内容。
Python
的GIL
通过锁住整个解释器来实现,意思就是说两个线程不可能同一时间被执行。当CPython
处理内存的时候,它使用GIL
来确保线程安全。
这种方法有利有弊,GIL
在Python
社区引发了激烈的讨论。想知道更多关于GIL
的内容,可以参考:What is the Python Global Interpreter Lock (GIL)?
垃圾收集
我们还是参考前面提到的书的例子,假设现在书中的一些文章已经很旧很老了。已经没有人再读或者引用这些文章了,如果在你当前的工作中没有人读或者引用这些文章了,你需要将这些文章擦掉,腾出空间写其他东西。
旧的、没有被引用的文章就是Python
中引用计数降为0
的对象(记住,每一个Python
对象都有一个引用计数和一个指向类型的指针)。
引用计数增加的情况:
- 对象被赋值给另一个变量
numbers = [1, 2, 3]
# Reference count = 1
more_numbers = numbers
# Reference count = 2
- 将一个对象作为参数传递
total = sum(numbers)
- 将一个对象包含在一个列表中
matrix = [numbers, numbers, numbers]
Python
可以通过sys
模块的sys.getrefcount()
方法获取到对象的引用计数值。但是记住,在你传入对象的时候,传入对象的引用计数会被加1
.
在任何情况下,如果一个对象仍需要被保留在你的代码中,那么它的引用计数必须大于0
。因为一旦引用计数降到0
,对象特定的释放内存的方法就会被调用,这块内存就会被释放,提供给其他对象使用。
值得思考的是,这里说的释放内存具体是什么意思?另外,其他对象是如何使用释放的这块内存?这个会在下一节讲到!
CPython的内存管理
本节我们将深入到Python
内部,解读CPython
的内存体系结构和算法。
根据之前提到的,从物理硬件到CPython
抽象了很多层。操作系统抽象了物理内存条,创建了一个虚拟内存层,这个虚拟内存层可以让应用程序(包含Python
)直接访问。
操作系统特定的虚拟内存管理器会分配一大块内存给Python
进程。下图中深灰色的块就是属于Python
进程的。

Python
划分出内存的一部分作为内部使用的内存和非对象(non-object
)使用的内存。另一部专门用作对象的存储(int
、dict
等对象)。需要注意的是,这张图画的比较简单,更容易理解整体框架,如果你想了解更细节的东西,可以自行研究CPython源码。
CPython
有一个对象分配器,它负责在对象内存区域(object memory area
)分配内存。大部分“魔法”都发生在这个对象内存分配器中。每次一个新对象需要申请或者删除空间的时候,它都会被调用。
通常情况下,Python
中的对象(例如list
/int
等),数据的增加和删除一次不会包含很多的数据量。所以,内存分配器是针对一次申请小数量的数据而设计的。
源码中的注释是这样说的:a fast, special-purpose memory allocator for small blocks, to be used on top of a general-purpose malloc
。在本文中,malloc
是C
语言库中的一个函数,用来分配内存的。
现在我们已经知道了CPython
的内存分配策略。然后我们会介绍内存管理中的3
大块东西和它们之间是如何联系的?
arena
是最大的内存块,它在内存中的页面边界(page boundary
)对齐。页面边界就是操作系统使用的固定长度并且连续的内存块的边缘。Python
中,我们假设系统的页大小(page size
)是256KB
。

arenas
中包含的是pools
,pool
是一个虚拟内存页(virtual memory page
),大小为4KB
。它就像是我们之前讲的书中的页(pages
)一样。pools
被分隔成更小的内存块(blocks
)。
一个给定的pool
中的所有blocks
都具有相同的“size class
”。“size class
”表示一个特定的块大小,返回请求的数据量(比如请求7
字节大小内存,它会返回8
字节大小的内存)。下图展示了“size class
”和请求字节大小的关系图:
Request in byte | Size of allocated block | Size class 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 |
Pools
pools
是由单一“size class
”的blocks
组成的。每一个pool
维护一个双向链表,用于连接其他相同“size class
”的pools
。以这种方式,算法可以很容易的找到给定大小的可用空间,这些可用的空间是可以跨pools
的。
usedpools
列表跟踪所有有可用空间的pools
,包含每一个“size class
”。当申请一个固定block size
时,算法会在usedpools
列表中找那个block size
对应的pools
的列表。
pools
自身有三种状态:
-
used
:有可用的blocks
-
full
:没有可用的blocks
-
empty
:没有存储任何数据,可以被指定为任何任意的size class
freepools
列表存储所有状态是empty
的pools
。但是,什么时候empty
的pool
会被使用?
假设你需要一个8字节大小的内存块。如果usedpools中没有8字节”size class
“的pool
,一个新的empty
状态的pool
会被初始化为存储8字节大小的blocks
。这个新的pool
会被添加到usedpools
列表中,以备后用。
假如一个full状态的pool
释放了一些blocks
,那么这个pool
会被重新加到usedpools
列表对应的“size class
”中。
上面可以看到,pools
是如何通过这个算法在三种状态之间自由转换的。
Blocks

如上图所示,pools
包含一个指向pool
中没有使用(free block
)的blocks
内存的指针。这里有一个细微的差别,根据源代码的注释可以知道,内存分配器申请完内存以后不会立即使用这块内存,直到真正需要使用这块内存的时候才会使用。
这意味着,一个pool
中可以有3
种状态的blocks
:
-
untouched
:未被分配的内存 -
free
:被分配的内存,但是即将被CPython
释放,内存中不再包含有关的数据 -
allocated
:被分配的内存,并且包含了相关的数据
freeblock
指针指向一个包含free
状态的blocks
的单链表。换句话说,可以存放数据的可用空间列表。如果需要的内存大于可用的free blocks
时,内存分配器会将pool
中的一些untouched blocks
加入到单链表中。
当内存管理器使block
变成free
状态时,它需要将这个free blocks
添加到freeblock
列表的最前面。实际情况下freeblock
列表一般不会是连续的,一般看起来会像下图所示的这样:

Arenas
arenas
包含pools
,这些pools
可以是used
,full
,empty
状态。arenas
自身没有像pools
那样明确的状态。
arenas
被一个双向链表连接起来,称为usable_arenas
。这个列表根据可用的free pools
的数量排序,free pools
越少的arena
将排在链表的前面。

这就意味着,将会从数据最多的arena
中选择出空间来存放新数据。思考一下,为什么不选择数据量最少的arena
来存放新数据?
其实,Python
释放内存时,没有将内存空间释放给操作系统,而是自己维护起来,随后会被其他Python
对象重新申请。
arenas
是唯一可以被真正释放给操作系统的对象,所以,Python
会尽可能的保持arena
为空,尽量先使用数据量多的arena
。这样,某块为空的arena
就可以真正被释放给操作系统,减少Python
程序的总体内存占用。
结论
内存管理是计算机工作中不可缺少的一部分。Python
在幕后处理了所有的事情,不管好事还是坏事。
这篇文章中,你学到了:
- 内存管理是什么?它为什么那么重要?
-
CPython
是如何使用C
语言实现的? - 处理数据的时候,
CPython
的内存管理是如何将数据结构和算法结合在一起的?
Python
抽象了很多计算机工作的细节。这使你工作在一个更高的层次,不用去关心数据存储在哪里,是如何存储的。
欢迎关注微信公众号(coder0x00)或扫描下方二维码关注,我们将持续搜寻程序员必备基础技能包提供给大家。
网友评论