< 返回博客

[翻译] Python的内存管理


有没有想过Python是如何在背后处理你的数据的?你的变量是如何存放在内存中的?他们什么时候被删除?

在这篇文章中,我们将深入研究Python的内部原理,来了解他是如何进行内存管理的。

通过这篇文章,你将会:

  • 学习更多关于底层计算的知识,尤其是内存相关;
  • 理解Python是如何抽象底层操作的;
  • 学习Python的内部内存管理算法。

理解Python的内部原理还可以让你更好地理解Python的一些行为,也希望你对Python能有更好地认识。在Python的背后发生了很多的事情,以确保你的程序能运行地与你预想的一致。

内存是一本空白的书

首先,你可以将计算机的内存当作一本用来写短篇小说的空白书本,但是开始时上面还没有任何内容,而最终会有不同的作者不断出现,每个作者都想要一些空间来写上他们的故事。

因为不允许覆写,所以每个人都需要注意自己写入的页。在开始写入自己的故事之前,他们会咨询这本书的管理员,管理员会决定他们可以往哪里写入。

由于这本书存在了很长时间,因此其中的许多故事都已经不再适用。当不再有人去读或者参考这些故事的时候,它们将会被移除,来为新的故事腾出空间。

本质上(In essence),计算机内存就像这样一本书。实际上,计算机通常会调用固定长度的连续内存页,(就像书页一样),所以这样的比喻很是恰当。

那些作者们就像是不同的应用和进程,它们需要空间去存储数据。那个决定哪里可以写入的管理员,就扮演了各种内存管理器的角色。而那个删除旧的故事,为新的故事腾出空间的人,就是垃圾回收器

内存管理:从硬件到软件

内存管理实际上是各种应用读写数据的过程,内存管理器会决定往哪里放置应用的数据。就像比喻中的有限的书页,内存块页是有限的,因此内存管理器必须找到空闲的空间来提供给应用。这个过程一般称作内存分配。

在另一边,当一段数据不再需要的时候,它们就可以被删除,或者说被释放。但是释放到哪里呢?这些“内存”是哪里来的呢?

在你运行你的Python程序的时候,在你的电脑的某个地方,有一个物理设备存储着你的数据。但是在对象实际到达硬件之前,Python代码要经过很多层的抽象。

这些硬件(比如内存或者硬盘)上的抽象层中最主要的一层就是操作系统,由它来执行(或者拒绝)对内存的读写请求。

在操作系统之上就是应用程序,比如默认的Python实现。你的Python代码的内存管理是由这个Python程序来处理的,其中用到的算法和数据结构就是这篇文章的重点。

默认的Python实现

CPython是默认的Python实现,它实际上是由C语言写的。

当我第一次听到这一点的时候很惊讶,一个语言竟然是用另一个语言写的?好吧,不全是,但是有一点。

Python语言定义在用英文写的参考手册中(译者:现在大部分的Python文档都已经有了中文翻译,在官网文档的左上角选择简体中文即可看到)。然而这个手册自身并非万能,你需要一些东西,来按照手册中的规则解释你所写的代码,另外还需要一些东西来在计算机上执行解释后的代码。默认的Python实现满足了这两点需求:它将你的Python代码转换成指令,然后再一个虚拟机上运行这些指令。

提示:虚拟机类似物理机器,但是使用软件实现的。它们一般处理类似汇编指令的基础指令。

Python是一个解释型的语言,你的代码实际上被编译成字节码,它们是更容易被计算机识别的指令,这些指令会在运行时被一个虚拟机解释。

你是否看到过.pyc文件或者__pycache__目录?那就是被虚拟机解释的字节码。

需要注意的一点是,除了CPython,还有很多不同的实现。IronPython(将Python代码)编译成微软的通用语言运行时;Jython编译成Java的字节码以在JVM上运行;还有PyPy,但这值得一整篇文章来写了,所以在这里我只是顺带一提。

出于本文的目的,我将重点介绍Python的默认实现:CPython的内存管理。

免责声明:尽管本文的大多数内容会保留到新版本的Python中,但未来情况仍有可能变化。这篇文章的参考版本是当前的最新版Python 3.7。

好的,现在我们明白了CPython是用C语言写的,并且它解释Python的字节码。那这与内存管理有什么关系呢?好吧,内存管理的算法和数据结构存在于CPython的实现代码中,那是用C语言写的。为了理解Python的内存管理,你必须首先对CPython自身有起码的了解。

CPython是用C写的,而C语言并非原生地支持面向对象编程。因此,在CPython的实现中有一些有意思的设计。

你可能听说过在Python中一切都是对象,包括intstr之类的类型,在CPython的实现级别上来说的确如此,有一个称为PyObject的结构体,CPython中所有其他对象都会使用这个结构。

这个PyObject是Python中所有对象的祖父,其中包含了两个东西:

  • ob_refcnt:引用计数
  • ob_type:另一个类型的指针

引用计数在垃圾回收时被使用,另外还有一个指针,它指向实际的对象类型,这个对象类型实际上也是另一个结构,用来描述一个Python对象(比如dict或者int)。

每个对象有它自己的对象内存分配器,它知道如何获取内存来存储这个对象。每个对象也有一个特定于对象内存释放器,它会在对象不再需要的时候“释放”掉内存。

然而,在所有关于内存分配和释放的讨论中,有一个重要的因素:内存时计算机中的共享资源,如果两个进程尝试去同时写入同一个地方就糟糕了。

全局解释器锁(GIL)

GIL就是处理共享资源的常见问题(比如计算机的内存)的一个解决方案。当两个线程同时尝试更改同一个资源时,他们可能踩到对方的脚趾头。最终的结果可能很混乱,两个线程都没有得到他们想要的结果。

再想一想那个书本的比喻。假设有两个作者固执地认为该他们写了,并且两个人想要同时向同一个书页中写入。

他们都忽略了对方的创作故事的企图,然后开始了向页面的写入。最终的结果就是两个故事互相重叠,导致整个页面完全无法阅读。

这个问题的解决方案之一是,当线程与共享资源(书页)进行交互时,在解释器上进行的单个全局锁定。换句话说,同时只有一个作者可以写入。

Python的GIL通过锁定整个解释器来实现这一点,这意味着另一个线程不可能抢占当前的线程。当CPython处理内存的时候,它利用GIL来保证操作的安全。

这种方式有利有弊,在Python社区中CIL受到了很大的质疑。要想了解更多关于CIL的内容,我建议阅读What is the Python Global Interpreter Lock (GIL)?

垃圾回收

我们再来回顾那个书本的比喻。假设书中有一些故事已经很老了,没有人正在阅读和引用这些故事了,这时候你就可以丢掉它们来为新的故事腾出空间了。

那些旧的、不再被引用的故事可以与Python中引用计数降为0的对象作类比。记住Python中的每个对象都有一个引用计数和指向类型的指针。

引用计数会因为不同的原因而增长。比如,当你将一个对象赋给另一个变量的时候,引用计数就会加一:

numbers = [1, 2, 3]
## 引用计数 = 1
more_numbers = numbers
## 引用计数 = 2

把一个对象作为参数传递的时候,引用计数也会增加:

total = sum(numbers)

最后一个例子是,当一个对象包含在一个列表中,它的引用计数也会增加:

matrix = [numbers, numbers, numbers]

Python允许通过sys模块来某个对象检查当前的引用计数,你可以使用sys.getrefcount(numbers)来获取,但是记住把对象传递给getrefcount()函数会使引用计数增加1。

无论那种情况,如果一个对象仍然被你的代码需要,它的引用计数就会大于0。一旦它降到0,这个对象对应的释放函数将会被调用,“释放”内存来让其他的对象使用。

但是“释放”内存是什么意思呢?其他的对象如何使用它?让我们进入CPython的内存管理。

CPython的内存管理

我们将深入CPython的内存架构和算法,系好安全带!

就像之前提到的那样,从硬件到CPython之间有很多层的抽象。操作系统抽象物理内存,并创建应用程序(包括Python)可访问的虚拟内存层。

操作系统的虚拟内存管理器为Python的进程分配了一大块内存。下面这张图片中的灰色区域正在被Python进程所拥有。

Python将一部分内存用于内部使用和非对象内存,另一部分用于对象的存储(你的intdict等等)。注意这是被简化之后的,如果你想要完整的图片,你可以查看CPython的源代码,那里可以看到所有的内存管理过程。

CPython有一个对象分配器,它的职责是在上述的对象内存区域(译者:绿色区域)中分配内存。这个对象分配器是大多数逻辑发生的地方,每当一个新的对象需要空间或者被删除的时候它就会被调用。

一般地,像listint这样的对象,在加入或者删除数据的时候不会同时涉及到大量数据,所以这个分配其的设计是在一次处理少量数据的时候工作良好,同时,不到万不得已,它也会尽可能地尝试不去分配内存。

源代码里的注释描述这个分配器为:一个专用于小块内存分配的快速分配器,基于通用目的的malloc分配器。其中,malloc是C语言标准库的内存分配函数.

现在我们来看看CPython的内存分配策略。首先我们讨论三个主要的部分以及它们之间的关系。

arena是内存中最大块,它会对齐到内存页的边界。页边界是操作系统所使用的定长连续内存块的边缘,Python假设系统页大小是256K字节。

区块内部是poolpool是一种虚拟的内存页(4K字节)。就像我们的比喻中的书页,这些pool被分割成较小的内存块(block)。

给定pool中的所有的block都有着相同的大小级别。给定一定大小的请求数据,对应的大小级别就指定了固定的block大小。下面这张表来自源代码的注释:

数据大小(字节) 块大小 大小级别序号
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字节大小的块中。

Pools

Pool是由相同大小级别的block组成的,每个pool维护了一个双向链表,连接到其他相同大小级别的pool,因此算法可以很容易地为所需的块大小找到可用的空间,即使是在不同的pool里面。

usedpools链表跟踪有一定空间可用的各个级别的pool,当请求某个块大小时,算法会在这个usedpools列表中检查这个块大小的pool列表。

pool会处于三种状态:usedfullemptyused状态的pool有着空闲的block来存储数据;full状态的pool中所有的block都已被分配出去,并且包含着数据;empty状态的pool中没有数据,在需要的时候,可以分配任何大小级别的块。

freepools链表跟踪着所有的empty状态的pool,什么时候empty pool会被使用呢?

假设你的代码需要8字节的内存块,如果在usedpools中没有8字节大小级别的pool,一个新的empty pool就会被初始化,来存储8字节的块,这个pool会被加入到usedpools列表中以便之后的分配请求。

full状态的pool由于一些内存不再需要而释放了一些块,那这个pool就会被重新加入到usedpools列表中。

现在你可以看到pool(以及大小级别)是如何在各个状态间转移的了。

Block

就像上图所述,pool中包含了一个指向其中空闲块的指针。其实这和实际的情况有所差别,根据源代码中的注释,这个内存分配器会“在各个层级上,不到万不得一不去动用内存”。

这意味着一个pool中的block可以拥有三种状态,如下:

  • untouched:从未被分配过的内存;
  • free:曾被分配过,但之后被CPython“释放”掉了,不再包含有效的数据;
  • allocated:实际包含有效数据的内存。

freeblock指针指向了一个free状态的block的单链表。换句话说,一个可以存储数据的地方的列表。如果需要更多free block,分配器就会从pool中获取一些untouched状态的块。

当内存分配器“释放”掉一些块之后,这些free block会被加入到freeblock列表的头部,(所以)这个列表可能是内存中不连续的块,就像第一个图所示。可能像是下面这张图:

Arena

arena包含了pool,这些pool的状态可能是usedfull或者empty,而arena自身并没有这些明确的状态。

相反,arena被组织到一个称为usable_arenas的双向链表中,这个链表根据其中可用的空pool的数量排序,空pool越少,就越靠近列表的前方。

这就意味着,在存储新数据的时候,包含最多数据的arena将会被选中。为什么不是反过来的?为什么不把数据放在有最多空闲空间的地方?

这就引出了“释放内存”的真实意义。你会注意到我说“释放”的时候一直带着引号,原因就是block只是被认为“释放”掉了,实际上这些内存并没有返还给操作系统,Python进程保留着这些内存,之后会用它存储新的数据。真正的释放内存会将其返回给操作系统。

arena是唯一一个可以被实际释放的东西,所以有理由认为,应该允许那些更接近空的arena变成完全的空,这样的话,其中的内存就可以被实际释放掉,以减少Python程序的整体内存占用。

结论

内存管理是计算机中不可或缺的一部分,不论结果好坏,Python几乎将所有的内存管理都在幕后处理。

在这篇文章中,你学到了:

  • 什么是内存管理以及它的重要性;
  • 默认的Python实现CPython是如何用C语言写的;
  • CPython内存管理中的算法和数据结构是如何处理你的数据的。

Pyhton抽象了许多计算机的严格细节,这就使你有能力在更高的层级上开发,而不用因为担心如何、在哪存储你的字节而头痛。


原文链接:Memory Management in Python


译者记

从整体来说,这篇文章是比较容易,适合对内存相关概念比较模糊的新手看;从Python的角度来说,大概讲述了关于Python内存管理中的一些概念、数据结构和算法,不太细致但作为入门了解也是挺好的。