内存管理

Python中的内存管理机制的层次结构提供了4层,其中最底层则是C运行的mallocfree接口,往上的三层才是由Python实现并且维护的。

                                                

python对内存的管理 简述python内存管理机制_python对内存的管理

Layer0层是C运行的mallocfree接口

Layer1层则是在Layer0层的基础之上对其提供的接口进行了统一的封装

这是因为虽然不同的操作系统都提供标准定义的内存管理接口,但是对于某些特殊的情况不同的操作系统都不同的行为,比如说调
用malloc(0),有的操作系统会返回NULL,表示内存申请失败;然而有的操作系统会返回一个貌似正常的指针,但是这个指针所指的内存
并不是有效的。为了广泛的移植性,Python必须保证相同的语义一定代表相同的运行行为。

Layer2层为内存池。内存管理机制上,Python构建了更高抽象的内存管理策略。比如说一些常用对象,包括整数对象、字符串对象等等。

Layer3层主要是对象缓冲池机制,它是建立在Layer2基础上的。

它有整数对象缓冲池、String对象缓冲池、List和Dict对象缓冲池


内存池机制

Python为了避免频繁的申请和删除内存所造成系统切换于用户态和核心态的开销,从而引入了内存池机制。

整个小块内存的内存池一共分为4层,从下至上分别是block、pool、arena和内存池

需要说明的是:block、pool和Arean都是代码中可以找到的实体,而最顶层的内存池只是一个概念上的东西,表示Python对于整个小块内存分配和释放行为的内存管理机制。

注意,python又分为大内存和小内存,内存大小以256字节为界限,大于256的大内存通过malloc进行分配,小于则通过内存池分配

                                           

python对内存的管理 简述python内存管理机制_链表_02

  1. block:最小的内存单元,大小为8的整数倍。有很多种类的block,不同种类的block都有不同的内存大小,申请内存的时候只需要找到适合自身大小的block即可,当然申请的内存也是存在一个上限,如果超过这个上限,则退化到使用最底层的malloc进行申请。
  2. pool:一个pool管理着一堆有固定大小的内存块,其大小通常为一个系统内存页的大小。
  3. arena:多个pool组合成一个arena。
  4. 内存池:一个整体的概念,用于随整个小块内存分配和释放

对象缓冲池

面试题:

设计一个缓冲池,用于存放系统所需要的资源。满足如下要求:
(1)当读取缓冲池资源时,如果没有该资源,则创建该资源,放入缓冲池中。
(2)缓冲池可以存放各种形式的资源。
(3)要有刷新机制,当一个资源长时间没有使用时,要把该资源从缓冲池中剔除。
要考虑分配资源的合理性和时效性,缓冲池可以有的参数有最小资源数、最大资源数、timeout等,重点描述一下缓冲池的刷新机制。

思路:

先要知道缓冲池是用来干嘛的!所谓缓冲池是为了减少磁盘的IO操作,专门在内存中开辟一块区域,将磁盘中一些经常访问的数据放入到该区域,以检查IO操作。

因此设计一个缓存池需要考虑的几个问题:

  1. 如何对数据库进行组织,使得可以快速查找到缓存池中的资源
  2. 当缓存池满的时候,使用什么样的换入换出策略,如何保证数据块置换的效率
  3. 对同一个数据块的并发访问
  4. 维护数据一致性,采用什么样的写入策略

缓冲池的概貌

                                            

python对内存的管理 简述python内存管理机制_python对内存的管理_03

hash bucket 是内存二维数组的第一维。它通过对buffer header里记录的数据块地址和数据块类型运用hash算法以后,得到的组号。

hash chain是属于同一个hash bucket的所有buffer header所串起来的链表。实际上,hash bucket只是一个逻辑上的概念。每个hash bucket都是通过不同的hash chain而体现出来的。每个hash chain都会由一个cache buffers chains latch来管理其并发操作。

而对于buffer header来说,每一个数据块在被读入buffer cache时,都会先在buffer cache中构造一个buffer header,buffer header与数据块一一对应。

 

刷新机制

所谓刷新即当一个资源长时间没有使用时,要把该资源从缓冲池中剔除,怎么样才能判定一个资源长时间没有使用呢?参考操作系统中Cache的设计机制,cache中常用换页机制,采用的有FIFO、LRU、Clock算法。

这里我们就是用LRU算法。

我们举一个例子。假设缓冲池只能容纳4个数据块,同时只有一个hash chain和一个LRU。

当系统刚刚启动,缓冲池是空的。这时前台进程获取数据块,系统找一个空的内存数据块,并将其对应的buffer header挂到hash chain上。同时,系统还会把该buffer header挂到LRU的最尾端。随后前台进程又发出获取数据块请求,这时所找到的buffer header在LRU上会挂到前一个buffer header的后面,也就是说请求所找到的buffer header现在变成了LRU的最尾端了。假设发出4数据请求以后找到了4个buffer header,从而用完了所有的buffer cache空间。这个时候的LRU可以用下图来表示。

                               

python对内存的管理 简述python内存管理机制_python原理_04

这个时候,进程发来了第5个数据块请求语句。这时的缓存池里已经没有空的内存数据块了。但是既然需要容纳下第五个数据块,就必然需要找一个可以被替换的内存数据块。这个内存数据块会到LRU上去找。按照系统设定的最近最少使用的原则,位于LRU最尾端的BH1将成为牺牲者,系统会把该BH1对应的内存数据块的内容清空,并将当前所获得的数据块的内容拷贝进去。这个时候,BH1就成了LRU的首端,而BH2则成为了LRU的尾端。如下图所示。在这种方式下,经常被访问的数据块可以一直靠近LRU的首端,也就保证了这些数据块可以尽可能的不被替换掉,从而保证了访问的效率。

 

                                  

python对内存的管理 简述python内存管理机制_python原理_05

加入访问次数的LRU

OK,假如我们想把每个数据块的访问次数加入到数据块置换策略中,该如何实现呢?

我们来增加一个辅助链表。缓冲池有LRU和LRUW两个链表,分别叫做辅助链表和主链表。同时还对buffer header增加了一个属性:touch数量,也就是每个buffer header曾经被访问过的次数,来对LRU链表进行管理。系统每访问一次buffer header,就会将该buffer header上的touch数量增加1,因此,touch数量“近似”的体现了某个内存数据块总共被访问的次数。注意,这只是近似,并不精确。因为touch的增加并没有使用锁来管理并发性。这只是一个大概值,表示趋势的,不用百分百的精确。

还是用上面的这个例子来说明。还是假设buffer cache只能容纳4个数据块,同时只有一个hash chain和一个LRU(确切的说应该是一对LRU主链表和辅助链表)。读入第一个数据块时,该数据块对应的buffer header会挂到LRU辅助链表(注意,这里是辅助链表,而不是主链表)的最末端,同时touch数量为1。读取第二个不同的数据块时,该数据块对应的buffer header会挂到前一个buffer header的后面,从而位于LRU辅助链表的最末端,同样touch为1。假设4个数据块全都用完以后的LRU链表可以用下图四描述。每个buffer header的touch数量都为1

 

                                  

python对内存的管理 简述python内存管理机制_缓冲池_06

上图中我们可以看到辅助LRU链表都挂满了,而主LRU链表还是空的。这个时候,系统要求返回指定的数据块。系统发现buffer cache里已经没有空的内存数据块了,于是从辅助LRU链表的尾部开始扫描,也就是从BH1开始扫描,以查找可以被替代的数据块。这时将选出BH1作为牺牲者,并将其对应的内存数据块的内容清空,同时将当前第五个数据块的内容拷贝进去。但是这里要注意,这个时候该BH1在LRU链表上的位置并不会发生任何的变化。而不会之前的那样,BH1变成LRU链表的首端。

接下来,系统发出两次数据块请求,分别要返回与第5个和第4个一样的数据块,也就是要返回当前的BH1和BH4。这个时候,oracle会增加BH1和BH4的touch数量,同时将该BH1和BH4从辅助LRU链表上摘下,转移到主LRU链表的中间位置。可以用下图描述。

                                     

python对内存的管理 简述python内存管理机制_python原理_07

 

 

这个时候,如果发来了第个数据块请求,要求返回与第3个相同的数据块,也就是当前的BH3,则这时该BH3会插入主LRU链表上的BH1和BH4中间,注意每次向主LRU列表插入buffer header时都是向中间位置插入。如果发来了第九句SQL要求返回BH2,则我们可以知道,BH2会转移到主LRU链表的中间。这个时候,辅助LRU链表就空了,没有buffer header了。

这时,如果又发来第10个数据块请求,要求返回一个新的、buffer cache中不存在所需内容的数据块时。oracle会先扫描辅助LRU链表,发现上面没有任何的buffer header时,则必须扫描主LRU链表。从尾部开始扫描,采用前面说到的与扫描辅助LRU链表相同的规则挑选牺牲者。挑出的可以被替代的buffer header将从主LRU链表上摘下,放入辅助LRU链表。

从上面所描述的buffer header在辅助LRU链表和主LRU链表之间交替的过程中,这种改进LRU链表的管理方式的目的是能够将多次被访问的数据块保留在内存里,同时又要平衡有限的内存资源。这种方式相比较之前而言,无疑是进步很多的。在之前中,某个数据块可能只会被访问一次,但是就这么一次的访问就将该数据块放到了LRU的首端,从而可能就挤掉了一个LRU上不是那么经常被访问,但是也会多次访问的数据块。而后面的算法,将访问一次的数据块和访问一次以上的数据块彻底分开,而且查找可用数据块时,始终都是从辅助LRU链表开始扫描。实际上也就使得越倾向于只访问一次的数据块越快的从内存中清理出去。