Python内存管理知识整理
一切变量皆是对象的引用
当创建对象时, Python 立即向操作系统请求内存
可以用id(变量名)来获取该变量所引用对象的内存地址
>>> a=1 >>> print(id(a)) 56780120
is关键字用于判断引用是否相同,==用于判断引用的内容是否相同
>>> a={‘1‘:1} >>> b={‘1‘:1} >>> a==b True >>> id(a) 44204920L >>> id(b) 45830760L >>> a is b False >>> a="123" >>> b="123" >>> a is b True >>> id(a) 45845320L >>> id(b) 45845320L
在Python中,整数和短小的字符,Python都会缓存这些对象,以便重复使用。当我们创建多个等于“123”的引用时,实际上是让所有这些引用指向同一个对象。
引用计数
当某个对象被创建并赋值给变量时,该对象的引用计数都被设置为1,再次被引用会增加该对象的引用计数,而当对象的引用被销毁,引用计数会减小。
查看一个对象的引用计数:
if __name__ == ‘__main__‘: from sys import getrefcount arr = [4,5,6,7,0,1,2] print(getrefcount(arr)) # 2
使用某个对象的引用作为getrefcount的参数时,此参数实际上创建了一个对象的临时引用,因此getrefcount返回的引用计数是该对象实际的引用计数+1
getrefcount不仅仅统计当前代码块对对象的引用计数,还统计了import模块中对对象的引用计数。在python的内置模块中,可能有很多对数字1的引用,因此
>>> from sys import getrefcount >>> getrefcount(1) 102
一个对象的引用计数变为0后,用户不可能通过任何方式动用这个对象,Python将立即将其释放,并将其占用的内存还给操作系统
引用计数法最主要的缺点在于不能解决对象的循环引用问题
循环引用
注意:只有容器对象才会产生循环引用的情况,比如列表、字典、用户自定义类的对象、元组等。而像数字,字符串这类简单类型不会出现循环引用。
a = { } # 变量a指向对象A,A的引用计数为 1 b = { } # 变量b指向对象B,B的引用计数为 1 a[‘b‘] = b # B的引用计数增1 b[‘a‘] = a # A的引用计数增1 del a # A的引用计数减 1,最后A对象的引用为 1 del b # B的引用计数减 1, 最后B对象的引用为 1
我们已经不能通过任何变量访问到A、B对象,但是由于它们各包含一个对方对象的引用,因此它们的引用计数无法归零,因此不会被回收。如果仅仅使用引用计数法来管理内存,则会因为循环引用造成内存泄露
为了解决对象的循环引用问题,Python引入了标记-清除和分代回收两种GC机制。
标记-清除
https://andrewpqc.github.io/2018/10/08/python-memory-management/
跟其名称一样,该算法在进行垃圾回收时分成了两步,分别是:
- 标记阶段,遍历所有的对象,如果是可达的(reachable),也就是还有对象引用它,那么就标记该对象为可达。
- 清除阶段,再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收。
在标记清除算法中,为了追踪容器对象,需要每个容器对象维护两个额外的指针,用来将容器对象组成一个双端链表,指针分别指向前后两个容器对象,方便插入和删除操作。python解释器(Cpython)维护了两个这样的双端链表,一个链表存放着需要被扫描的容器对象,另一个链表存放着临时不可达对象。
标记阶段
GC第一次遍历所有对象,复制每个对象的引用计数,可以记为gc_ref。假设,每个对象i,该计数为gc_ref_i。Python会遍历所有的对象i。对于每个对象i引用的对象j,将相应的gc_ref_j减1。这一步操作就相当于解除了循环引用对引用计数的影响。
接着,GC第二次遍历所有的容器对象,如果对象的gc_ref值为0,那么这个对象就被标记为unreachable;如果对象的gc_ref不为0,则被标记为reachable,并且会递归地将从该节点出发可以到达的所有节点标记为reachable
被标记为unreachable的对象会被移到Unreachable链表中
清除阶段
回收所有被标记为unreachable的对象
分代回收
在标记-清除算法执行的过程中,需要扫描整个内存空间,应用程序会被暂停,为了提升工作效率,Python采用了分代回收的策略
弱代假说:年轻的对象通常消亡得快,而老对象则很可能存活更长时间。
python将所有对象分为0、1、2三代,他们对应的是3个链表。
所有新建对象都是0代,当某一代对象经历过垃圾回收,依然存活,则被归入下一代。
如果0代经历一定次数的垃圾回收,则会启动对0代和1代的垃圾回收;当1代也经历了一定次数的垃圾回收,则会启动对0、1、2代的垃圾回收
查看gc相关阙值:
>>> import gc >>> print(gc.get_threshold()) (700, 10, 10)
700是被分配的对象与被释放的对象之差(分配内存的数目减去释放内存的数目);后面两个10,表示10次0代垃圾回收后,才会执行一次0、1代的垃圾回收;10次1代垃圾回收后,才会执行一次0、1、2代的垃圾回收
查看gc实时计数情况:
>>> print(gc.get_count()) (562, 10, 0) >>> a={1} >>> print(gc.get_count()) (563, 10, 0) >>> del a >>> print(gc.get_count()) (562, 10, 0) >>> gc.collect() 0 >>> print(gc.get_count()) (22, 0, 0)
562表示距离上一次0代垃圾检查,Python分配内存的数目减去释放内存的数目
10表示距离上次1代垃圾检查,0代垃圾检查的数量
0表示距离上次2代垃圾检查,1代垃圾检查的数量
gc.collect(generation=2) 若被调用时不包含参数,则启动完全的垃圾回收。可以通过generation参数指定启动哪一代的垃圾