from:http://kkpattern.github.io/2015/06/20/python-memory-optimization-zh.html

准备工作

为了方便解释Python的内存管理机制, 本文使用了gc模块来辅助展示内存中的Python对象以及Python垃圾回收器的工作情况. 本文中具体使用到的接口包括:

gc.disable()  # 暂停自动垃圾回收. gc.collect()  # 执行一次完整的垃圾回收, 返回垃圾回收所找到无法到达的对象的数量. gc.set_threshold()  # 设置Python垃圾回收的阈值. gc.set_debug()  # 设置垃圾回收的调试标记. 调试信息会被写入std.err. 

完整的gc模块文档可以参看这里.

同时我们还使用了objgraphPython库, 本文中具体使用到的接口包括:

objgraph.count(typename)  # 对于给定类型typename, 返回Python垃圾回收器正在跟踪的对象个数. 

objgraph可以通过命令pip install objgraph安装. 完整的文档可以参看这里.

Python内存管理机制

Python有两种共存的内存管理机制: 引用计数垃圾回收. 引用计数是一种非常高效的内存管理手段, 当一个Python对象被引 用时其引用计数增加1, 当其不再被一个变量引用时则计数减1. 当引用计数等于0时对象被删除.

import gc  import objgraph  gc.disable()   class A(object):     pass  class B(object):  pass  def test1():  a = A()    b = B()  test1() print objgraph.count('A') print objgraph.count('B') 

上面程序的执行结果为:

Object count of A: 0 Object count of B: 0 

test1中, 我们分别创建了类A和类B的对象, 并用变量ab引用起来. 当test1调用结束后objgraph.count('A')返回0, 意味着内存中A的对象数量 没有增长. 同理B的对象数量也没有增长. 注意我们通过gc.disable()关闭了 Python的垃圾回收, 因此test1中生产的对象是在函数调用结束引用计数为0时被自 动删除的.

引用计数的一个主要缺点是无法自动处理循环引用. 继续上面的代码:

def test2():     a = A()     b = B()     a.child = b     b.parent = a  test2() print 'Object count of A:', objgraph.count('A') print 'Object count of B:', objgraph.count('B') gc.collect() print 'Object count of A:', objgraph.count('A') print 'Object count of B:', objgraph.count('B') 

在上面的代码的执行结果为:

Object count of A: 1 Object count of B: 1 Object count of A: 0 Object count of B: 0 

test1相比test2的改变是将AB的对象通过childparent相互引用 起来. 这就形成了一个循环引用. 当test2调用结束后, 表面上我们不再引用两个对象, 但由于两个对象相互引用着对方, 因此引用计数不为0, 则不会被自动回收. 更糟糕的是由于现在没有任何变量引用他们, 我们无法再找到这两个变量并清除. Python使用垃圾回收机制来处理这样的情况. 执行gc.collect(), Python垃圾 回收器回收了两个相互引用的对象, 之后AB的对象数又变为0.

垃圾回收机制

本节将简单介绍Python的垃圾回收机制. Garbage Collection for Python 以及Python垃圾回收源码 中的注释进行了更详细的解释.

在Python中, 所有能够引用其他对象的对象都被称为容器(container). 因此只有容器之间才可能形成循环引用. Python的垃圾回收机制利用了这个特点来寻找需要被释放的对象. 为了记录下所有的容器对象, Python将每一个 容器都链到了一个双向链表中, 之所以使用双向链表是为了方便快速的在容器集合中插入和删除对象. 有了这个 维护了所有容器对象的双向链表以后, Python在垃圾回收时使用如下步骤来寻找需要释放的对象:

  1. 对于每一个容器对象, 设置一个gc_refs值, 并将其初始化为该对象的引用计数值.
  2. 对于每一个容器对象, 找到所有其引用的对象, 将被引用对象的gc_refs值减1.
  3. 执行完步骤2以后所有gc_refs值还大于0的对象都被非容器对象引用着, 至少存在一个非循环引用. 因此 不能释放这些对象, 将他们放入另一个集合.
  4. 在步骤3中不能被释放的对象, 如果他们引用着某个对象, 被引用的对象也是不能被释放的, 因此将这些 对象也放入另一个集合中.
  5. 此时还剩下的对象都是无法到达的对象. 现在可以释放这些对象了.

值得注意的是, 如果一个Python对象含有__del__这个方法, Python的垃圾回收机制即使发现该对象不可到达 也不会释放他. 原因是__del__这个方式是当一个Python对象引用计数为0即将被删除前调用用来做清理工作的. 由于垃圾回收找到的需要释放的对象中往往存在循环引用的情况, 对于循环引用的对象ab, 应该先调用哪 一个对象的__del__是无法决定的, 因此Python垃圾回收机制就放弃释放这些对象, 转而将这些对象保存起来, 通过gc.garbage这个变量访问. 程序员可以通过gc.garbage手动释放对象, 但是更好的方法是避免在代码中 定义__del__这个方法.

除此之外, Python还将所有对象根据’生存时间’分为3代, 从0到2. 所有新创建的对象都分配为第0代. 当这些对象 经过一次垃圾回收仍然存在则会被放入第1代中. 如果第1代中的对象在一次垃圾回收之后仍然存货则被放入第2代. 对于不同代的对象Python的回收的频率也不一样. 可以通过gc.set_threshold(threshold0[, threshold1[, threshold2]]) 来定义. 当Python的垃圾回收器中新增的对象数量减去删除的对象数量大于threshold0时, Python会对第0代对象 执行一次垃圾回收. 每当第0代被检查的次数超过了threshold1时, 第1代对象就会被执行一次垃圾回收. 同理每当 第1代被检查的次数超过了threshold2时, 第2代对象也会被执行一次垃圾回收.

由于Python的垃圾回收需要检查所有的容器对象, 因此当一个Python程序生产了大量的对象时, 执行一次垃圾回收将 带来可观的开销. 因此可以通过一些手段来尽量避免垃圾回收以提高程序的效率.

调优手段

手动垃圾回收

对Python的垃圾回收进行调优的一个最简单的手段便是关闭自动回收, 根据情况手动触发. 例如在用Python开发游戏时, 可以在一局游戏的开始关闭GC, 然后在该局游戏结束后手动调用一次GC清理内存. 这样能完全避免在游戏过程中因此 GC造成卡顿. 但是缺点是在游戏过程中可能因为内存溢出导致游戏崩溃.

调高垃圾回收阈值

相比完全手动的垃圾回收, 一个更温和的方法是调高垃圾回收的阈值. 例如一个游戏可能在某个时刻产生大量的子弹对象(假如是2000个). 而此时Python的垃圾回收的threshold0为1000. 则一次垃圾回收会被触发, 但这2000个子弹对象并不需要被回收. 如果此时 Python的垃圾回收的threshold0为10000, 则不会触发垃圾回收. 若干秒后, 这些子弹命中目标被删除, 内存被引用计数机制 自动释放, 一次(可能很耗时的)垃圾回收被完全的避免了.

调高阈值的方法能在一定程度上避免内存溢出的问题(但不能完全避免), 同时可能减少可观的垃圾回收开销. 根据具体项目 的不同, 甚至是程序输入的不同, 合适的阈值也不同. 因此需要反复测试找到一个合适的阈值, 这也算调高阈值这种手段 的一个缺点.

避免循环引用

一个可能更好的方法是使用良好的编程习惯尽可能的避免循环引用. 两种常见的手段包括: 手动解循环引用和使用弱引用.

手动解循环引用

手动解循环引用指在编写代码时写好解开循环引用的代码, 在一个对象使用结束不再需要时调用. 例如:

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') print 'Object count of B:', objgraph.count('B') 

上面代码的运行结果为:

Object count of A: 0 Object count of B: 0 

使用弱引用

弱引用指当引用一个对象时, 不增加该对象的引用计数, 当需要使用到该对象的时候需要首先检查该对象是否还存在. 弱引用的实现方式有多种, Python自带一个弱引用库weakref, 其详细文档参加这里. 使用weakref改写我们的代码:

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') print 'Object count of B:', objgraph.count('B') 

上面代码的运行结果为:

Object count of A: 0 Object count of B: 0 

除了使用Python自带的weakref库以外, 通常我们也可以根据自己项目的业务逻辑实现弱引用. 例如在游戏开发中, 通常很多对象都是有 其唯一的ID的. 在引用一个对象时我们可以保存其ID而不是直接引用该对象. 在需要使用该对象的时候首先根据ID去检查该对象是否存在.

效率测试

为了测试各种调优手段对Python运行效率的实际影响, 本文使用了如下代码进行效率测试:

class Dungeon(object):   def __init__(self):         self._monster_list = []    def add_monster(self, monster):         self._monster_list.append(monster)      monster.on_add_to_dungeon(self)     def destroy(self):      self._monster_list = []  class Monster(object):    ATTRIBUTE_NAME_LIST = None     def __init__(self, attribute_count):        self._dungeon = None       for i in xrange(attribute_count):           self.__dict__[Monster.ATTRIBUTE_NAME_LIST[i]] = i      def on_add_to_dungeon(self, dungeon):       self._dungeon = dungeon   class MonsterWithWeakref(Monster):   def on_add_to_dungeon(self, dungeon):       self._dungeon = weakref.ref(dungeon)   def run_logic(      max_iteration, dungeon_per_iter, monster_per_dungeon, attribute_count,      unloop_model):  iter_timer_list = []   for i in xrange(max_iteration):         begin_at = timeit.default_timer()      for dc in xrange(dungeon_per_iter):             dungeon = Dungeon()            for mc in xrange(monster_per_dungeon):              if unloop_model == 2:                     monster = MonsterWithWeakref(attribute_count)              else:                   monster = Monster(attribute_count)                 dungeon.add_monster(monster)            if unloop_model == 1:                 dungeon.destroy()       end_at = timeit.default_timer()        iter_timer_list.append(end_at-begin_at)     return iter_timer_list 

在上面的测试代码中, 我们模拟一个非常简单的游戏场景, 在每一帧(在上面的测试代码中为一个iteration) 会创建若干个副本(Dungeon)对象, 对每一个副本对象创建并加入若干个怪物(Monster)对象. 当一个怪物加入 副本时便会形成一个循环引用. 当每一帧结束时, 新创建的副本和怪物都不再使用, 根据调优的方式不同分别 进行不同的处理: 1. 不进行任何处理; 2. 通过手动解环的方式解除循环引用; 3. 在创建怪物时使用weakref引用副本. 然后测试会记录下每一帧的运行时间然后返回.

上图是对每一种调优方法进行500帧测试的结果. 其中GC(threshold0:700)指只使用Python的GC手段, 700为Python默认的threshold0值. GC(threshold0:10000)为将threshold0的值调高到10000.Manual为手动解除循环引用. Weakref为使用weakref避免循环引用. 从图中可以看到GC(threshold0:700)的平均每帧耗时最高, 且每隔一段时间会出现一次较高的费时, 原因是此时GC在 工作. 而GC(threshold:10000)的平均每帧耗时则更低, 且出现因GC造成的高费时的次数也更少, 然而 由于调高threshold0值以后每次需要回收的对象数量大大增加, 因此GC耗时的峰值是最高的. 使用weakref 的每帧耗时则低很多且平稳度更高. 而表现最为出色的则是手动解除循环引用.

所以在使用Python时, 一种好的习惯是将Python的垃圾回收作为一种保护机制, 用来回收编码中泄露的循环 引用对象, 而在实际编程中则尽量解开所有的循环引用以节省大量的GC开销.

查找循环引用

从上一节的测试中可以看到如果能在编码时解开所有的循环引用对于程序运行的效率会有不小的提升, 尤其 是对那些需要长时间运行的, 时间敏感的Python程序(例如Web服务器). 但是在实际项目中很难保证所有的循环引用都被解开. 因此常常需要先查找运行的程序中存在哪些循环 引用然后再解开. Python的gc模块提供了gc.set_debug接口来设置一些辅助的调试信息. 如果我们 调用gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_OBJECTS)则每当Python进行垃圾回收时都会将 其发现的无法到达需要回收的对象的信息打印出来. 例如:

gc.collect() gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_OBJECTS) test2() gc.collect() 

上面的程序会输出:

gc: collectable <T 0x7fb6cacf6090> gc: collectable <T 0x7fb6cace0fd0> gc: collectable <dict 0x7fb6cac974b0> gc: collectable <dict 0x7fb6cac97280> 

gc.set_debug(gc.DEBUG_UNCOLLECTABLE | gc.DEBUG_OBJECTS)则会输出所有无法到达且 垃圾回收器无法回收的对象, 例如:

class T2(object):  def __del__(self):      pass  def test_uncollectable():     a = T2()   b = T2()   a.child = b    b.parent = a  gc.collect() gc.set_debug(gc.DEBUG_UNCOLLECTABLE | gc.DEBUG_OBJECTS) test_uncollectable() gc.collect() 

上面的程序会输出:

gc: uncollectable <T2 0x7fb91fd2a110> gc: uncollectable <T2 0x7fb91fd2a150> gc: uncollectable <dict 0x7fb91fccf4b0> gc: uncollectable <dict 0x7fb91fccf168> 

设置gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_UNCOLLECTABLE | gc.DEBUG_OBJECTS) 则能同时捕获两种情况. 当在测试环境运行程序时, 通过上面的调用打开debug信息, 将 debug信息保存到log文件中, 用grep等工具找到所有循环引用对象中那些属于我们自己编写的. 然后在代码中解除循环引用即可.

转载于:https://www.cnblogs.com/buxizhizhoum/p/7714670.html

Python内存管理机制及优化简析(转载)相关推荐

  1. python内存管理机制_python内存管理机制

    python内存管理机制: 引用计数 垃圾回收(引用计数,标记清除,分带回收) 内存池 1. 引用计数 当一个python对象被引用时 其引用计数增加 1 ; 当其不再被变量引用时 引用计数减 1 ; ...

  2. python内存管理机制错误_Python内存管理机制和垃圾回收机制的简单理解

    一.内存管理机制 1.由c开发出来的cpython 2.include / objests 3.需要下载python源码包 4.Pyobject:float PyVarObject: 5.在pytho ...

  3. python内存管理错误的是_解读Python内存管理机制(转载)

    内存管理,对于Python这样的动态语言,是至关重要的一部分,它在很大程度上甚至决定了Python的执行效率,因为在Python的运行中,会创建和销毁大量的对象,这些都涉及到内存的管理. 小块空间的内 ...

  4. 【Python】Python内存管理机制

    文章目录 1.1 可变对象和不可变对象 1.2 Python垃圾回收 (1)垃圾回收 (2)标记-清除 and 分代回收 1.3 Python内存池 Reference 1.1 可变对象和不可变对象 ...

  5. Python内存管理机制

    1.Python引用,变量,对象 python中的对象可以分为不可变对象和可变对象两种类型,分别是:小整数.简单字符串(对于整数和字符串为什么要强调小整数和简单字符串参考博客Python缓存机制),, ...

  6. python内存管理机制_[转] Python内存管理机制

    一.引用计数 1.变量与对象 变量赋值的时候才创建,它可以指向(引用)任何类型的对象 python里每一个东西都是对象,它们的核心就是一个结构体:PyObject 变量必须先赋值,再引用. 比如,你定 ...

  7. python内存池机制,python基础—12python内存管理机制

    一.内存池机制 1.对象池 面试题目: python内存管理机制是什么? 1). 由于python中万物皆对象,内存管理机制就是对象的存储问题,Python会分 配一块内存空间去存储对象. 2) 对于 ...

  8. python内存管理说法错误_python面试题总结1-内存管理机制

    1.python内存管理机制 (1).引用计数 (2). 垃圾回收 (3). 内存池机制 引用计数 在python中每创建一个对象,对应的会有一个引用计数,当发生赋值操作如a=b,对应的b的引用计数会 ...

  9. python的内存管理机制及调优手段_Python的内存管理机制及调优手段?

    转载自 http://kkpattern.github.io/2015/06/20/python-memory-optimization-zh.html 准备工作 为了方便解释Python的内存管理机 ...

最新文章

  1. asp.net mysql 创建变_[ASP.net教程]EF Core使用CodeFirst在MySql中创建新数据库以及已有的Mysql数据库如何使用DB First生成域模型...
  2. div模拟textarea文本域轻松实现高度自适应——张鑫旭
  3. python中文注释报错
  4. Mysql-学习笔记(==》常用函数 八)
  5. 字符串处理分割字符串
  6. Java JDK代理、CGLIB、AspectJ代理分析比较
  7. 离职证明电子版_离职证明中说劳动者因违纪离职的怎么办?
  8. MySQL中的执行计划(explain)
  9. ORACLE中exists与in的区别
  10. 关于JS的window.onload与$(function (){})方法区别
  11. 真香 Jenkins 主从模式解决问题So Easy~
  12. 程序员如何财务自由【原创】
  13. 手机android的文件怎么恢复,手机文件误删除怎么恢复-互盾安卓恢复大师
  14. Java生成解析一维码二维码
  15. WFD抓包及视频提取
  16. Stm32F4x采用外部触发法测矩形波频率和占空比
  17. 程序员的自我修养_之二_曾国藩的“大悔大悟”
  18. typora 公式对齐_Typora极简教程
  19. 共克时艰|链下思考系列之一区块链能做点什么
  20. linux 给用户添加权限

热门文章

  1. 仿新版ofo共享单车小黄人动态效果
  2. Github爆火!暗黑破坏2经典游戏开源,重温经典!
  3. RAMPS1.4 3d打印控制板接线与测试5
  4. 从多个角度分析顺序表和链表区别和特点
  5. 橄榄山+住建部BIM数据库:
  6. windows10更新体验windows11
  7. OC 获取view相对位置_三晶变频器启动跳OC维修故障处理
  8. 专题分纲目录 学习原则
  9. java爬虫实时采集小说+springboot推荐算法+实现在线小说免费阅读推荐系统
  10. 计算机组装与维护论文 致谢,计算机组装论文范文2篇