Python内存管理知识整理

wangqing 2020-05-19

一切变量皆是对象的引用

当创建对象时, 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/

跟其名称一样,该算法在进行垃圾回收时分成了两步,分别是:

  1. 标记阶段,遍历所有的对象,如果是可达的(reachable),也就是还有对象引用它,那么就标记该对象为可达。
  2. 清除阶段,再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收。

在标记清除算法中,为了追踪容器对象,需要每个容器对象维护两个额外的指针,用来将容器对象组成一个双端链表,指针分别指向前后两个容器对象,方便插入和删除操作。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参数指定启动哪一代的垃圾

参考资料

gc --- 垃圾回收器接口

Python中的垃圾回收机制

聊聊python的内存管理

python内存分配

Python深入06 Python的内存管理

画说 Ruby 与 Python 垃圾回收

相关推荐