python内存管理
引用计数机制
源码分析
想要了解计数机制就要先了解单个对象的结构。
Python里面每一个东西都是对象,他们的核心是一个结构体Py_Object,所有Python对象的头部包含了这样一个结构PyObjec一个是类型标志符,标识这个对象的类型。另一个是计数器,记录当前指向该对象的引用数目,表示这个对象被多少个变量名所引用。
// object.h
struct _object {
Py_ssize_t ob_refcnt; # 引用计数值
struct PyTypeObject *ob_type;
} PyObject;
看一个比较具体点的例子,int型对象的定义:
// intobject.h
typedef struct {
PyObject_HEAD
long ob_ival;
} PyIntObject;
python内部使用引用计数来保持追踪内存中的对象,有多少个对象就有多少个引用计数,当创建对象的同时也会创建一个引用计数。当引用计数为0时,被垃圾回收。
当给一个对象分配一个新名称或者将一个对象放入一个容器(列表、元组或字典)时,该对象的引用计数都会增加。
比如,下面这个例子中,a 的引用计数是 3,因为有 a、b 和作为参数传递的 getrefcount 这三个地方,都引用了一个空列表。
import sys
a = []
b = a
# 注意调用getrefcount()函数会临时增加一次引用计数,得到的结果比预期的多一次
print(sys.getrefcount(a))
输出:3
我们通过一些例子来看下,可以使python对象的引用计数增加或减少的场景。
import sys
a = []
# 两次引用,一次是a,一次是getrefcount()
print(sys.getrefcount(a))
def funA(b):
# 4次调用,a=[],函数调用栈,函数参数,getrefcount()
print(sys.getrefcount(b))
funA(a)
# 两次引用,一次是a,一次是getrefcount()
print(sys.getrefcount(a))
输出:
2
4
2
每当对象被创建或者被引用时,将该对象的引用次数加一。当对象的引用被销毁时,该对象的引用次数减一。当对象的引用次数减到零时,说明程序中已经没有任何对象持有该对象的引用,那么其所占用的空间也就可以被释放。
那么引用计数的具体场景又有哪些呢?
引用计数增加的场景:
对象被创建并赋值给某个变量,比如:a = 'ABC'
变量间的相互引用(相当于变量指向了同一个对象),比如:b=a
变量作为参数传到函数中。比如:ref_method(a),
将对象放到某个容器对象中(列表、元组、字典)。比如:c = [1, a, 'abc']
引用计数减少的场景:
当一个变量离开了作用域,比如:函数执行完成时,执行方法前后的引用计数保持不变,这就是因为方法执行完后,对象的引用计数也会减少
对象的引用变量被销毁时,比如del a或者del b。注意如果del a,再去获取a的引用计数会直接报错。
对象被从容器对象中移除时,比如:c.remove(a)
直接将整个容器销毁时,比如:del c
import sys
def ref_method(str):
print(sys.getrefcount(str))
print("我调用了{}".format(str))
print("方法执行完了")
def ref_count():
print("测试引用计数增加")
a = 'A'
# 注意在函数中生成的引用计数是随机的,所以生成的实际是172+1
print(sys.getrefcount(a))
b = a
print(sys.getrefcount(a))
ref_method(a)
print(sys.getrefcount(a))
c = [1,a,'abc']
print(sys.getrefcount(a))
print("测试引用计数减少")
del b
print(sys.getrefcount(a))
c.remove(a)
print(sys.getrefcount(a))
del c
print(sys.getrefcount(a))
a = 783
print(sys.getrefcount(a))
if __name__ == '__main__':
ref_count()
输出:
测试引用计数增加
173
174
176
我调用了A
方法执行完了
174
175
测试引用计数减少
174
173
173
4
垃圾回收机制
之前我们提到了具体的对象结构,但是这种结构有明显的缺点就是无法解决循环引用的问题。A和B相互引用而再没有外部引用A与B中的任何一个,它们的引用计数都为1,但显然应该被回收
循环引用的示例:
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
为了解决这两个致命弱点,Python又引入了以下两种GC机制(标记-清楚和分代回收)。
标记-清除
『标记清除(Mark—Sweep)』算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。
在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。
标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象
标记-清除是一种周期性策略,相当于是一个定时任务,每隔一段时间进行一次扫描。并且标记-清除工作时会暂停整个应用程序,等待标记清除结束后才会恢复应用程序的运行。所以又创建了一个策略:分代回收
分代回收
分代回收建立标记清除的基础之上,因为我们的标记-清除策略会将我们的程序阻塞。为了减少应用程序暂停的时间,Python 通过“分代回收”(Generational Collection)策略。以空间换时间的方法提高垃圾回收效率。简单来说就是:对象存在时间越长,越可能不是垃圾,应该越少去收集
Python 将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python 将内存分为了 3“代”,分别为年轻代(第 0 代)、中年代(第 1 代)、老年代(第 2 代)。
那什么时候会触发分代回收呢?
import gc
print(gc.get_threshold())
# (700, 10, 10)
# 上面这个是默认的回收策略的阈值
# 也可以自己设置回收策略的阈值
gc.set_threshold(500, 5, 5)
- 700:表示当分配对象的个数达到700时,进行一次0代回收
- 10:当进行10次0代回收以后触发一次1代回收
- 10:当进行10次1代回收以后触发一次2代回收
内存池机制
Python引用了一个内存池(memory pool)机制,即Pymalloc机制,用于对小块内存的申请和释放管理
内存池的概念
当创建大量消耗小内存的对象时,频繁调用new/malloc会导致大量的内存碎片,致使效率降低。内存池的概念就是预先在内存中申请一定数量的,大小相等的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够了之后再申请新的内存。这样做最显著的优势就是能够减少内存碎片,提升效率。
python中内存池是如何工作的
CPython(python解释器)的内存架构图:
-
python的对象管理主要位于Level+1~Level+3层
-
Level+3层:对于python内置的对象(比如int,dict等)都有独立的私有内存池,对象之间的内存池不共享,即int释放的内存,不会被分配给float使用
-
Level+2层:当申请的内存大小小于256KB时,内存分配主要由 Python 对象分配器(Python’s object allocator)实施,像整数的[-5,256]
-
Level+1层:当申请的内存大小大于256KB时,由Python原生的内存分配器进行分配,本质上是调用C标准库中的malloc/realloc等函数
关于释放内存方面,当一个对象的引用计数变为0时,Python就会调用它的析构函数。调用析构函数并不意味着最终一定会调用free来释放内存空间,如果真是这样的话,那频繁地申请、释放内存空间会使Python的执行效率大打折扣。因此在析构时也采用了内存池机制,从内存池申请到的内存会被归还到内存池中,以避免频繁地申请和释放动作。
Python中的内存管理机制——Pymalloc
所以,python中的内存管理有两套机制
一是针对小对象,就是大小小于256kb时,pymalloc会在内存池中申请内存空间。
二是针对大于256kb的对象,会直接执行 new/malloc 的行为来申请新的内存空间。