python的内存管理算法与优化
前期准备
- 我们可以用python的gc模块控制python的内存管理和回收
- gc.disable()# 暂停自动垃圾回收
- gc.collect()# 执行完整的垃圾回收,返回无法到达的对象的数量
- gc.set_threshold()# 设置垃圾回收的阈值
- gc.set_debug()# 设置垃圾回收的调试标记. 调试信息会被写入std.err.
- sys跟objgraph库
python内存管理算法
python的内存管理机制有两种:引用计数和分代垃圾回收
引用次数
- 引用计数+1的情况
对象被创建 a=‘123’
对象被引用 b=a
对象被当作参数传入函数 fun(a)
对象最为元素存储到容器中 c={a:’1’} - 引用计数-1的情况
对象的别名被显式销毁 del b
对象的别名被赋予其它值 b=1
对象离开它的作用域,比如函数执行完毕后,函数里面的局部变量的引用计数-1
对象从容器中删除,或者所在的容器被销毁 del c - 引用计数的优点
高效
回收内存的时间是分布的,引用计数为0马上回收,不会给系统造成停顿
对象生命周期明确
容易实现 - 引用计数缺点
额外空间维护引用计数
无法解决循环引用的情况
循环引用的例子
a=[1]
b=[2]
a.append(b)
b.append(a)
del a
del b
#del a del b只是把引用计数-1,del后ab原来所指的对象的引用计数为1 无法进行资源回收,但也无法访问
- 查看引用计数的方法
sys.getrefcount()
objgraph.count()
垃圾回收机制
python的垃圾回收机制就是为了解决循环引用的问题
python的垃圾回收机制分为mark-sweep算法和分代(generational)算法
- mark-sweep算法
分为mark(标记)和sweep(清除)两部分
python中所有能够引用其它对象的对象都叫做container(容器),只有container之间才会出现循环引用
把所有的容器都放到一个双向链表中,使用双向链表是为了方便快速插入删除对象
mark部分具体的操作如下
- 每个容器设置一个gc_ref,并初始化为该容器的引用计数值ob_ref
- 对每个容器,找到它引用的所有对象,将被引用对象的gc_ref-1
- 对所有容器执行完上述操作后,所有gc_ref不为0的容器则还存在被引用的情况,不能销毁,把他们放到另一个集合A
- 上一个操作中的集合A中,他们所引用的对象也是不能释放的,也放进集合A中
- 剩下的不在集合A里面的则可以进行回收
需要回收的内存就是存在循环引用的容器,循环引用的容器集合会成为一个孤岛,外部没有办法访问到,mark部分的原理其实就是模拟了一次容器自身的释放,这样就可以打破循环引用的容器集合中互相依赖的情况
sweep部分可以略过,就是对mark找出来的部分进行回收
- 分代(generational)算法
python根据容器的活跃程度把容器分为三代:0代、1代、2代,每一代都是一个由双向链表实现的容器集合
上述的mark-sweep算法其实并非每次都对所有容器都进行标记清除,而是每次对同一代的容器进行标记清除
给容器分代的原因—弱代假说
- 弱代假说的观点:年轻的对象更快销毁,年老的对象可能存活更长时间,比如局部变量跟全局变量的对比
- 如果不用分代算法,每次都对所有容器进行mark-sweep,实际上有一部分容器还没到达销毁时间,我们不希望这部分容器被频繁地执行算法,所以有了分代算法
分代算法的过程跟触发每一代的规则
- 每当容器被创建时,python把它加入0代链表中
- 0代:当被分配的对象的数量减去被释放对象后的差值大于设置的threshold0时,启动0代中的mark-sweep算法,产生的不被销毁的容器集合并入1代链表中
- 1代:当0代启动mark-sweep算法的次数大于设置的threshold1时,启动1代中的mark-sweep算法,产生的不被销毁的容器集合并入2代链表中
- 2代:当1代启动mark-sweep算法的次数大于设置的threshold2时,启动2代中的mark-sweep算法
通过gc.set_threshold()可以设置threshold0、threshold1、threshold2的值
python内存管理优化
每一次python进行垃圾回收,都要对所有的容器进行两次遍历(第一次设置gc_ref值,第二次让gc_ref-1),所以消耗会很大,我们可以在程序层面进行一些调优
调优方式
- 手动垃圾回收
- 关闭自动回收gc.disable()
- 合适的时候用gc.collect()触发垃圾回收,比如打游戏过程中不进行垃圾回收,在用户等待或游戏结算的时候再出发垃圾回收
-
提高垃圾回收阈值
通过gc.set_threshold()设置回收阈值,减少垃圾回收的次数 -
避免循环引用
整个垃圾回收机制都是为了解决循环引用的问题,如果代码能保证没有循环引用问题,则可以直接关闭垃圾回收
常见手段
- 手动解循环引用
class A(object):
def __init__(self):
self.child = None
def destroy(self):
self.child = None
class B(object):
def __init__(self):
self.parent = None
def destroy(self):
self.parent = None
def test3():
a = A()
b = B()
a.child = b
b.parent = a
a.destroy()
b.destroy()
test3()
print 'Object count of A:', objgraph.count('A’) #0
print 'Object count of B:', objgraph.count('B’) #0
- 使用弱引用,python自带的弱引用库weakref 弱引用相关参考:https://yuerblog.cc/2018/08/28/python-weakref-real-usage/
def test4():
a = A()
b = B()
a.child = weakref.ref(b)
b.parent = weakref.ref(a)
test4()
print 'Object count of A:', objgraph.count('A’) #0
print 'Object count of B:', objgraph.count('B’) #0
内存泄露
有了引用计数和垃圾回收,python仍然有可能发生内存泄露,发生的情况如下
- 对象被另一个生命周期特别长的对象所引用,比如网络服务器,可能存在一个全局的单例ConnectionManager,管理所有的连接Connection,如果当Connection理论上不再被使用的时候,没有从ConnectionManager中删除,那么就造成了内存泄露。
- 循环引用的对象中定义了__del__函数,如果定义了这个函数,python无法判断析构对象的顺序,因此会不做处理
参考
http://www.doc88.com/p-78747715867.html
http://kkpattern.github.io/2015/06/20/python-memory-optimization-zh.html
https://blog.csdn.net/xiongchengluo1129/article/details/80462651
https://www.cnblogs.com/xybaby/p/7491656.html