Python的内存管理机制及调优手段?

2020-04-27 19:12发布

2条回答

内存管理机制:引用计数、垃圾回收、内存池

 

引用计数:

  引用计数是一种非常高效的内存管理手段,当一个Python对象引用时其引用计数加一,当其不再被一个变量引用时则减一。当引用计数等于0时对象被删除。

 

1.引用计数:  

  引用计数也是一种垃圾收集机制,而且也是一种最直观,最简单的垃圾收集技术。当 Python 的某个对象的引用计数降为 0 时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如某个新建对象,它被分配给某个引用,对象的引用计数变为 1。如果引用被删除,对象的引用计数为 0,那么该对象就可以被垃圾回收。不过如果出现循环引用的话,引用计数机制就不再起有效的作用了。

2.标记清除:

  如果两个对象的引用计数都为1,但是仅仅存在他们之间的循环引用,那么这两个对象都是需要被回收的。也就是说,他们的引用计数虽然表现为非0,但是实际上有效的引用计数为0.所以现将循环引用摘掉,就会得出这两个对象的有效计数。

 

3、分代收集

经过上面的【标记-清理】方法,已经可以保证对垃圾的回收了,但还有一个问题,【标记-清理】什么时候执行比较好呢,是对所有对象都同时执行吗?

同时执行很显然不合理,我们知道,存活越久的对象,说明他的引用更持久(好像是个屁话,引用不持久就被删除了),为了更合理的进行【标记-删除】,就需要对对象进行分代处理,思路很简单:

1、新创建的对象做为0代
2、每执行一个【标记-删除】,存活的对象代数就+1
3、代数越高的对象(存活越持久的对象),进行【标记-删除】的时间间隔就越长。这个间隔,江湖人称阀值

 

4.调优手段(了解)

1.手动垃圾回收
2.调高垃圾回收阈值
3.避免循环引用(手动解循环引用和使用弱引用)
Naughty
3楼 · 2020-11-27 14:17


转载自 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等工具找到所有循环引用对象中那些属于我们自己编写的.然后在代码中解除循环引用即可.


相关问题推荐

  • 回答 3

    换行。比如,print hello\nworld效果就是helloworld\n就是一个换行符。\是转义的意思,'\n'是换行,'\t'是tab,'\\'是,\ 是在编写程序中句子太长百,人为换行后加上\但print出来是一整行。...

  • 回答 42

    十种常见排序算法一般分为以下几种:(1)非线性时间比较类排序:a. 交换类排序(快速排序、冒泡排序)b. 插入类排序(简单插入排序、希尔排序)c. 选择类排序(简单选择排序、堆排序)d. 归并排序(二路归并排序、多路归并排序)(2)线性时间非比较类排序:...

  • 回答 70
    已采纳

    前景很好,中国正在产业升级,工业机器人和人工智能方面都会是强烈的热点,而且正好是在3~5年以后的时间。难度,肯定高,要求你有创新的思维能力,高数中的微积分、数列等等必须得非常好,软件编程(基础的应用最广泛的语言:C/C++)必须得很好,微电子(数字电...

  • 回答 28

    迭代器与生成器的区别:(1)生成器:生成器本质上就是一个函数,它记住了上一次返回时在函数体中的位置。对生成器函数的第二次(或第n次)调用,跳转到函数上一次挂起的位置。而且记录了程序执行的上下文。生成器不仅记住了它的数据状态,生成器还记住了程序...

  • 回答 9

    python中title( )属于python中字符串函数,返回’标题化‘的字符串,就是单词的开头为大写,其余为小写

  • 回答 6

    第一种解释:代码中的cnt是count的简称,一种电脑计算机内部的数学函数的名字,在Excel办公软件中计算参数列表中的数字项的个数;在数据库( sq| server或者access )中可以用来统计符合条件的数据条数。函数COUNT在计数时,将把数值型的数字计算进去;但是...

  • 回答 1

    head是方法,所以需要取小括号,即dataset.head()显示的则是前5行。data[:, :-1]和data[:, -1]。另外,如果想通过位置取数据,请使用iloc,即dataset.iloc[:, :-1]和dataset.iloc[:, -1],前者表示的是取所有行,但不包括最后一列的数据,结果是个DataFrame。...

  • Python入门简单吗2021-09-23 13:21
    回答 45

    挺简单的,其实课程内容没有我们想象的那么难、像我之前同学,完全零基础,培训了半年,直接出来就工作了,人家还在北京大公司上班,一个月15k,实力老厉害了

  • 回答 4

    Python针对众多的类型,提供了众多的内建函数来处理(内建是相对于导入import来说的,后面学习到包package时,将会介绍),这些内建函数功用在于其往往可对多种类型对象进行类似的操作,即多种类型对象的共有的操作;如果某种操作只对特殊的某一类对象可行,Pyt...

  • 回答 8

     相当于 ... 这里不是注释

  • 回答 4

    还有FIXME

  • 回答 3

    python的两个库:xlrd和xlutils。 xlrd打开excel,但是打开的excel并不能直接写入数据,需要用xlutils主要是复制一份出来,实现后续的写入功能。

  • 回答 8

    单行注释:Python中的单行注释一般是以#开头的,#右边的文字都会被当做解释说明的内容,不会被当做执行的程序。为了保证代码的可读性,一般会在#后面加一两个空格然后在编写解释内容。示例:#  单行注释print(hello world)注释可以放在代码上面也可以放在代...

  • 回答 2

    主要是按行读取,然后就是写出判断逻辑来勘测行是否为注视行,空行,编码行其他的:import linecachefile=open('3_2.txt','r')linecount=len(file.readlines())linecache.getline('3_2.txt',linecount)这样做的过程中发现一个问题,...

  • 回答 4

    或许是里面有没被注释的代码

  • 回答 26

    自学的话要看个人情况,可以先在B站找一下视频看一下

没有解决我的问题,去提问