内存管理机制
可变对象/不可变对象
- 可变对象:如列表、字典等,本质是不论怎么改变值,他的地址都不会发生改变。
- 不可变对象:如int、float、bool、str、元组等,当你重新赋值时,在底层他将会修改指向的数据,而不会对原来指向的值进行修改,对原来的值修改的仅时引用次数减一
内存分配原则
python中一切皆对象,在底层中每个对象都是基于结构体实现的,而这些结构体里都有着上下指向、引用计数和数据类型的属性,在初始化时都会给数据的引用计数设置为1,每当多一个指向时,其引用计数就加一,每删除一个他的指向,就引用计数减一,当为0时就会对该数据进行垃圾回收
缓存机制
在python的内存管理当中可能存在一些缓存机制(如:int、float、str、list等都有),即:将某个数据删除时,其可能不会将这个对象完全销毁,而是将对象存放到一个链表中,当又创建同类型的对象时,将会直接赋值给缓存的同类型对象,再通过变量引用指向他,举例:
>>> a = "xxx"
>>> id(a)
2436976108520
>>> del a
# 删除了字符串a
>>> b = "yyy"
# 创建字符串b
>>> id(b)
# 可以发现内存地址时一样的
2436976108520
垃圾回收机制
对象引用次数
一般情况下垃圾回收基于对象引用次数,当初始化时次数为1,被其他对象引用时加1,使用del
本质则是将引用次数减一,而当引用次数变成0以后则会自动触发垃圾回收机制将其回收,代码层面上则是会调用该对象的__del__
方法,举例:
class D(dict):
def __init__(self, name):
self.name = name
print("对象:{}被创建!".format(self.name))
def __del__(self):
print("对象:{}被销毁!".format(self.name))
a = D('a')
a = D('b')
print("程序结束!")
结果:
对象:a被创建!
对象:b被创建!
对象:a被销毁!
程序结束!
对象:b被销毁!
可以看出第一句实例化A时对象被创建,对象的引用计数初始化为1,当第二句执行时,新的A对象被创建,新的对象引用计数为1,而旧的A对象因为a指向了其他数据,所以引用次数减一,此时旧的A对象引用次数变成0,触发销毁机制,从而自动调用了__del__
方法。当程序结束时,因为要回收内存,因此新的对象A也自动调用__del__
方法。
查看引用次数
可以用sys.getrefcount()
方法来查看引用次数,要注意因为将内容传入该方法时引用也会加1,所以我们实际想知道的引用次数应该是输出的结果减一,举例:
>>> a = 1000
>>> sys.getrefcount(a)
# 加上传入方法的a,引用次数为2
2
>>> b = a
>>> sys.getrefcount(a)
# 因为b也引用了a,所以引用次数加1
3
>>> del b
>>> sys.getrefcount(a)
# 删除了b以后引用次数减1
2
标记清除
前面的引用计数能够解决一般情况下的内存回收问题,但是对于循环引用的情况,可能就会无法回收,从而造成内存泄漏的问题,例如下面代码:
class D(dict):
def __init__(self, name):
self.name = name
print("对象:{}被创建!".format(self.name))
def __del__(self):
print("对象:{}被销毁!".format(self.name))
a = D('a')
b = D('b')
a['x'] = b
b['x'] = a
a = 1
b = 1
print("程序结束!")
结果:
对象:a被创建!
对象:b被创建!
程序结束!
对象:a被销毁!
对象:b被销毁!
可以看到上面的两个字典类因为互相指向了,所以即使销毁了,引用计数也永远大于0,此时垃圾回收机制也就不起作用了,所以之后即使a和b都指向了其他值,但因为他们原先指向的字典类互相有指向,引用计数不为0,导致他们直到程序结束内存才被回收。此时如果想要回收,那么就需要先收集垃圾,然后再进行回收,Python提供了gc.collect()
方法用于手动回收数据,举例:
import gc
class L(list):
def __del__(self):
print(self, "end")
a = L([1,2,3])
b = L([1,2,a])
a[-1] = b
del a, b
# 手动回收第0代(后面会介绍分代回收,总共有3代)
print("gc generation 0 nums:", gc.collect(0))
print("end")
# [1, 2, [1, 2, [...]]] end
# [1, 2, [1, 2, [...]]] end
# gc generation 0 nums: 2
# end
可以看到两个互相引用的对象被回收了,而这种手动回收的方式就基于了标记清除来实现:
- 首先GC会对所有活动的对象打上标记,即一个个点,然后他们之间的引用通过指向来表明,此时就构成了一个有向图
- 然后GC会从根对象出发,沿着有向边遍历整个图,而对于不可达的对象,那么就被视为需要清理的垃圾对象。
分代回收
建立在垃圾清除的基础上,其将对象的活动时间分为3代,新生的对象在0代,如果他们在第0代中能够存活下来,就会被放入1代里,当在1代中也存活了下来,再被放到2代,默认当对象数量减去释放的对象数量(即当前可达的对象数量)超过700时将会对0代对象进行回收处理,当进行了10次0代回收则会触发1代回收,当进行了10次1代回收则会触发2代回收,这些配置可以通过gc.get_threshold()
方法获取,并通过gc.set_threshold()
自定义,举例:
>>> gc.get_threshold()
(700, 10, 10)
>>> gc.set_threshold(500, 5, 3)
>>> gc.get_threshold()
(500, 5, 3)
这里我们再来对前面循环引用的情况通过分代回收来查看效果,首先由于默认的设置里是需要对象数量减去释放数量超过700时才会触发,而这里我们使用的对象示例较少,所以需要我们调整这个触发的阈值,然后为了更加明显地看出回收的步骤,这里也重写了__new__
方法,代码如下:
import gc
gc.set_threshold(2, 10, 10)
# 第一个参数代表,如果设置为0代表禁用,这里设置2,代表第0代超过2个对象时触发垃圾回收
# 后面两个是对第一代和第二代的进行回收,这里只要大于1就行了
# 等于1的话那么会不停触发对1/2代的回收,从而导致对第0代的回收失败
class D(dict):
def __new__(self, name):
print("对象:{}被分配!".format(name))
return dict.__new__(self)
def __init__(self, name):
self.name = name
print("对象:{}被创建!".format(self.name))
def __del__(self):
print("对象:{}被销毁!".format(self.name))
print("初始时的垃圾回收计数器:", gc.get_count())
a = D('a')
b = D('b')
print("创建了两个对象时的回收计数器:", gc.get_count())
a['x'] = b
b['x'] = a
a = 1
b = 2
print("修改了两个对象时的垃圾回收计数器:", gc.get_count())
c = D('c')
# 分配空间给C时,可以看到触发了第0代的回收
print("新分配空间给对象C时的垃圾回收计数器:", gc.get_count())
print("程序结束!")
结果:
初始时的垃圾回收计数器: (0, 8, 1)
对象:a被分配!
对象:a被创建!
对象:b被分配!
对象:b被创建!
创建了两个对象时的回收计数器: (2, 8, 1)
修改了两个对象时的垃圾回收计数器: (2, 8, 1)
对象:c被分配!
对象:a被销毁!
对象:b被销毁!
对象:c被创建!
新分配空间给对象C时的垃圾回收计数器: (0, 9, 1)
程序结束!
对象:c被销毁!
可以看出在我们的主要代码跑起前已经进行过8次1代和1次2代的垃圾回收了,当创建了两个对象以后,0代增加了2个,修改了这两个对象的指向后,计数器看起来还是2个a和b,但是实际上因为原来的两个字典循环引用导致未被释放,所以实际有4个,只是有2个是不可达的,因此在给对象c分配空间时计数器增加1变成3,因为超过了2,需要进行一次对0代的垃圾回收,因此a和b这两个不可达的就被销毁,然后再创建对象c,最终程序结束,将未被释放的对象a、b和c都销毁
更多参考:
https://blog.csdn.net/it_yuan/article/details/52850270
https://www.jb51.net/article/79306.htm
https://www.jianshu.com/p/0c37059ce224
https://testerhome.com/topics/16556
弱引用
当引用某个数据时,引用计数不会加一,假如有些数据被删除后,希望直接被垃圾回收,就可以利用弱引用来实现,举例:
import weakref
s = {1,2,3}
w = weakref.ref(s)
print(w())
s.remove(1)
print(w())
del s
print(w())
# {1, 2, 3}
# {2, 3}
# None
弱引用集合
- 示例1:
import weakref
class A: pass
class B: pass
s = {1,2,3}
w = weakref.WeakSet()
a = A()
b = B()
w.add(a)
w.add(b)
print(w.data)
del a
print(w.data)
# {<weakref at 0x000002105CAE38B8; to 'A' at 0x000002105C83A2B0>, <weakref at 0x000002105CAE3778; to 'B' at 0x000002105C9494E0>}
# {<weakref at 0x000002105CAE3778; to 'B' at 0x000002105C9494E0>}
- 示例2:
import weakref
class A:
def __del__(self):
print("对象A被删除!")
a = A()
# b是a的引用
b = a
# c是a的弱引用
c = weakref.ref(a)
# 创建一个弱引用集合
s = weakref.WeakSet()
# 往集合当中添加一个对a的弱引用
s.add(a)
print("a的弱引用:", weakref.getweakrefs(a), "数量:", weakref.getweakrefcount(a))
del a
print("c指向的对象:", c())
del b
print("c指向的对象:", c())
# a的弱引用: [<weakref at 0x000001F8C66FB548; to 'A' at 0x000001F8C66FA1D0>, <weakref at 0x000001F8C6994778; to 'A' at 0x000001F8C66FA1D0>] 数量: 2
# c指向的对象: <__main__.A object at 0x000001F8C66FA1D0>
# 对象A被删除!
# c指向的对象: None
弱引用字典
import weakref
class A: pass
a = A()
b = A()
w = weakref.WeakValueDictionary()
# w = {}
# 将w改成字典,则会发现a没有被回收
w["a"] = a
w["b"] = b
print(list(w.keys()))
del a
print(list(w.keys()))
# ['a', 'b']
# ['b']
可以看到将a
删除以后,弱引用字典里的a
也被删除,从而起到一个类似缓冲的作用