python内存管理
来源:互联网 发布:淘宝卖什么好呢 编辑:程序博客网 时间:2024/06/10 02:51
起因
春节期间,翻了翻《垃圾回收的算法和实现》,真是一本好书。时间比较短还没有完全看完。但是让我吃惊的时候,这是一本将垃圾回收的书,但是在它的实现篇里居然对python的内存管理有比较深入的讲解。今天有空,又细细结合python 3.6的代码看了一遍。在这里写下我的这个读书笔记。
intro
上面是我结合书中的讲解,把它提到的一些概念用这个图都表达了出来。当然也可以说,我把书中的几张图合成了一张。
其实如果你去看python的源代码,在Objects/obmalloc.c这个文件中,对python是怎么维护内存的有详细的讲解。
概念
arena
这个区域是从堆内存里面直接malloc出来的,每个是256KB。
pool
针对malloc出来的arena,我们会对它进行分割,pool的大小是4K字节。至于为什么是4K字节,书中说这个是因为现在的操作系统大都是以4KB为大小做了内存页的管理单位,把我们的pool的大小也就设为了4KB。
#define SMALL_REQUEST_THRESHOLD 512static void *_PyObject_Alloc(int use_calloc, void *ctx, size_t nelem, size_t elsize){... if ((nbytes - 1) < SMALL_REQUEST_THRESHOLD) { // do all the small objects allocation just as the book describes //... }redirect: /* Redirect the original request to the underlying (libc) allocator. * We jump here on bigger requests, on error in the code above (as a * last chance to serve the request) or when the max memory limit * has been reached. */ { void *result; if (use_calloc) result = PyMem_RawCalloc(nelem, elsize); else result = PyMem_RawMalloc(nbytes); if (!result) _Py_AllocatedBlocks--; return result; }}
从上面的代码可以看到,只有对象大小在512字节以内的才会走到我们现在要讨论的这个分配系统,而大于512字节的,则直接走malloc了。
这里的设置跟书中提到的256字节的限制是不同的,可能在不同版本中,这个值得到了修正。
对于usedpool,它里面所维护的都是正在使用的pool,用代码中的话来说
- 在它里面的pool至少有一个block是被分配的
- 在它里面的pool至少有一个block是没有被分配的
那么那些被分配满block的pool去哪里了呢?它没有在这个usedpool中,它们在堆中处于游离状态。
而那些空的pool,则将它们返回给arena的freepools,由arena继续管理。
当arena里面全都是空pool的时候,这个arena就可以被释放掉了,arena的释放是通过free
来完成的。
需要特殊说明的是,在上图中,pool左边我特意画了一块空白的空间出来,这个在书中是没有的,因为根据我的理解和读代码所得,每个pool都带有它自己的管理结构pool_header,也就是这个pool的metadata,它的定义我也在图中写出来了。它里面维护着这个pool中的一些信息,以及一些变量用来在这个pool中进行block分配。
block
有了pool之后,我们就可以在pool中划分出我们的block来了,但是我们在使用python过程中,对象的大小千奇百怪,为了内能适应不同的对象大小要求,python内部,采用了类似于malloc管理内存的方法,针对于每一个大小的对象,我们都会有一个pool与它对应。这要就有了上图中右边的这个结构。
这个结构是为了在分配过程中快速找到相对应的pool的一个结构,它可以在O(1)的时间分配所需要的内存。
usedpools
注意右边这个usedpool的全局变量,从概念上来看,它应该保存的是pool_header的前后关系,但是在在代码的实现中,里面实际上只是保存了pool_header里面的nextpool
和prevpool
的信息。为什么要这么做,在源代码里作者也给出了答案,这个结构需要经常的变动,如果把pool_header的信息全都放进去的话,会有一些空间上的浪费,使得cpu不能一次把整个结构load到cpu cache中去,或者说是cache line中去。为了防止这种情况的发生,加快对这个结构的访问,才做了这个优化。
pool中的freeblock
下面说说在看代码的过程中,我所遇到的一个问题,通过这个问题更深的了解了在pool中,block的分配是如何进行的。
typedef uint8_t block; if ((pool->freeblock = *(block **)bp) != NULL) { ... }
在看到代码的时候,经常看到*(block **)bp
,从代码的上下文来看,这个就是讲pool->freeblock
指向下一个freeblock,但是简单的这个指针操作真的就能完成了嘛?
bp本来就是一个block指针,现在把它强转成block指针的指针,也就是说强转成一个指向block指针数组的的指针(简单这么理解),然后再解引用,相当于取这个数组的第一个元素(一个block的指针),这个就是我们下一个freeblock了?注意这里的block实际上是一个8位整型的别名。这里很是疑惑,于是转过头去看pool初始化的地方的代码。
init_pool: /* Frontlink to used pools. */ next = usedpools[size + size]; /* == prev */ pool->nextpool = next; pool->prevpool = next; next->nextpool = pool; next->prevpool = pool; pool->ref.count = 1; if (pool->szidx == size) { /* Luckily, this pool last contained blocks * of the same size class, so its header * and free list are already initialized. */ bp = pool->freeblock; assert(bp != NULL); pool->freeblock = *(block **)bp; UNLOCK(); if (use_calloc) memset(bp, 0, nbytes); return (void *)bp; } /* * Initialize the pool header, set up the free list to * contain just the second block, and return the first * block. */ pool->szidx = size; size = INDEX2SIZE(size); bp = (block *)pool + POOL_OVERHEAD; pool->nextoffset = POOL_OVERHEAD + (size << 1); pool->maxnextoffset = POOL_SIZE - size; pool->freeblock = bp + size; *(block **)(pool->freeblock) = NULL; //----#1 UNLOCK(); if (use_calloc) memset(bp, 0, nbytes); return (void *)bp;
在此看到代码注释1的时候,就明白了,初始化的时候,就使用了freeblock的第一个block指针,也就是说每个block的第一个指针大小(在32位机上就是第一个32位,在64位机上就是第一个64位)是用来存放下一个block的内存地址的。注意这里最开始并没有把整个pool都分割完,只是用了最前面两个block,然后freeblock以NULL结尾。那么当我们将要分配第三块block时,会发生什么呢?
if (pool->nextoffset <= pool->maxnextoffset) { /* There is room for another block. */ pool->freeblock = (block*)pool + pool->nextoffset; pool->nextoffset += INDEX2SIZE(size); *(block **)(pool->freeblock) = NULL; UNLOCK(); if (use_calloc) memset(bp, 0, nbytes); return (void *)bp; }
这里的pool实际上就上面我说的pool_header,通过它里面nextoffset的协助,我们继续划出了一块block,同时将freeblock的next置为NULL。
这里就得到证实,在分配block的时候,的确是使用了block的第一个32/64位做了连接这个链表的线索。当然如果想进一步证实,可以自己编译一个python的debug版本,调试一下看看,这里我就先不做了。(哎,又偷懒了)
剩下的问题
- 上面只是介绍了分配和释放对象大小小于512字节的情况,但是大于512字节的要怎么维护的呢?
- 比如一个list对象,刚开始的时候是比较小的,但是随着计算的增加,它是有可能超过512字节的,那么大小超过后会怎么处理呢?
更新
今天看到这篇文章,里面说到二维数组跟二级指针的问题,跟这里的二级指针的使用可以对照一下。
为什么不能将二维数组名传递给二级指针?
假如我们将a赋值给p,p = (int**)a; 既然p是二级指针,那么当然可以这么用:**p; 这样会出什么问题呢?
1)首先看一下p的值,p指向a[0][0],即p的值为a[0][0]的地址;
2)再看一下p的值,p所指向的类型是int,占4字节,根据前面所讲的解引用操作符的过程:从p指向的地址开始,取连续4个字节的内容。得到的正式a[0][0]的值,即0。
3)再看一下**p的值,诶,报错了?当然报错了,因为你访问了地址为0的空间,而这个空间你是没有权限访问的。
备注
如果图片看不清楚的话,我放到了网盘里面,可以去那里看。
https://pan.baidu.com/s/1i50XHOD
- python内存管理
- Python的内存管理
- python 内存管理
- Python的内存管理
- python 的内存管理
- Python的内存管理
- Python的内存管理
- Python 的内存管理
- Python的内存管理
- Python内存管理
- python的内存管理
- Python的内存管理
- python 内存管理
- Python的内存管理
- Python的内存管理
- python的内存管理
- python的内存管理
- python内存管理
- Data Partitioning Guidance
- C 练习实例13 - 水仙花数
- 3.1 读入一个参数
- linux安装redis
- jquery 方法
- python内存管理
- Java和C++访问权限以及多态机制的一些区别
- 省份,城市,县,镇四级select级联
- 剑指Offer系列-面试题10:二进制中1的个数
- GitLab事故之技术详叙
- MTU,MMS
- css属性选择器
- C 练习实例16 - 最大公约数和最小公倍数
- MySQL备份原理详解