python内存管理
1. 引用和对象
我们先看这样一个赋值语句 a=1
在 python 中,整数 1 为一个对象,而 a 是一个引用 a→1
Python是动态类型的语言(动态类型),对象与引用分离。Python通过引用来操作对象。
在Python中,整数和短小的字符,Python都会缓存这些对象,以便重复使用。
# id()是Python内置的函数,它用于返回对象的身份(identity),也就是内存地址。
# hex 返回一个数的十六进制表示
a = 1
b = 1
print(id(1))
print(id(a))
print(id(b))
print(hex(id(a)))
1644917568
1644917568
1644917568
0x620b7340
从上边代码的输出可以看出,a 和 b 其实是指向同一个对象的两个引用。
我们现在使用 is 来检测一下,整数和短小的字符指的是什么。
a = 1
b = 1
print(a is b)
True
a = 'good'
b = 'good'
print(a is b)
True
a = 'good night'
b = 'good night'
print(a is b)
print(a[:4] is 'good')
print(a[:4] is b[:4])
False
False
False
a = [1]
b = [1]
print(a is b)
False
可以看到,python缓存了整数和短小的字符,所以指向它们的引用都是指向的缓存好的对象。
对于较长的字符,则是另外分配的内存,即使是相同的字符串的引用,由于它们指向的对象是另外分配的内存,它们的内存地址也是不同的。
a = 'good night'
b = 'good night'
print(hex(id(a)))
print(hex(id(b)))
0x19b922e1070
0x19b922e17b0
在 Python 中,每个对象都有指向该对象的引用计数。我们可以使用 sys.getrefcount() 来查看某个对象的引用计数,参数传递给 getrefcount()时,会创建一个临时的引用,所以 getrefcount() 会比期望结果多 1。
import sys
a = [1, 2]
sys.getrefcount(a)
2
Python 中的容器对象,如 list、tuple、dict 等,可以包含多个对象。实际上,它们包含的只是对象的引用而已。
a = [1, 2]
b = [a]
print(b)
[[1, 2]]
a[0] = -1
print(b)
[[-1, 2]]
使用 del 可以删除一个引用,同时也会减少相应对象的引用计数。此外,将引用指向别的对象也会使引用计数减少。
a = [1]
b = a
c = b
print( sys.getrefcount(c) )
del a # del 删除变量,减少引用计数
print( sys.getrefcount(c) )
b = None # 指向别的对象,减少引用计数
print( sys.getrefcount(c) )
4
3
2
两个对象还可以相互引用,这样会形成一个引用环。
a = [1]
b = [a]
a.append(b)
print(a)
[1, [[...]]]
一个对象也有可能会形成引用环。
a = [1]
a.append(a) # 如果是 a = [a],则不会形成引用环,此时 a 为 [[1]]
print(a)
[1, [...]]
2.垃圾回收
当Python中的对象越来越多,它们将占据越来越大的内存,这个时候就需要垃圾回收(Garbage Collection)了。当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。
不过,垃圾回收时,Python不能进行其它的任务。频繁的垃圾回收将大大降低Python的工作效率。如果内存中的对象不多,就没有必要总启动垃圾回收。所以,Python只会在特定条件下,自动启动垃圾回收。当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。我们可以通过gc模块的get_threshold()方法,查看该阈值:
import gc
print(gc.get_threshold())
(700, 10, 10)
后面的两个10是与分代回收相关的阈值。700即是垃圾回收启动的阈值,可以通过 gc.set_threshold()
方法重新设置。另外,还可以手动回收gc.collect()
另外,引用环会给 GC 带来很大的麻烦。对于上面提到的一个对象引用环的情况,即使删除了引用 a ,list 对象的引用计数也不会为0。
Python 采用了分代回收的策略。这一策略的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。我们的程序往往会产生大量的对象,许多对象很快产生和消失,但也有一些对象长期被使用。出于信任和效率,对于这样一些“长寿”对象,我们相信它们的用处,所以减少在垃圾回收中扫描它们的频率。
Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。
这两个次数即上面get_threshold()返回的(700, 10, 10)返回的两个10。也就是说,每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收。
另外,引用环会给 GC 带来很大的麻烦。
a = [1]
a.append(a) # 如果是 a = [a],则不会形成引用环,此时 a 为 [[1]]
print(a)
[1, [...]]
对于上面提到的一个对象引用环的情况,即使删除了引用 a ,list 对象的引用计数也不会为0,不会被垃圾回收。
为了回收这样的引用环,Python复制每个对象的引用计数,可以记为 gc_ref。假设,每个对象 i,该计数为 gc_ref_i。Python 会遍历所有的对象 i。对于每个对象 i 引用的对象 j,将相应的 gc_ref_j 减 1。在结束遍历后,gc_ref 不为0的对象,和这些对象引用的对象,以及继续更下游引用的对象,需要被保留。而其它的对象则被垃圾回收。
下面例子中 a 指向的对象的引用计数为 2(=3-1),在去除引用 a 后变为 1,由于对象引用了本身,所以它的引用计数应该减一,即 gc_ref = 0,这表明改对象应该被垃圾回收。
a = [1]
a.append(a) # 如果是 a = [a],则不会形成引用环,此时 a 为 [[1]]
print(a)
print(sys.getrefcount(a))
[1, [...]]
3