python的内存管理算法与优化

python的内存管理算法与优化

前期准备

  1. 我们可以用python的gc模块控制python的内存管理和回收
    • gc.disable()# 暂停自动垃圾回收
    • gc.collect()# 执行完整的垃圾回收,返回无法到达的对象的数量
    • gc.set_threshold()# 设置垃圾回收的阈值
    • gc.set_debug()# 设置垃圾回收的调试标记. 调试信息会被写入std.err.
  2. sys跟objgraph库

python内存管理算法

python的内存管理机制有两种:引用计数和分代垃圾回收

引用次数
  1. 引用计数+1的情况
    对象被创建 a=‘123’
    对象被引用 b=a
    对象被当作参数传入函数 fun(a)
    对象最为元素存储到容器中 c={a:’1’}
  2. 引用计数-1的情况
    对象的别名被显式销毁 del b
    对象的别名被赋予其它值 b=1
    对象离开它的作用域,比如函数执行完毕后,函数里面的局部变量的引用计数-1
    对象从容器中删除,或者所在的容器被销毁 del c
  3. 引用计数的优点
    高效
    回收内存的时间是分布的,引用计数为0马上回收,不会给系统造成停顿
    对象生命周期明确
    容易实现
  4. 引用计数缺点
    额外空间维护引用计数
    无法解决循环引用的情况
    循环引用的例子
a=[1]
b=[2]
a.append(b)
b.append(a)


del a
del b

#del a del b只是把引用计数-1,del后ab原来所指的对象的引用计数为1 无法进行资源回收,但也无法访问


  1. 查看引用计数的方法
    sys.getrefcount()
    objgraph.count()
垃圾回收机制

python的垃圾回收机制就是为了解决循环引用的问题
python的垃圾回收机制分为mark-sweep算法和分代(generational)算法

  1. 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找出来的部分进行回收

  1. 分代(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),所以消耗会很大,我们可以在程序层面进行一些调优

调优方式
  1. 手动垃圾回收
  • 关闭自动回收gc.disable()
  • 合适的时候用gc.collect()触发垃圾回收,比如打游戏过程中不进行垃圾回收,在用户等待或游戏结算的时候再出发垃圾回收
  1. 提高垃圾回收阈值
    通过gc.set_threshold()设置回收阈值,减少垃圾回收的次数

  2. 避免循环引用
    整个垃圾回收机制都是为了解决循环引用的问题,如果代码能保证没有循环引用问题,则可以直接关闭垃圾回收

常见手段

  • 手动解循环引用
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

发布了51 篇原创文章 · 获赞 7 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_36267931/article/details/102753869