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 == : 					monster = MonsterWithWeakref(attribute_count) 				else: 					monster = Monster(attribute_count) 				dungeon.add_monster(monster) 			if unloop_model == : 				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等工具找到所有循环引用对象中那些属于我们自己编写的. 然后在代码中解除循环引用即可.

Python内存管理机制及优化简析(转载)的更多相关文章

  1. 【python测试开发栈】—python内存管理机制(二)—垃圾回收

    在上一篇文章中(python 内存管理机制-引用计数)中,我们介绍了python内存管理机制中的引用计数,python正是通过它来有效的管理内存.今天来介绍python的垃圾回收,其主要策略是引用计数 ...

  2. Python内存管理机制-《源码解析》

    Python内存管理机制 Python 内存管理分层架构 /* An object allocator for Python. Here is an introduction to the layer ...

  3. 【python测试开发栈】python内存管理机制(一)—引用计数

    什么是内存 在开始进入正题之前,我们先来回忆下,计算机基础原理的知识,为什么需要内存.我们都知道计算机的CPU相当于人类的大脑,其运算速度非常的快,而我们平时写的数据,比如:文档.代码等都是存储在磁盘 ...

  4. 解读Python内存管理机制

    转自:http://developer.51cto.com/art/201007/213585.htm 内存管理,对于Python这样的动态语言,是至关重要的一部分,它在很大程度上甚至决定了Pytho ...

  5. 了解Python内存管理机制,让你的程序飞起来

    引用: 语言的内存管理是语言设计的一个重要方面.它是决定语言性能的重要因素.无论是C语言的手工管理,还是Java的垃圾回收,都成为语言最重要的特征.这里以Python语言为例子,说明一门动态类型的.面 ...

  6. Python深入之python内存管理机制(重点)

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:醍醐三叶   关于python的存储问题, (1)由于python中 ...

  7. (重点)Python深入之Python内存管理机制你会吗?

    前言本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理.作者:醍醐三叶 请注意:如果你平时学Python遇到问题找不到人解答?或者没有 ...

  8. python内存管理机制

    主要分为三部分: (1)内存池机制(2)引用计数(3)垃圾回收 (1)内存池机制对于python来说,对象的类型和内存都是在运行时确定的,所以python对象都是动态类型简单来说,python内存分为 ...

  9. Python 源码剖析(六)【内存管理机制】

    六.内存管理机制 1.内存管理架构 2.小块空间的内存池 3.循环引用的垃圾收集 4.python中的垃圾收集 1.内存管理架构 Python内存管理机制有两套实现,由编译符号PYMALLOC_DEB ...

随机推荐

  1. VS2010/MFC编程入门系列教程 (转)

    http://www.jizhuomi.com/school/  鸡啄米编程课堂 http://www.jizhuomi.com/software/257.html http://blog.csdn. ...

  2. c# list排序的三种实现方式 (转帖)

    用了一段时间的gridview,对gridview实现的排序功能比较好奇,而且利用C#自带的排序方法只能对某一个字段进行排序,今天demo了一下,总结了三种对list排序的方法,并实现动态传递字段名对 ...

  3. android 关于listview scrollview 底部 控件无法显示的两个解决方案

    方案一 用LinearLayout实现,代码如下: <!-- 中奖纪录 by mhd --> <LinearLayout xmlns:android="http://sch ...

  4. Android画图之抗锯齿 paint 和 Canvas 两种方式

    在画图的时候,图片如果旋转或缩放之后,总是会出现那些华丽的锯齿.其实Android自带了解决方式.    方法一:给Paint加上抗锯齿标志.然后将Paint对象作为参数传给canvas的绘制方法. ...

  5. web中显示中文名称的图片,可以这样配置filter

    com.cy.filter.UrlFilter: package com.cy.filter; import java.io.IOException; import java.net.URLDecod ...

  6. 术语-服务:BaaS

    ylbtech-术语-服务:BaaS BaaS(后端即服务:Backend as a Service)公司为移动应用开发者提供整合云后端的边界服务. 1.返回顶部 1. 中文名:后端即服务 外文名:B ...

  7. 超链接中 utm_source, utm_medium 等参数的含义是什么?

    作者:张溪梦 Simon链接:https://www.zhihu.com/question/48724061/answer/122730629来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非 ...

  8. servlet的讲解

    https://www.ibm.com/developerworks/cn/java/j-lo-servlet/

  9. Asynchronous programming with Tornado

    Asynchronous programming can be tricky for beginners, therefore I think it’s useful to iron some bas ...

  10. 第5章 进程环境(1)_进程结构(task_struct)

    1. 进程的概念和进程结构 1.1 进程 (1)程序(program):是一些保存在磁盘上有序指令的集合,是存放在磁盘文件中的可执行文件.但没有任何执行的概念,它是静态的. (2)进程(process ...