转载自 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可以通过命令pipinstallobjgraph安装.完整的文档可以参看这里.

Python内存管理机制

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

importgc

importobjgraph

gc.disable()

classA(object):

pass

classB(object):

pass

deftest1():

a=A()

b=B()

test1()

printobjgraph.count('A')

printobjgraph.count('B')

上面程序的执行结果为:

ObjectcountofA:0

ObjectcountofB:0

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

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

deftest2():

a=A()

b=B()

a.child=b

b.parent=a

test2()

print'ObjectcountofA:',objgraph.count('A')

print'ObjectcountofB:',objgraph.count('B')

gc.collect()

print'ObjectcountofA:',objgraph.count('A')

print'ObjectcountofB:',objgraph.count('B')

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

ObjectcountofA:1

ObjectcountofB:1

ObjectcountofA:0

ObjectcountofB:0

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

垃圾回收机制

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

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

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

值得注意的是,如果一个Python对象含有__del__这个方法,Python的垃圾回收机制即使发现该对象不可到达也不会释放他.原因是__del__这个方式是当一个Python对象引用计数为0即将被删除前调用用来做清理工作的.由于垃圾回收找到的需要释放的对象中往往存在循环引用的情况,对于循环引用的对象a和b,应该先调用哪一个对象的__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,则不会触发垃圾回收.若干秒后,这些子弹命中目标被删除,内存被引用计数机制自动释放,一次(可能很耗时的)垃圾回收被完全的避免了.

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

避免循环引用

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

手动解循环引用

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

classA(object):

def__init__(self):

self.child=None

defdestroy(self):

self.child=None

classB(object):

def__init__(self):

self.parent=None

defdestroy(self):

self.parent=None

deftest3():

a=A()

b=B()

a.child=b

b.parent=a

a.destroy()

b.destroy()

test3()

print'ObjectcountofA:',objgraph.count('A')

print'ObjectcountofB:',objgraph.count('B')

上面代码的运行结果为:

ObjectcountofA:0

ObjectcountofB:0

使用弱引用

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

deftest4():

a=A()

b=B()

a.child=weakref.ref(b)

b.parent=weakref.ref(a)

test4()

print'ObjectcountofA:',objgraph.count('A')

print'ObjectcountofB:',objgraph.count('B')

上面代码的运行结果为:

ObjectcountofA:0

ObjectcountofB:0

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

效率测试

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

classDungeon(object):

def__init__(self):

self._monster_list=[]

defadd_monster(self,monster):

self._monster_list.append(monster)

monster.on_add_to_dungeon(self)

defdestroy(self):

self._monster_list=[]

classMonster(object):

ATTRIBUTE_NAME_LIST=None

def__init__(self,attribute_count):

self._dungeon=None

foriinxrange(attribute_count):

self.__dict__[Monster.ATTRIBUTE_NAME_LIST[i]]=i

defon_add_to_dungeon(self,dungeon):

self._dungeon=dungeon

classMonsterWithWeakref(Monster):

defon_add_to_dungeon(self,dungeon):

self._dungeon=weakref.ref(dungeon)

defrun_logic(

max_iteration,dungeon_per_iter,monster_per_dungeon,attribute_count,

unloop_model):

iter_timer_list=[]

foriinxrange(max_iteration):

begin_at=timeit.default_timer()

fordcinxrange(dungeon_per_iter):

dungeon=Dungeon()

formcinxrange(monster_per_dungeon):

ifunloop_model==2:

monster=MonsterWithWeakref(attribute_count)

else:

monster=Monster(attribute_count)

dungeon.add_monster(monster)

ifunloop_model==1:

dungeon.destroy()

end_at=timeit.default_timer()

iter_timer_list.append(end_at-begin_at)

returniter_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

gc:collectable

gc:collectable

gc:collectable

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

classT2(object):

def__del__(self):

pass

deftest_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

gc:uncollectable

gc:uncollectable

gc:uncollectable

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

python的内存管理机制及调优手段_Python的内存管理机制及调优手段?相关推荐

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

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

  2. python是基于什么的内存管理方式_python的内存管理机制是什么?

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

  3. python中内存管理机制一共分为多少层_python 内存管理机制

    内存管理机制 ​python中万物皆对象,python的存储问题是对象的存储问题,并且对于每个对象,python会分配一块内存空间去存储它 ​Python的内存管理机制:引入计数.垃圾回收.内存池机制 ...

  4. Hadoop性能调优、YARN的内存和CPU配置

    转 Hadoop性能调优.YARN的内存和CPU配置 2018年06月12日 21:01:54 toto1297488504 阅读数:2417 转: https://blog.csdn.net/deh ...

  5. python竞赛内存_Python的内存机制浅析

    Python的内存管理大概分为三点: 引用计数 垃圾回收 内存池机制 这是我要讲的东西嘛?Of course! 肯定不是啊,这些网上都有我要讲的是人家没有的才好玩啊. No. 1  内存 内存是计算机 ...

  6. python内存管理变量_Python变量内存管理

    Python变量内存管理 一 .引入 解释器在执行到定义变量的语法时,会申请内存空间来存放变量的值,而内存的容量是有限的,这就涉及到变量值所占用内存空间的回收问题,当一个变量值没有用了(简称垃圾)就应 ...

  7. 性能调优-------(六)内存使用率高,5分钟如何快速排查问题(真实场景实战图文讲解)

    零.总结: 本次问题通过分析,由于平时70%+的内存使用率,目前达到88%是由于5个月系统未重新发布内存数据和缓存不断增加以及堆内存的增加累计达到了内存使用率的报警阀值88%. 那么平时如果出现内存使 ...

  8. python内存管理及释放_python的内存管理

    python的内存分配 python的内存分配主要分为4层.在Objects/obmalloc.c中的注释里画出了python的内存分配模型如下. 截图来自python代码的注释 其中第0层是库函数的 ...

  9. linux内存管理的主要概念是虚拟内存,有关linux内存管理机制的相关内容,linux物理内存和虚拟内存,深入了解Linux内存运行 ......

    在linux中空闲内存很少,似乎所有的内存都被系统占用了,表面感觉是内存不够用了,其实不然. 这是Linux内存管理的一个优秀特性,区别于Windows的内存管理. 主要特点: 无论物理内存有多大,L ...

最新文章

  1. 因为我把JMM原理讲解了一遍,这给足了我涨薪的底气!
  2. [C++11]指针空值类型nullptr
  3. mysql批量插入数据的函数和存储过程
  4. flex----其他交互设计
  5. java中的类如何选取,java里如何获取泛型的类型
  6. requests之json系列(一)
  7. 关于坑爹的编解码问题
  8. 什么是 Java 内存模型( JMM )
  9. cesium——鼠标拾取坐标并转换为经纬高
  10. 软件测试系统主要包括哪几项内容,系统测试包含哪几部分?
  11. 微信公众号如何添加附件链接
  12. LabVIEW与Matlab混合编程进行图像处理(附带颜色栏Colorbar)
  13. “梦中行千里不如现实行一步。”创业亦是如此丨国仁网络资讯
  14. iOS14降级iOS13
  15. Shader --- 法阵
  16. 卫康直销如何安全的参与中国MMM互助金融社区系统?
  17. 迁移学习一——TCA和SSTCA
  18. 03 矩阵不只是mn个数字
  19. 洛谷_2495 [SDOI2011]消耗战(虚树)
  20. 二本院校毕业一年的工作经历(Java开发)

热门文章

  1. python udp 丢包_Python语言---TCP、UDP
  2. gnss观测质量检查软件下载_【速查】你可能下载了个假“北斗”!
  3. 获取当前日期是本年的第几周java与mysql获取值不一致
  4. mac ---- 安装ftp服务器
  5. 更新失败 连接超时_苹果发布 iOS iPadOS 更新 修复蓝牙连接失败等错误
  6. wampserver mysql乱码_ajax +json +php提交mysql数据库 中文乱码(wamp server 本地测试)求解?...
  7. 6加载光盘 virtualbox vm_6个轻量级Linux发行版(2019年)
  8. python mpi多线程_使用 MPI for Python 并行化遗传算法
  9. java response文件流下载,后缀名称设置
  10. RabbitMQ消息接收的确认方式