python内存管理

python内存管理是通过引用计数来实现的。当对象的引用计数为0时,会被gc回收。

为了探索对象在内存的存储,我们可以求助于Python的内置函数id()。它用于返回对象的身份(identity)。其实,这里所谓的身份,就是该对象的内存地址。判断对象a和b的内存地址是否一致(而不是a和b的值是否一致)可以用is来判断。如a="good",b="good",print(a is b)//True。

a=1

b=1

print(a is b) //True

a="good"

b="good"

print(a is b) //True

a="it is a very good day"

b="it is a very good day"

print(a is b) //False

a=[]

b=[]

print(a is b) //False

由于Python缓存了整数和短字符串,因此每个对象只存有一份。比如,所有整数1的引用都指向同一对象。即使使用赋值语句,也只是创造了新的引用,而不是对象本身。长的字符串和其它对象可以有多个相同的对象,可以使用赋值语句创建出新的对象。

在Python中,每个对象都有存有指向该对象的引用总数,即引用计数(reference count)。我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。

from sys import getrefcount

a = [1, 2, 3]

print(getrefcount(a)) //2

b = a

print(getrefcount(b))  //3

当一个对象A被另一个对象B引用时,A的引用计数将增加1。

from  sys import getrefcount

a = [1, 2, 3]

print(getrefcount(a)) //2

b = [a, a]

print(getrefcount(a))  //4

容器对象的引用可能构成很复杂的拓扑结构。我们可以用objgraph包来绘制其引用关系,比如:

x = [1, 2, 3]

y = [x, dict(key1=x)]

z = [y, (x, y)]

import objgraph

objgraph.show_refs([z], filename='ref_topo.png')

objgraph是Python的一个第三方包。安装之前需要安装xdot。

sudo apt-get install xdot

sudo pip install objgraph

两个对象可能相互引用,从而构成所谓的引用环(reference cycle)。如:

a = []

b = [a]

a.append(b)

即使是一个对象,只需要自己引用自己,也能构成引用环。

a = []

a.append(a)

print(getrefcount(a)) //3

某个对象的引用计数可能减少。比如,可以使用del关键字删除某个引用:

from sys import getrefcount

a = [1, 2, 3]

b = a

print(getrefcount(b))  //3

del a

print(getrefcount(b)) //2


a=[1,2,3]

print(getrefcount(a)) //2

b=[a,a]

print(getrefcount(a)) //4

print(getrefcount(b)) //2

如果某个引用指向对象A,当这个引用被重新定向到某个其他对象B时,对象A的引用计数减少:

from sys import getrefcount

a = [1, 2, 3]

b = a

print(getrefcount(b))  //3

a = 1

print(getrefcount(b))  //2

吃太多,总会变胖,Python也是这样。当Python中的对象越来越多,它们将占据越来越大的内存。不过你不用太担心Python的体形,它会乖巧的在适当的时候“减肥”,启动垃圾回收(garbage collection),将没用的对象清除。在许多语言中都有垃圾回收机制,比如Java和Ruby。尽管最终目的都是塑造苗条的提醒,但不同语言的减肥方案有很大的差异.

从基本原理上,当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如某个新建对象,它被分配给某个引用,对象的引用计数变为1。如果引用被删除,对象的引用计数为0,那么该对象就可以被垃圾回收。比如下面的表:

a = [1, 2, 3]

del a

del a后,已经没有任何引用指向之前建立的[1, 2, 3]这个表。用户不可能通过任何方式接触或者动用这个对象。这个对象如果继续待在内存里,就成了不健康的脂肪。当垃圾回收启动时,Python扫描到这个引用计数为0的对象,就将它所占据的内存清空。

Python不能进行其它的任务。频繁的垃圾回收将大大降低Python的工作效率。如果内存中的对象不多,就没有必要总启动垃圾回收。所以,Python只会在特定条件下,自动启动垃圾回收。当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。

我们可以通过gc模块的get_threshold()方法,查看该阈值:

import gc

print(gc.get_threshold()) //返回(700, 10, 10)

返回(700, 10, 10),后面的两个10是与分代回收相关的阈值,后面可以看到。700即是垃圾回收启动的阈值。可以通过gc中的set_threshold()方法重新设置。

除了自动垃圾回收,也可以手动启动垃圾回收,即使用gc.collect()。后面的两个10都是分代回收相关的阈值,什么是分代回收呢?python采用了分代回收的策略。这一策略的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。我们的程序往往会产生大量的对象,许多对象很快产生和消失,但也有一些对象长期被使用。出于信任和效率,对于这样一些“长寿”对象,我们相信它们的用处,所以减少在垃圾回收中扫描它们的频率。

Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。

这两个次数即上面get_threshold()返回的(700, 10, 10)返回的两个10。也就是说,每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收。

同样可以用set_threshold()来调整,比如对2代对象进行更频繁的扫描。

import gc

gc.set_threshold(700, 10, 5)

引用环的存在会给上面的垃圾回收机制带来很大的困难。这些引用环可能构成无法使用,但引用计数不为0的一些对象。

a = []

b = [a]

a.append(b)

del a

del b

上面我们先创建了两个表对象,并引用对方,构成一个引用环。删除了a,b引用之后,这两个对象不可能再从程序中调用,就没有什么用处了。但是由于引用环的存在,这两个对象的引用计数都没有降到0,不会被垃圾回收。

为了回收这样的引用环,Python复制每个对象的引用计数,可以记为gc_ref。假设,每个对象i,该计数为gc_ref_i。Python会遍历所有的对象i。对于每个对象i引用的对象j,将相应的gc_ref_j减1。在结束遍历后,gc_ref不为0的对象,和这些对象引用的对象,以及继续更下游引用的对象,需要被保留。而其它的对象则被垃圾回收。

(1)对于每一个容器对象,设置一个gc_refs值,并将其初始化为该对象的引用计数值。

(2)对于每一个容器对象,找到所有其引用的对象,将被引用对象的gc_refs值减1.

(3)执行完步骤2以后,所有gc_refs的值还大于0的对象都被非容器对象引用着。至少存在一个非循环引用。因此,不能释放这些对象。将他们放入另一个集合。

(4)在步骤3中不能被释放的对象,如果他们引用着某个对象,被引用的对象也是不能被释放的。因此将这些对象也放入另一个集合中。

(5)此时还剩下的对象都是无法到达的对象,现在可以释放这些对象了。



Python作为一种动态类型的语言,其对象和引用分离。这与曾经的面向过程语言有很大的区别。为了有效的释放内存,Python内置了垃圾回收的支持。Python采取了一种相对简单的垃圾回收机制,即引用计数,并因此需要解决孤立引用环的问题。Python与其它语言既有共通性,又有特别的地方。对该内存管理机制的理解,是提高Python性能的重要一步。

gc module是python垃圾回收机制的接口模块,可以通过该module启停垃圾回收、调整回收触发的阈值、设置调试选项。

如果没有禁用垃圾回收,那么Python中的内存泄露有两种情况:要么是对象被生命周期更长的对象所引用,比如global作用域对象;要么是循环引用中存在__del__

垃圾回收比较耗时,因此在对性能和内存比较敏感的场景也是无法接受的,如果能解除循环引用,就可以禁用垃圾回收。

使用gc module的DEBUG选项可以很方便的定位循环引用,解除循环引用的办法要么是手动解除,要么是使用weakref。

Python中,一切都是对象,又分为mutable和immutable对象。二者区分的标准在于是否可以原地修改,“原地“”可以理解为相同的地址。可以通过id()查看一个对象的“地址”,如果通过变量修改对象的值,但id没发生变化,那么就是mutable,否则就是immutable。

判断两个变量是否相等(值相同)使用==, 而判断两个变量是否指向同一个对象使用 is。比如下面a1 a2这两个变量指向的都是空的列表,值相同,但是不是同一个对象。

>>> a1, a2 = [], []

>>> a1 == a2

True

>>> a1 is a2

False

为了避免频繁的申请、释放内存,避免大量使用的小对象的构造析构,python有一套自己的内存管理机制。

python会有自己的内存缓冲池(layer2)以及对象缓冲池(layer3)。在Linux上运行过Python服务器的程序都知道,python不会立即将释放的内存归还给操作系统,这就是内存缓冲池的原因。而对于可能被经常使用、而且是immutable的对象,比如较小的整数、长度较短的字符串,python会缓存在layer3,避免频繁创建和销毁。

a = 1

print(getrefcount(a)) //601

从对象1的引用计数信息也可以看到,python的对象缓冲池会缓存十分常用的immutable对象,比如这里的整数1。

什么是循环引用,就是一个对象直接或者间接引用自己本身,引用链形成一个环。

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

对于每一个容器对象, 设置一个gc_refs值, 并将其初始化为该对象的引用计数值.

对于每一个容器对象, 找到所有其引用的对象, 将被引用对象的gc_refs值减1.

执行完步骤2以后所有gc_refs值还大于0的对象都被非容器对象引用着, 至少存在一个非循环引用. 因此 不能释放这些对象, 将他们放入另一个集合.

在步骤3中不能被释放的对象, 如果他们引用着某个对象, 被引用的对象也是不能被释放的, 因此将这些 对象也放入另一个集合中.

此时还剩下的对象都是无法到达的对象. 现在可以释放这些对象了.

关于分代回收:

除此之外, 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代对象也会被执行一次垃圾回收.

为什么要分代呢,这个算法的根源来自于weak generational hypothesis。这个假说由两个观点构成:首先是年亲的对象通常死得也快,比如大量的对象都存在于local作用域;而老对象则很有可能存活更长的时间,比如全局对象,module, class。

垃圾回收的原理就如上面提示,详细的可以看Python源码,只不过事实上垃圾回收器还要考虑__del__,弱引用等情况,会略微复杂一些。

什么时候会触发垃圾回收呢,有三种情况:

达到了垃圾回收的阈值,Python虚拟机自动执行

手动调用gc.collect()

Python虚拟机退出的时候

对于垃圾回收,有两个非常重要的术语,那就是reachable与collectable(当然还有与之对应的unreachable与uncollectable),后文也会大量提及。

reachable是针对python对象而言,如果从根集(root)能到找到对象,那么这个对象就是reachable,与之相反就是unreachable,事实上就是只存在于循环引用中的对象,Python的垃圾回收就是针对unreachable对象。

而collectable是针对unreachable对象而言,如果这种对象能被回收,那么是collectable;如果不能被回收,即循环引用中的对象定义了__del__, 那么就是uncollectable。Python垃圾回收对uncollectable对象无能为力,会造成事实上的内存泄露。

gc module

这里的gc(garbage collector)是Python 标准库,该module提供了与上一节“垃圾回收”内容相对应的接口。通过这个module,可以开关gc、调整垃圾回收的频率、输出调试信息。gc模块是很多其他模块(比如objgraph)封装的基础,在这里先介绍gc的核心API。

gc.enable(); gc.disable(); gc.isenabled()

开启gc(默认情况下是开启的);关闭gc;判断gc是否开启

gc.collection() 

执行一次垃圾回收,不管gc是否处于开启状态都能使用

gc.set_threshold(t0, t1, t2); gc.get_threshold()

设置垃圾回收阈值; 获得当前的垃圾回收阈值

注意:gc.set_threshold(0)也有禁用gc的效果

gc.get_objects()

返回所有被垃圾回收器(collector)管理的对象。这个函数非常基础!只要python解释器运行起来,就有大量的对象被collector管理,因此,该函数的调用比较耗时!

比如,命令行启动python

>>> import gc

>>> len(gc.get_objects())

3749

gc.get_referents(*obj)

返回obj对象直接指向的对象

gc.get_referrers(*obj)

返回所有直接指向obj的对象

gc.set_debug(flags)

设置调试选项,非常有用,常用的flag组合包含以下

gc.DEBUG_COLLETABLE: 打印可以被垃圾回收器回收的对象

gc.DEBUG_UNCOLLETABLE: 打印无法被垃圾回收器回收的对象,即定义了__del__的对象

gc.DEBUG_SAVEALL:当设置了这个选项,可以被拉起回收的对象不会被真正销毁(free),而是放到gc.garbage这个列表里面,利于在线上查找问题

内存泄露

既然Python中通过引用计数和垃圾回收来管理内存,那么什么情况下还会产生内存泄露呢?有两种情况:

第一是对象被另一个生命周期特别长的对象所引用,比如网络服务器,可能存在一个全局的单例ConnectionManager,管理所有的连接Connection,如果当Connection理论上不再被使用的时候,没有从ConnectionManager中删除,那么就造成了内存泄露。

第二是循环引用中的对象定义了__del__函数,这个在《程序员必知的Python陷阱与缺陷列表》一文中有详细介绍,简而言之,如果定义了__del__函数,那么在循环引用中Python解释器无法判断析构对象的顺序,因此就不做处理。

在任何环境,不管是服务器,客户端,内存泄露都是非常严重的事情。

如果是线上服务器,那么一定得有监控,如果发现内存使用率超过设置的阈值则立即报警,尽早发现些许还有救。当然,谁也不希望在线上修复内存泄露,这无疑是给行驶的汽车换轮子,因此尽量在开发环境或者压力测试环境发现并解决潜在的内存泄露。在这里,发现问题最为关键,只要发现了问题,解决问题就非常容易了,因为按照前面的说法,出现内存泄露只有两种情况,在第一种情况下,只要在适当的时机解除引用就可以了;在第二种情况下,要么不再使用__del__函数,换一种实现方式,要么解决循环引用。

那么怎么查找哪里存在内存泄露呢?武器就是两个库:gc、objgraph

在上面已经介绍了gc这个模块,理论上,通过gc模块能够拿到所有的被garbage collector管理的对象,也能知道对象之间的引用和被引用关系,就可以画出对象之间完整的引用关系图。但事实上还是比较复杂的,因为在这个过程中一不小心又会引入新的引用关系,所以,有好的轮子就直接用吧,那就是objgraph

objgraph

objgraph的实现调用了gc的这几个函数:gc.get_objects(), gc.get_referents(), gc.get_referers(),然后构造出对象之间的引用关系。objgraph的代码和文档都写得比较好,建议一读。

下面先介绍几个十分实用的API

def count(typename)

返回该类型对象的数目,其实就是通过gc.get_objects()拿到所用的对象,然后统计指定类型的数目。

def by_type(typename)

返回该类型的对象列表。线上项目,可以用这个函数很方便找到一个单例对象

def show_most_common_types(limits = 10)

打印实例最多的前N(limits)个对象,这个函数非常有用。在《Python内存优化》一文中也提到,该函数能发现可以用slots进行内存优化的对象

def show_growth()

统计自上次调用以来增加得最多的对象,这个函数非常有利于发现潜在的内存泄露。函数内部调用了gc.collect(),因此即使有循环引用也不会对判断造成影响。

另外一种更方便的方法,就是使用弱引用weakref, weakref是Python提供的标准库,旨在解决循环引用。

weakref模块提供了以下一些有用的API:

(1)weakref.ref(object, callback = None)

创建一个对object的弱引用,返回值为weakref对象,callback: 当object被删除的时候,会调用callback函数,在标准库logging (__init__.py)中有使用范例。使用的时候要用()解引用,如果referant已经被删除,那么返回None。比如下面的例子

import weakref

class OBJ(object):

    def f(self):

        print 'HELLO'


if __name__ == '__main__':

    o = OBJ()

    w = weakref.ref(o)

    w().f()

    del o

    w().f()  //抛出异常:AttributeError: ‘NoneType’ object has no attribute ‘f’。因为这个时候被引用的对象已经被删除了

(2)weakref.proxy(object, callback = None)

创建一个代理,返回值是一个weakproxy对象,callback的作用同上。使用的时候直接用 和object一样,如果object已经被删除 那么抛出异常   ReferenceError: weakly-referenced object no longer exists。

# -*- coding: utf-8 -*-

import weakref

class OBJ(object):

    def f(self):

        print 'HELLO'


if __name__ == '__main__':

    o = OBJ()

    w = weakref.proxy(o)

    w.f()

    del o

    w.f()

(3)weakref.WeakSet

这个是一个弱引用集合,当WeakSet中的元素被回收的时候,会自动从WeakSet中删除。WeakSet的实现使用了weakref.ref,当对象加入WeakSet的时候,使用weakref.ref封装,指定的callback函数就是从WeakSet中删除。感兴趣的话可以直接看源码(_weakrefset.py),下面给出一个参考例子:

# -*- coding: utf-8 -*-

import weakref

class OBJ(object):

    def f(self):

        print 'HELLO'


if __name__ == '__main__':

    o = OBJ()

    ws = weakref.WeakSet()

    ws.add(o)

    print len(ws) #  1

    del o

    print len(ws) # 0

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

  • 1.元类 1.1.1类也是对象 在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。在Python中这...
    TENG书阅读 1,253评论 0 3
  • 在比较浅层次上我们通过说明如下问题来进一步深入了解python内存管理机制:Python中到底是“传引用”还是“传...
    tdeblog阅读 2,482评论 0 0
  • 参考:http://www.cnblogs.com/CBDoctor/p/3781078.html 先从较浅的层面...
    麦兜胖胖次阅读 708评论 0 1
  • 不曾想 遇见你 不曾想 与你相约 你如同漓江的水 平静缓慢 驶向你的远方 追随 是我的远方 走在宽阔的马路上 期望...
    艾拉夫阅读 224评论 0 1
  • 以前没考研的时候看到一句话 路还有一半你怎么就不走了 那时候想啊,考研无非就是再经历一次高三罢了,想想自己的高三也...
    桑珞阅读 313评论 0 0