在 Python 中,内存管理涉及到一个包含所有 Python 对象和数据结构的私有堆(heap)。这个私有堆的管理由内部的 Python 内存管理器(Python memory manager) 保证。Python 内存管理器有不同的组件来处理各种动态存储管理方面的问题,如共享、分割、预分配或缓存。
内存管理机制
python3.6.9 内存管理的官方文档 https://docs.python.org/zh-cn/3.6/c-api/memory.html
Python的内存管理主要是针对“一切皆对象”这个设计理念设计的(这里我们仅针对CPython实现展开说明。)。什么是对象,Object?在Python的世界里对象就是任何分配在堆中的值。
在CPython实现中,对象是struct, PyObject,包含四个部分:
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
} PyObject;
typedef struct {
Python的内存机制以金字塔行,-1,-2层主要有操作系统进行操作,
Python中所有小于256个字节的对象都使用pymalloc实现的分配器,而大的对象则使用系统的 malloc。另外Python对象,如整数,浮点数和List,都有其独立的私有内存池,对象间不共享他们的内存池。也就是说如果你分配又释放了大量的整数,用于缓存这些整数的内存就不能再分配给浮点数。
扩展:在 C 中如果频繁的调用 malloc 与 free 时,是会产生性能问题的.再加上频繁的分配与释放小块的内存会产生内存碎片. Python 在这里主要干的工作有:
如果请求分配的内存在1~256字节之间就使用自己的内存管理系统,否则直接使用 malloc.
这里还是会调用 malloc 分配内存,但每次会分配一块大小为256k的大块内存.
经由内存池登记的内存到最后还是会回收到内存池,并不会调用 C 的 free 释放掉.以便下次使用.对于简单的Python对象,例如数值、字符串,元组(tuple不允许被更改)采用的是复制的方式(深拷贝?),也就是说当将另一个变量B赋值给变量A时,虽然A和B的内存空间仍然相同,但当A的值发生变化时,会重新给A分配空间,A和B的地址变得不再相同
当创建大量消耗小内存的对象时,频繁调用new/malloc会导致大量的内存碎片,致使效率降低。内存池的作用就是预先在内存中申请一定数量的,大小相等的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够之后再申请新的内存。这样做最显著的优势就是能够减少内存碎片,提升效率。
python中的内存管理机制为Pymalloc
1.内存池的工作方式:
2.内存池的优势
对于小对象内存,Python主要提供了三类对象进行管理:arena, pool, block。
Block是Python对象内存分配的最小单位,从8byte(或者16byte)到512byte不等
* Request in bytes 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
pool 是相同分组(大小) block 的集合。一般,pool的大小为4kb(内存分页的大小),这主要是为了方便处理内存的fragmentation。如果一个对象被回收了,内存管理器可以再次利用这部分内存存储其他合适大小的对象。pool的结构如下:
/* Pool for small blocks. */
struct pool_header {
union { block *_padding;
uint count; } ref; /* number of allocated blocks */
block *freeblock; /* pool's free list head */
struct pool_header *nextpool; /* next pool of this size class */
struct pool_header *prevpool; /* previous pool "" */
uint arenaindex; /* index into arenas of base adr */
uint szidx; /* block size class index */
uint nextoffset; /* bytes to virgin block */
uint maxnextoffset; /* largest valid nextoffset */
};
可以看到,每一个pool通过双链表连接在一起,szidx 记录了这个pool下面的block的size,ref.count 记录了已经被使用的blocks,arenaindex 则记录了这个pool所属的 arena 地址。freeblock 是当前可用的第一个block的地址。值得注意的是,如果一个block是空的,那么它会存储下一个空block的地址,这样方便寻址。
每一个pool包含三个状态:
为了提高寻址效率,Python还维护一个array usedpools, 存储不同分组的pool的头地址。如下:
另外,block和pool不会直接分配内存,他们只是维护内存的数据结构,内存是由 arena 进行分配的。
Arena 代表了一片大小为256kb的在堆上的内存空间,每一个 arena 包含 64 个pool。arena的结构如下:
struct arena_object {
uintptr_t address;
block* pool_address;
uint nfreepools;
uint ntotalpools;
struct pool_header* freepools;
struct arena_object* nextarena;
struct arena_object* prevarena;
};
arena也是被双链表连接在一起,ntotalpools和nfreepool记录了目前可用的pool的信息。
高级语言一般都有垃圾回收机制,其中c、c++使用的是用户自己管维护内存的方式,这种方式比较自由,但如果回收不当也会引起垃内存泄露等问题。而python采用的是引用计数机制为主,标记-清理和分代收集两种机制为辅的策略。
从基本原理上,当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如某个新建对象,它被分配给某个引用,对象的引用计数变为1。如果引用被删除,对象的引用计数为0,那么该对象就可以被垃圾回收。比如下面的表:
a = [1, 2, 3]
del a
解析del
del a后,已经没有任何引用指向之前建立的[1,2,3],该表引用计数变为0,用户不可能通过任何方式接触或者动用这个对象,当垃圾回收启动时,Python扫描到这个引用计数为0的对象,就将它所占据的内存清空。
(1)、垃圾回收时,Python不能进行其它的任务,频繁的垃圾回收将大大降低Python的工作效率;
(2)、Python只会在特定条件下,自动启动垃圾回收(垃圾对象少就没必要回收)
(3)、当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(objectdeallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。
我们可以通过gc模块的get_threshold()方法,查看该阈值:
import gc
print(gc.get_threshold())
返回(700, 10, 10),后面的两个10是与分代回收相关的阈值,后面可以看到。700即是垃圾回收启动的阈值。可以通过gc中的set_threshold()方法重新设置。
我们也可以手动启动垃圾回收,即使用gc.collect()。
python中一切皆对象,所以python底层计数结构地就可以抽象为:
pyobject{
引用计数; reference count
引用的对象类型; object type
引用对象的值 value
}
是不是简单明了。现在我们先去考虑一下,什么情况下引用计数+1,什么情况下-1,当引用次数为0时,肯定就是需要进行回收的时刻。
1、对象被创建时,例如 mark="bill"
2、对象被copy引用时,例如 mark2=mark,此时mark引用计数+1
3、对象被作为参数,传入到一个函数中时
4、对象作为一个子元素,存储到容器中时,例如 list=[mark,mark2]
1、对象别名被显示销毁,例如 del mark
2、对象引用被赋予新的对象,例如mark2=mark3,此时mark引用计数-1(对照引用计数+1的情况下的第二点来看)
3、一个函数离开他的作用域,例如函数执行完成,它的引用参数的引用计数-1
4、对象所在容器被销毁,或者从容器中删除。
实例:
import sys
a = "mark"
print(sys.getrefcount(a))
结果:
4
备注:如果实际结果与上面不符,跟使用的编辑器有很大关系,重点是理解计数引用原理,不要太在意为啥不是1.
想理解原因可以转看:https://stackoverflow.com/questions/45021901/why-does-a-newly-created-variable-in-python-have-a-ref-count-of-four
1、简单、直观
2、实时性,只要没有了引用就释放资源。
1、维护引用计数需要消耗一定的资源
2、循环应用时,无法回收。也正是因为这个原因,才需要通过标记-清理和分代收集机制来辅助引用计数机制。
由上面内容我们可以知道,引用计数机制有两个缺点,缺点1还可以勉强让人接受,缺点2如果不解决,肯定会引起内存泄露,为了解决这个问题,引入了标记删除。
Theory of the mark-and-sweep
在标记扫描集合中,收集器首先检查 程序变量;指向的任何内存块都将添加到列表中 要检查的块数。对于该列表中的每个块,它都会设置一个标志 (标记)以表明它仍然是必需的,并且 它已被处理。它还会将任何块添加到列表中 由尚未标记的块指向。这样, 程序可以访问的所有块都被标记。
在第二阶段,收集器扫描所有分配的内存, 搜索尚未标记的块。如果找到任何内容,则 将它们返回到分配器以供重用。
五个内存块,其中三个可从程序变量访问。
在上图中,块 1 可以直接从程序访问 变量,块 2 和 3 是间接可访问的。第4和第5座 程序无法访问。第一步将标记块 1, 并记住块 2 和 3 以供以后处理。第二步 将标记块 2。第三步将标记块 3,但不会 请记住块 2,因为它已经标记。扫描阶段将忽略 块 1、2 和 3,因为它们被标记,但会回收块 4 和 5.
简单标记扫描收集的两个缺点是:
如果系统需要实时或交互式响应,那么简单 标记扫描系列可能不适合目前的情况,但还有更多 复杂的垃圾收集算法由此派生技术。
看个实例,从实例中领会标记删除:
a=[1,2]#假设此时a的引用为1
b=[3,4]#假设此时b的引用为1
#循环引用
a.append(b)#b的引用+1=2
b.append(a)#a的引用+1=2
#假如现在需要删除a,应该如何回收呢?
# 手动触发垃圾回收
# gc.collect()
c=[5,6]#假设此时c的引用为1
d=[7,8]#假设此时d的引用为1
#循环引用
c.append(d)#c的引用+1=2
d.append(c)#d的引用+1=2
#假如现在需要同时删除c、d,应该如何回收呢?
首先我们应该已经知道,不管上面两种情况的哪一个都无法只通过计数来完成回收,因为随便删除一个变量,它的引用只会-1,变成1,还是大于0,不会回收,为了解决这个问题,开始看标记删除来大展神威吧。
python标记删除时通过l两个容器来完成的:死亡容器、存活容器。
首先,我们先来分析情况2,删除c、d
删除后,c的引用为1,d的引用为1,根据引用计数,还无法删除
标记删除第一步:对执行删除操作后的每个引用-1,此时c的引用为0,d的引用为0,
把他们都放到死亡容器内。把那些引用仍然大于0的放到存活容器内。
标记删除第二步:遍历存活容器,查看是否有的存活容器引用了死亡容器内的对象,
如果有就把该对象从死亡容器内取出,放到存活容器内。
由于c、d都没有对象引用他们了,所以经过这一步骤,他们还是在死亡组。
标记删除第三部:将死亡组所有对象删除。
这样就完成了对从c、d的删除。
同样道理,我们来分析:只删除a的过程:
综上所说,发现对于循环引用,必须将循环引用的双发对象都删除,才可以被回收。
经过上面的【标记-清理】方法,已经可以保证对垃圾的回收了,但还有一个问题,【标记-清理】什么时候执行比较好呢,是对所有对象都同时执行吗?
同时执行很显然不合理,我们知道,存活越久的对象,说明他的引用更持久(好像是个屁话,引用不持久就被删除了),为了更合理的进行【标记-删除】,就需要对对象进行分代处理,思路很简单:
Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。
分代(generation)回收的策略
新创建的对象做为0代
2、执行一个【标记-删除】,存活的对象代数就+1
3、代数越高的对象(存活越持久的对象),进行【标记-删除】的时间间隔就越长。这个间隔,江湖人称阀值。
gc扫描次数(第0代>第1代>第2代)
当某一代中被分配的对象与被释放的对象之差达到某一阈值时,就会触发当前一代的gc扫描。当某一代被扫描时,比它年轻的一代也会被扫描,因此,第2代的gc扫描发生时,第0,1代的gc扫描也会发生,即为全代扫描。
>>> import gc
>>> gc.get_threshold() ## 分代回收机制的参数阈值设置
(700, 10, 10)
同样可以用set_threshold()来调整,比如对2代对象进行更频繁的扫描。
import gc
gc.set_threshold(700, 10, 5)
1、调用gc.collect() 2、GC达到阀值时 3、程序退出时
由于整数使用广泛,为了避免为整数频繁销毁、申请内存空间,引入了小整数对象池。[-5,257)是提前定义好的,不会销毁,单个字母也是。
那对于其他整数,或者其他字符串的不可变类型,如果存在重复的多个,例如:
a1=“mark” a2=“mark” a3=“mark” a4=“mark” … a1000=“mark”
如果每次声明都开辟出一段空间,很显然不合理,这个时候python就会使用intern机制,靠引用计数来维护。
总计:
1、小整数[-5,257):共用对象,常驻内存
2、单个字符:共用对象,常驻内存
3、单个单词等不可变类型,默认开启intern机制,共用对象,引用计数为0时销毁。
gc.collect 源码解读
collect 是垃圾回收的核心函数,理解的这个函数,也就理解了整个垃圾回收机制流程。
static Py_ssize_t collect(int generation)
{
int i;
Py_ssize_t m = 0; /* # objects collected */
Py_ssize_t n = 0; /* # unreachable objects that couldn't be collected */
PyGC_Head *young; /* the generation we are examining */
PyGC_Head *old; /* next older generation */
PyGC_Head unreachable; /* non-problematic unreachable trash */
PyGC_Head finalizers; /* objects with, & reachable from, __del__ */
PyGC_Head *gc;
double t1 = 0.0;
...
/* update collection and allocation counters */
if (generation+1 < NUM_GENERATIONS)
generations[generation+1].count += 1;
for (i = 0; i <= generation; i++)
generations[i].count = 0;
/* 上面提到的,如过回收的不是最幼代,会把比它年轻的代都合并过来 */
for (i = 0; i < generation; i++) {
gc_list_merge(GEN_HEAD(i), GEN_HEAD(generation));
}
/* young 指向当前要回收的代
* old 指向比 young 老一代的代,如果 yound 已经是最老,那么和 yound 相等 */
young = GEN_HEAD(generation);
if (generation < NUM_GENERATIONS-1)
old = GEN_HEAD(generation+1);
else
old = young;
/* 垃圾回收流程的第一步:设置对象的 gc_refs 为 ob_refcnt */
update_refs(young);
/* 垃圾回收流程的第二步 1:将每个 reachable 的对象的 gc_refs 减 1 */
subtract_refs(young);
/* 初始化 unreachable 链表 */
gc_list_init(&unreachable);
/* 垃圾回收流程的第二步 2:将所有 unreachable 的对象移动到 unreachable 链表
* 注意:流程中是说将 reachable 的对象移动单独集合,这是老的实现方式,并没有错,只是后来发现
* unreachable 的对象往往对于 unreachable 的,移动 unreachable 更有效率 */
move_unreachable(young, &unreachable);
/* Move reachable objects to next generation. */
if (young != old) {
if (generation == NUM_GENERATIONS - 2) {
long_lived_pending += gc_list_size(young);
}
gc_list_merge(young, old);
}
else {
/* We only untrack dicts in full collections, to avoid quadratic
dict build-up. See issue #14775. */
untrack_dicts(young);
long_lived_pending = 0;
long_lived_total = gc_list_size(young);
}
/* 初始化 finalizers 链表,用于存放带有析构方法的对象 */
gc_list_init(&finalizers);
/* unreachable 的对象本来是可以回收的,但是如果对象定义了析构方法,就要放弃回收
* 并将对象移到 finalizers 链表 */
move_finalizers(&unreachable, &finalizers);
/* finalizers 现在链接了所有 unreachable 但是带有析构方法的对象,被这些对象引用的
* 其它 unreachable 对象也不能回收,将它们也移动 finalizers 链表 */
move_finalizer_reachable(&finalizers);
/* 统计可回收对象的数量,打印 debug 信息 */
for (gc = unreachable.gc.gc_next; gc != &unreachable; gc = gc->gc.gc_next) {
m++;
if (debug & DEBUG_COLLECTABLE) {
debug_cycle("collectable", FROM_GC(gc));
}
}
/* 处理弱引用对象,这里暂不深入 */
m += handle_weakrefs(&unreachable, old);
/* unreachable 链表剩下的对象都是可回收的了,调用这些对象的 tp_clear 方法进行垃圾回收,
* 循环引用在这里被打破 */
delete_garbage(&unreachable, old);
/* Collect statistics on uncollectable objects found and print
* debugging information. */
for (gc = finalizers.gc.gc_next;
gc != &finalizers;
gc = gc->gc.gc_next) {
n++;
if (debug & DEBUG_UNCOLLECTABLE)
debug_cycle("uncollectable", FROM_GC(gc));
}
/* debug */
...
/* finalizers 里的 unreachable 对象往上移一代 */
(void)handle_finalizers(&finalizers, old);
/* freelist 是一些类型下面(如 tupe )为了避免每次创建新对象都要申请内存而缓存的一些
* 空对象,当 GC 回收的是最老的代时,这些类型下的 freelist 里的对象会也被释放(不是全部
* 释放,tuple 下的逻辑是保留一个)*/
if (generation == NUM_GENERATIONS-1) {
clear_freelists();
}
...
return n+m;
}
https://zhuanlan.zhihu.com/p/150032487
https://docs.python.org/zh-cn/3.6/c-api/memory.html
https://www.cnblogs.com/TMesh/p/11731010.html
https://zhuanlan.zhihu.com/p/65839740
https://www.cnblogs.com/shengulong/p/10143856.html
https://www.zhihu.com/question/32373436/answer/549698608
https://www.memorymanagement.org/mmref/index.html#mmref-intro
https://www.geeksforgeeks.org/mark-and-sweep-garbage-collection-algorithm/
https://www.cnblogs.com/workherd/p/14199648.html
https://zhuanlan.zhihu.com/p/65838548
https://github.com/python/cpython/blob/ad051cbce1360ad3055a048506c09bc2a5442474/Objects/obmalloc.c#L534