Python是一门开发效率很高的语言,而且其既下里巴人,又阳春白雪。也就是说这门语言只要稍加学习就可以上手开发,而深入探究也会发现Python有很多高深的东西。最近读了《Python源码剖析》,收获良多,今天就把Python的内存管理整理一番。
本文的组织如下:
第一部分:整理Python的内存管理机制,主要包括内存池以及对象缓存池
第二部分:从百度的笔试题来探讨如何实现一个缓存池。
第一部分:整理Python的内存管理机制
![](https://img-my.csdn.net/uploads/201205/08/1336476307_4497.jpg)
其中Layer2为内存池,Layer3为对象缓冲池。Python的对象缓冲池Layer3是建立在Layer2基础上的。
本文重点总结内存池和对象缓冲池。
内存池
ython又分为大内存和小内存。大小以256字节为界限,对于大内存使用Malloc进行分配,而对于小内存则使用内存池进行分配。
Python的内存池又分为4个层次:Block、Pool、Arean、usedpool,如下图所示
![](https://img-my.csdn.net/uploads/201205/08/1336476362_4151.jpg)
其中block是最小的内存单元,大小为8的整数倍。如果想申请27B的内存,会分配一个32B的block,其中申请size和size_index之间的关系有对应,见下图
![](https://img-my.csdn.net/uploads/201205/08/1336476393_5592.jpg)
其中block是最小的内存单元,大小为8的整数倍。如果想申请27B的内存,会分配一个32B的block,其中申请size和size_index之间的关系有对应,见下图
![](https://img-my.csdn.net/uploads/201205/08/1336476410_7947.jpg)
对象缓冲池
整数对象缓冲池
对于[-5,257]这样的小整数,系统已经初始化好,可以直接拿来用。而对于其他的大整数,系统则提前申请了一块内存空间,等需要的时候在这上面创建大整数对象。
最上面的为PyInBlock结构,每个PyInBlock中是一个PyIntObject数组。
而连续数组在内存不断进行申请和释放中就会出现不连续的空闲块,因此需要把所有的空闲内存块组织起来,这样就需要使用free_list进行组织
2. String对象缓冲池
Python为256个Ascii码进行了对象缓冲池,见下图
这里面的过程是先对所创建的字符对象进行Intern机制,再将intern的结果缓存到字符串缓冲池characters中。所谓的intern机制是指在系统中建立一个(key,value)映射关系的集合,记录着被intern机制处理过的PyStringObject对象。对于被Intern之后的字符串,在整个Python运行时,系统中都只有唯一的与该字符串对应的PyStringObject对象。这样当判断两个PyStringObject对象是否相同时,如果它们都被Intern了,那么只需要简单地检查它们对应的PyObject*是否相同即可。这个机制既节省了空间,又简化了对PyStringObject对象的比较
3. List和Dict对象缓冲池
![](https://img-my.csdn.net/uploads/201205/08/1336476591_5584.jpg)
第二部分 设计缓冲池
说完了Python的内存管理,我们再来探讨一下如何设计并实现一个对象缓冲池。首先来看一下百度的一个笔试题。
设计一个缓冲池,用于存放系统所需要的资源。满足如下要求:
(1)当读取缓冲池资源是,如果没有该资源,则创建该资源,放入缓冲池中。
(2)缓冲池可以存放各种形式的资源。
(3)要有刷新机制,当一个资源长时间没有使用时,要把该资源从缓冲池中剔除。
要考虑分配资源的合理性和时效性,缓冲池可以有的参数有最小资源数、最大资源数、timeout等,重点描述一下缓冲池的刷新机制。
先要知道缓冲池是用来干嘛的!所谓缓冲池是为了减少磁盘的IO操作,专门在内存中开辟一块区域,将磁盘中一些经常访问的数据放入到该区域,以检查IO操作。
因此设计一个缓存池需要考虑的几个问题:
a) 如何对数据库进行组织,使得可以快速查找到缓存池中的资源
b) 当缓存池满的时候,使用什么样的换入换出策略,如何保证数据块置换的效率
c) 对同一个数据块的并发访问
d)维护数据一致性,采用什么样的写入策略
缓冲池的概貌
这里的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算法。
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可以用下图来表示。![](https://img-my.csdn.net/uploads/201205/08/1336476749_5905.jpg)
这个时候,进程发来了第5个数据块请求语句。这时的缓存池里已经没有空的内存数据块了。但是既然需要容纳下第五个数据块,就必然需要找一个可以被替换的内存数据块。这个内存数据块会到LRU上去找。按照系统设定的最近最少使用的原则,位于LRU最尾端的BH1将成为牺牲者,系统会把该BH1对应的内存数据块的内容清空,并将当前所获得的数据块的内容拷贝进去。这个时候,BH1就成了LRU的首端,而BH2则成为了LRU的尾端。如下图所示。在这种方式下,经常被访问的数据块可以一直靠近LRU的首端,也就保证了这些数据块可以尽可能的不被替换掉,从而保证了访问的效率。
![](https://img-my.csdn.net/uploads/201205/08/1336476756_3333.jpg)
加入访问次数的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。
![](https://img-my.csdn.net/uploads/201205/08/1336476804_6532.jpg)
上图中我们可以看到辅助LRU链表都挂满了,而主LRU链表还是空的。这个时候,系统要求返回指定的数据块。系统发现buffer cache里已经没有空的内存数据块了,于是从辅助LRU链表的尾部开始扫描,也就是从BH1开始扫描,以查找可以被替代的数据块。这时将选出BH1作为牺牲者,并将其对应的内存数据块的内容清空,同时将当前第五个数据块的内容拷贝进去。但是这里要注意,这个时候该BH1在LRU链表上的位置并不会发生任何的变化。而不会之前的那样,BH1变成LRU链表的首端。
接下来,系统发出两次数据块请求,分别要返回与第5个和第4个一样的数据块,也就是要返回当前的BH1和BH4。这个时候,oracle会增加BH1和BH4的touch数量,同时将该BH1和BH4从辅助LRU链表上摘下,转移到主LRU链表的中间位置。可以用下图描述。
![](https://img-my.csdn.net/uploads/201205/08/1336476824_8644.jpg)
这个时候,如果发来了第个数据块请求,要求返回与第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链表开始扫描。实际上也就使得越倾向于只访问一次的数据块越快的从内存中清理出去。