一切变量皆是对象的引用
当创建对象时, 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的参数时,此参数实际上创建了一个对象的临时引用,在Python当中,所有的传参都是引用传递。因此getrefcount返回的引用计数是该对象实际的引用计数+1
getrefcount不仅仅统计当前代码块对对象的引用计数,还统计了import模块中对对象的引用计数。在python的内置模块中,可能有很多对数字1的引用,因此
>>> from sys import getrefcount
>>> getrefcount(1)
102
一个对象的引用计数变为0后,用户不可能通过任何方式动用这个对象,但是,此对象占用的内存并不会立即被回收,因为python在执行垃圾回收时会暂停其他所有任务,因此垃圾回收只会在必要的时候执行
可以使用gc.get_count()查看gc实时计数情况验证这一点
>>> gc.get_count()
(560, 10, 0)
560表示距离上一次0代垃圾检查,Python分配内存的数目减去释放内存的数目
10表示距离上次1代垃圾检查,0代垃圾检查的数量
0表示距离上次2代垃圾检查,1代垃圾检查的数量
>>> gc.get_count()
(560, 10, 0)
>>> a=[1,2,3]
>>> id(a)
23419440
>>> gc.get_count()
(561, 10, 0)
// 为对象[1,2,3]分配了内存
>>> del a
>>> gc.get_count()
(561, 10, 0)
// 删除引用a后,并没有立即回收[1,2,3]占用的内存
>>> a=[1,2,3]
>>> gc.get_count()
(561, 10, 0)
>>> id(a)
23419440
// 没有重新创建[1,2,3]对象,因此分配内存的数目没有增加
// a再次引用了之前没有被回收的对象[1,2,3],它们的内存地址是一样的
>>> a=[1,2,3,4]
>>> gc.get_count()
(562, 10, 0)
// 为对象[1,2,3,4]分配了内存,[1,2,3]引用计数归零,但还是没有回收[1,2,3]占用的内存
引用计数法最主要的缺点在于不能解决对象的循环引用问题
循环引用
注意:只有容器对象才会产生循环引用的情况,比如列表、字典、用户自定义类的对象、元组等。而像数字,字符串这类简单类型不会出现循环引用。
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代的垃圾回收
手动触发垃圾回收:
>>> print(gc.get_count())
(562, 10, 0)
>>> a={1}
>>> print(gc.get_count())
(563, 10, 0)
>>> gc.collect()
0
>>> print(gc.get_count())
(22, 0, 0)
gc.collect(generation=2)
若被调用时不包含参数,则启动完全的垃圾回收。可以通过generation参数指定启动哪一代的垃圾