美文网首页
【Real Python】内存管理

【Real Python】内存管理

作者: lndyzwdxhs | 来源:发表于2019-01-24 18:27 被阅读12次

目录

  • 内存就像是一本空白的书
  • 内存管理:从硬件到软件
  • 默认的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中所有的东西都是对象,甚至包括类型(例如,intstr等)。是的,在CPython的代码中确实是这样实现的。在CPython中有一个struct结构叫做PyObjectCPython中的所有对象都使用这个struct结构。

注意:
C语言中,struct是一个自定义数据类型,它将不同数据类型组织在一起。与面向对象语言相比,它就像是一个只有属性没有方法的类。

Python中的所有对象都包含了PyObject结构,这个结构里边只有两个域:

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

引用计数(ob_refcnt)用作垃圾收集( garbage collection)。 指针(ob_type)指向当前对象的类型。对象类型是另外一种结构,这种结构用来描述dict/int等Python中的类型对象。

每一个对象都有特定的内存分配器,这个内存分配器知道如何获取内存来存储字节的对象。除此之外,每一个对象也都有特定的内存释放器,它会释放不再需要的内存。

然鹅,在我们讨论的内存申请与释放中有一个重要的因素:内存是计算机上的共享资源,当两个不同的程序同一时间尝试去写同一块内存时,就会有大问题出现。

全局解释器锁 (GIL)

GIL是解决共享资源(例如计算机上的内存等)分配的一种解决方案。可以想象,当两个线程同一时间尝试去修改相同的资源,最后的结果是不可预期的,它们随机的执行导致最后两个线程得到的结果都不是自己想要的。

类比之前书的例子,假设两个作者都认为该自己写文章了,不仅如此,他们还需要相同的时间在相同的页上内容。

两个作者之间都忽略彼此的动作,都开始自顾自的写着文章。最后的结果就是两篇文章都叠在了一起,导致这个页都无法阅读了。

这个问题的一个解决方案就是当一个线程准备访问共享资源(书中的页)的时候,用一个独立的全局的锁将解释器锁住。换句话说,就是同一时间只能有一个作者写内容。

PythonGIL通过锁住整个解释器来实现,意思就是说两个线程不可能同一时间被执行。当CPython处理内存的时候,它使用GIL来确保线程安全。

这种方法有利有弊,GILPython社区引发了激烈的讨论。想知道更多关于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)使用的内存。另一部专门用作对象的存储intdict等对象)。需要注意的是,这张图画的比较简单,更容易理解整体框架,如果你想了解更细节的东西,可以自行研究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。在本文中,mallocC语言库中的一个函数,用来分配内存的。

现在我们已经知道了CPython的内存分配策略。然后我们会介绍内存管理中的3大块东西和它们之间是如何联系的?

arena是最大的内存块,它在内存中的页面边界(page boundary)对齐。页面边界就是操作系统使用的固定长度并且连续的内存块的边缘。Python中,我们假设系统的页大小(page size)是256KB

Arena, Pools, and Block

arenas中包含的是poolspool是一个虚拟内存页(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列表存储所有状态是emptypools。但是,什么时候emptypool会被使用?

假设你需要一个8字节大小的内存块。如果usedpools中没有8字节”size class“的pool,一个新的empty状态的pool会被初始化为存储8字节大小的blocks。这个新的pool会被添加到usedpools列表中,以备后用。

假如一个full状态的pool释放了一些blocks,那么这个pool会被重新加到usedpools列表对应的“size class”中。

上面可以看到,pools是如何通过这个算法在三种状态之间自由转换的。

Blocks

Diagram of Used, Full, and Emtpy Pools

如上图所示,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列表一般不会是连续的,一般看起来会像下图所示的这样:

Diagrams showing freeblock Singly-Linked List Pointing to Free Blocks in a Pool

Arenas

arenas包含pools,这些pools可以是usedfullempty状态。arenas自身没有像pools那样明确的状态。

arenas被一个双向链表连接起来,称为usable_arenas。这个列表根据可用的free pools的数量排序,free pools越少的arena将排在链表的前面。

usable_areas Doubly-linked List of Arenas

这就意味着,将会从数据最多的arena中选择出空间来存放新数据。思考一下,为什么不选择数据量最少的arena来存放新数据?

其实,Python释放内存时,没有将内存空间释放给操作系统,而是自己维护起来,随后会被其他Python对象重新申请。

arenas是唯一可以被真正释放给操作系统的对象,所以,Python会尽可能的保持arena为空,尽量先使用数据量多的arena。这样,某块为空的arena就可以真正被释放给操作系统,减少Python程序的总体内存占用。

结论

内存管理是计算机工作中不可缺少的一部分。Python在幕后处理了所有的事情,不管好事还是坏事。

这篇文章中,你学到了:

  • 内存管理是什么?它为什么那么重要?
  • CPython是如何使用C语言实现的?
  • 处理数据的时候,CPython的内存管理是如何将数据结构和算法结合在一起的?

Python抽象了很多计算机工作的细节。这使你工作在一个更高的层次,不用去关心数据存储在哪里,是如何存储的。


欢迎关注微信公众号(coder0x00)或扫描下方二维码关注,我们将持续搜寻程序员必备基础技能包提供给大家。


相关文章

  • 【Real Python】内存管理

    目录内存就像是一本空白的书内存管理:从硬件到软件默认的Python实现(CPython)全局解释器锁 (GIL)垃...

  • python内存管理机制

    Python内存管理机制 Python内存管理机制主要包括以下三个方面: 引用计数机制 垃圾回收机制 内存池机制 ...

  • 深入理解Python内存管理与垃圾回收,再也不怕问了(一)

    面试官:听说你学Python?那你给我讲讲Python如何进行内存管理? 我:???内存管理不太清楚额。。。 面试...

  • python内存释放

    Python内存释放 python话说会自己管理内存,实际上,对于占用很大内存的对象,并不会马上释放。举例,a=r...

  • 新手上路?八大秘术助你offer无数(Python初学者/码农必

    话不多说,干货: 1、Python是如何进行内存管理的? Python的内存管理主要有三种机制:引用计数机制、垃圾...

  • 面试日记--python的内存管理

    面试中被问到python的内存管理,只是说是python有自己的内存管理机制,有自己的垃圾回收机制,却不能详细作答...

  • Python基础知识

    一、Python简介 Python 是一种解释型语言,在 Python 中,由于内存管理是由 Python ...

  • python(Class7)

    内存管理之循环引用 在Python3.x中,内存管理问题基本上不会出现,类似与OC中的ARC机制在Python2....

  • Python3学习 - 第二节

    为什么说Python采用的是基于值的内存管理模式? Python采用的是基于值的内存管理方式,如果为不同变量赋相同...

  • iOS ARC

    内存管理 引用计数:Objective-C Python 垃圾收集:C#,Java等 区别 内存管理的基本规则 自...

网友评论

      本文标题:【Real Python】内存管理

      本文链接:https://www.haomeiwen.com/subject/pbyujqtx.html