python 内存管理,内存泄漏

内存管理机制

Python的内存管理内存总共分为4层(Layer0-3):

第一层Layer1的仅仅是对malloc的简单包装,raw memory,目的是为了兼容各个操作系统,因为不同的操作系统调用malloc的时候可能会有不同的行为结果;第二层Layer2是内存管理机制的核心,其中gc就是在这一层发挥至关重要的作用。第三层,是对象缓冲池,如python对一些对象的直接操作,包括int,list等。
对于可能被经常使用、而且是immutable的对象,如bool类型,元祖类型,小的整数、长度较短的字符串等,python会缓存在layer3,直接供python调用,避免频繁创建和销毁。

>>> a,b=1234567890123,1234567890123
>>> a is b
True
>>> a,b=(1,2,3,'a'),(1,2,3,'a')
>>> a is b
False
>>> a,b=('a'),('a')
>>> a is b
True

当一个对象逻辑上不被使用了,但并没有被释放,那么就存在内存泄露,很有可能会造成程序效率低下甚至崩溃;
Python分配内存的时候又分为大内存和小内存。大小以256字节为界限,对于大内存使用Malloc进行分配,而对于小内存则使用内存池进行分配。由于小内存的分配和释放是频繁的,因此内存池的使用大大提高了python的执行效率。

引用计数

在python中大多数对象的生命周期都是通过引用计数来管理的,引用计数也是一种最直观最简单的垃圾收集技术
每个python对象都有一个引用计数器,用于记录多少变量指向这个对象,可以通过sys模块的getrefcount查询获得。

>>> sys.getrefcount({'a':1})
1
>>> sys.getrefcount(1)
590

每一个对象都会维护一个引用计数器,当一个对象被引用的时候,它的计数器就+1,当一个对象的引用被销毁时,计数器-1,当这个对象的引用计数为0的时候,说明这个对象已经没有使用了,可以被释放,就会被回收,具有实时性。由于引用计数需要维护计数器等额外的操作,为了与引用计数搭配,在内存的分配和释放上获得最高的效率,python因此设计了大量的内存池机制。
下面这些情况引用计数+1:

  1. 对象被创建:a=4
  2. 引用被复制:y=x
  3. 被作为参数传递给函数:f(x)
  4. 作为容器对象的一个元素:a=[1,x]

下面这些情况引用计数-1

  1. 离开作用域。比如f(x)函数结束时,x指向的对象引用减1。
  2. 引用被显式的销毁:del x
  3. 对象的一个别名被赋值给其他对象:y=1
  4. 对象从一个容器对象中移除:l.remove(x)
  5. 容器对象本身被销毁:del l。

python 的内存管理主要以引用计数为主,引用计数机制能释放大部分无用对象,除了一种情况,循环引用,因为循环引用的对象引用计数器永不为0.
循环引用,就是一个对象直接或者间接引用自己本身,导致计数器不为0:

class Test(object):
    pass
t1 = Test()
t1.a = t1

a, b = Test(), Test()
a.attr_b = b
b.attr_a = a
l1=[]
l2=[]
l1.append(l2)
l2.append(l1)

标记清除

标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用问题。标记清除和分代回收就是为了解决循环引用而生的。
标记清除会使用垃圾收集监控对象,讲对象放到链表上,被垃圾收集监控的对象并非只有垃圾收集机制才能回收,正常的引用计数就能销毁一个被纳入垃圾收集机制监控的对象。虽然有很多对象挂在垃圾收集机制监控的链表上,但实际更多时候,是引用计数机制在维护这些对象,只有对引用计数无能为力的循环引用,垃圾收集机制才起作用,事实上,除循环引用外的对象,垃圾收集机制是无能为力的,因为挂在垃圾收集机制上的对象都是引用计数不为0的,如果是0,早就被引用计数清理了。

del x 并不一定会调用__del__方法,只有引用计数 == 0时,__del__()才会被执行,如果一个Python对象定义了__del__这个方法, Python的垃圾回收机制即使发现该对象不可到达 也不会释放他. 原因是__del__这个方式是当一个Python对象引用计数为0即将被删除前调用用来做清理工作的.由于垃圾回收找到的需要释放的对象中往往存在循环引用的情况, 对于循环引用的对象a和b, 应该先调用哪 一个对象的__del__是无法决定的,当执行垃圾回收的时候,会将循环引用中定义了__del__函数的类实例放到gc.garbage列表, 因此Python垃圾回收机制就放弃释放这些对象,会造成事实上的内存泄露, 转而将这些对象保存起来, 应避免在代码中定义__del__方法.

import time
class Test(object):
    def __init__(self, name):
        self.__name = name

    def __del__(self):
        print("__del__方法被调用")
# 创建对象
cat = Test("猫")
cat2 = cat
del cat
del cat2
time.sleep(10)

垃圾回收时,Python不能进行其它的任务,会造成程序卡顿。频繁的垃圾回收将大大降低Python的工作效率。当Python运行时,会记录其中分配对象和取消分配对象的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。

>>> import gc
>>> print(gc.get_threshold())
(700, 10, 10)
>>> 

700是垃圾回收启动的阈值,后面两个10和分代回收有关,也就是新增对象与释放对象的差值为700时,进行一次垃圾回收,主要目标是循环引用,这个时候会造成卡顿

gc.enable(); gc.disable(); gc.isenabled() #开启gc(默认);关闭gc;判断gc是否开启
gc.collection() #执行一次垃圾回收,不管gc是否处于开启状态都能使用
gc.set_threshold(t0, t1, t2)  #设置垃圾回收阈值; 
gc.get_threshold() # 获得当前的垃圾回收阈值
gc.get_objects() #获取所有被垃圾回收器监控管理的对象
gc.get_referents(obj) #返回obj对象直接指向的对象
gc.get_referrers(obj) #返回所有直接指向obj的对象

分代回收

同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象
Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。get_threshold()返回的(700, 10, 10)返回的两个10。也就是说,每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收。理论上,存活时间久的对象,使用的越多,越不容易被回收,这也是分代回收设计的思想。

内存泄漏

本文是在python2环境下

发生内存泄漏的两中情况:
第一是对象被另一个生命周期特别长的对象所引用
第二是循环引用中的对象定义了_del_函数

检测内存泄漏的工具有很多,这里列举几种常见且有用的工具:

  • objgraph python2,3下都可使用
  • tracemalloc python3下使用
  • pympler

objgraph

文档地址:https://mg.pov.lt/objgraph/

# dot t.dot -T png -o pic.png
count(typename) #返回该类型对象的数目,其实就是通过gc.get_objects()拿到所用的对象,然后统计指定类型的数目。
by_type(typename) #返回该类型的对象列表。线上项目,可以用这个函数很方便找到一个单例对象
show_most_common_types(limits = 10)# 打印实例最多的前N(limits)个对象,调用前,最好先gc.collet一下
show_backrefs() #生成有关objs的引用图,看出看出对象为什么不释放。
find_backref_chain(obj, predicate, max_depth=20, extra_ignore=()) #找到一条指向obj对象的最短路径,且路径的头部节点需要满足predicate函数 (返回值为True)可以快捷、清晰指出 对象的被引用的情况,后面会展示这个函数的威力
show_chain() # 将find_backref_chain 找到的路径画出来。
show_growth 可以看出自上次调用后,对象的增长情况
# -*- coding:utf-8 -*-
import time,gc,objgraph

class Test(object):
    def __init__(self, name):
        self.__name = name

    def __del__(self):
        print("__del__ is called!")
# 创建对象

class L(list):
    def __del__(self):
        print('list')

def leak():
    objgraph.show_growth()
    a = Test("a")
    b = Test("b")
    # c=a
    a.attrb = b
    b.attra = a

    l1 = L([1,2])
    l2 = L([3,4])
    l1.append(l2)
    l2.append(l1)

    
leak()
# gc.collect()
print('----------')
objgraph.show_growth()

当定位到哪个对象存在内存泄漏,就可以用show_backrefs查看这个对象的引用链。

# -*- coding:utf-8 -*-
import time,gc,objgraph

class Test(object):
    def __init__(self, name):
        self.__name = name

    def __del__(self):
        print("__del__ is called!")
# 创建对象

class L(list):
    def __del__(self):
        print('list')

def leak():
    # objgraph.show_growth()
    a = Test("a")
    b = Test("b")
    # c=a
    a.attrb = b
    b.attra = a

    del a,b

    l1 = L([1,2])
    l2 = L([3,4])
    l1.append(l2)
    l2.append(l1)

    
leak()
gc.collect()
print(gc.garbage)
print('----------')
# objgraph.show_growth()
objgraph.show_backrefs(objgraph.by_type('Test')[0], max_depth = 10, filename = 'pic.png')
# objgraph.show_backrefs(objgraph.by_type('Test')[0], extra_ignore=(id(gc.garbage),),  max_depth = 10, filename = 'pic.png')

上图所示,Test类的对象存在循环引用,并且无法用gc清除,因为循环引用对象定义了_del_方法。另外,可以看见gc.garbage(类型是list)也引用了这两个对象,原因在于当执行垃圾回收的时候,会将定义了del函数的类实例(被称为uncollectable object)放到gc.garbage列表,因此,也可以直接通过查看gc.garbage来找出定义了del的循环引用。在这里,通过增加extra_ignore来排除gc.garbage的影响。代码越复杂,相互之间的引用关系越多,show_backrefs越难以看懂。这个时候就可以使用show_chain和find_backref_chain

# -*- coding:utf-8 -*-
import time,gc,objgraph

class Test(object):
    def __init__(self, name):
        self.__name = name

    def __del__(self):
        print("__del__ is called!")
# 创建对象

class L(list):
    def __del__(self):
        print('list')

def leak():
    # objgraph.show_growth()
    a = Test("a")
    b = Test("b")
    # c=a
    a.attrb = b
    b.attra = a

    l1 = L([1,2])
    l2 = L([3,4])
    l1.append(l2)
    l2.append(l1)

    
leak()
# gc.collect()
print('----------')
# objgraph.show_growth()
# objgraph.show_backrefs(objgraph.by_type('Test')[0], max_depth = 10, filename = 'chain.png')
objgraph.show_chain(
    objgraph.find_backref_chain(
        objgraph.by_type('Test')[0],
        objgraph.is_proper_module
    ),
    filename='chain.png'
)

对象定义了_del_方法,且存在循环引用,垃圾回收回收不了。
python2上述代码经测试正常,但是python3报错。难道是python3改进了?待确认,python3的del加gc回收 是否可以消除循环引用,答案是Python 3.4以后都可以自动处理。

另外,关于内存泄漏的定位,还可设置gc为debug模式,打印出不可回收对象,从而排查出可能发生内存泄漏的对象。

# -*- coding:utf-8 -*-
import time,gc,objgraph

class Test(object):
    def __init__(self, name):
        self.__name = name

    def __del__(self):
        print("__del__ is called!")
# 创建对象

class L(list):
    def __del__(self):
        print('list')

def leak():
    objgraph.show_growth()
    a = Test("a")
    b = Test("b")
    # c=a
    a.attrb = b
    b.attra = a

    del a,b

    l1 = L([1,2])
    l2 = L([3,4])
    l1.append(l2)
    l2.append(l1)
gc.set_debug(gc.DEBUG_LEAK)
# gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
leak()

print('----------')

objgraph.show_growth()

tracemalloc

tracemalloc 是python3内置库,非常轻量,可以用于追踪内存的使用情况,功能强大,用法也很简单,遗憾的是python2不支持。https://docs.python.org/3/library/tracemalloc.html

例:

import tracemalloc

tracemalloc.start() # 开始跟踪内存分配

test = [i for i in range(100000)]

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')  # lineno,逐行统计;filename,统计整个文件内存
for stat in top_stats:
    print(stat)

结果:

/Users/mac/temp/makemoney_admin_flask/app/test.py:5: size=3533 KiB, count=99745, average=36 B

从结果来看,

文件第5行消耗了3533 KiB的内存。

如果想统计某段程序的内存情况,可以比较两段快照之间的内存,如下:

import tracemalloc

tracemalloc.start()
# ... start your application ...

snapshot1 = tracemalloc.take_snapshot()
test1 = [i for i in range(100000)]
test2 = [i for i in range(100000)]
# ... call the function leaking memory ...
snapshot2 = tracemalloc.take_snapshot()

top_stats = snapshot2.compare_to(snapshot1, 'lineno')

print("[ Top 10 differences ]")
for stat in top_stats[:10]:
    print(stat)

结果 :

/Users/mac/temp/makemoney_admin_flask/app/test.py:8: size=3532 KiB (+3532 KiB), count=99744 (+99744), average=36 B
/Users/mac/temp/makemoney_admin_flask/app/test.py:7: size=3532 KiB (+3532 KiB), count=99744 (+99744), average=36 B
/Users/mac/temp/makemoney_admin_flask/app/test.py:6: size=576 B (+576 B), count=1 (+1), average=576 B
/Users/mac/anaconda3/lib/python3.6/tracemalloc.py:387: size=96 B (+96 B), count=2 (+2), average=48 B
/Users/mac/anaconda3/lib/python3.6/tracemalloc.py:524: size=56 B (+56 B), count=1 (+1), average=56 B
/Users/mac/anaconda3/lib/python3.6/tracemalloc.py:281: size=40 B (+40 B), count=1 (+1), average=40 B

从打印结果得知,消耗内存的程序段分布,可以知道哪些代码消耗内存较大,分析具体内存泄漏的情况,非常有用。

pympler

也是一种可以排查追踪内存泄漏的工具,参考文档https://pythonhosted.org/Pympler/

# -*- coding:utf-8 -*-
from pympler import tracker


tr = tracker.SummaryTracker()

import time,gc,objgraph

class Test(object):
    def __init__(self, name):
        self.__name = name

    def __del__(self):
        print("__del__ is called!")
# 创建对象

class L(list):
    def __del__(self):
        print('list')

def leak():

    a = Test("a")
    b = Test("b")
    # c=a
    a.attrb = b
    b.attra = a

    # del a,b

    l1 = L([1,2])
    l2 = L([3,4])
    l1.append(l2)
    l2.append(l1)

# gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
tr.print_diff()
leak()
print('----------')
tr.print_diff()

结果:

                       types |   # objects |   total size
============================ | =========== | ============
                        list |        3539 |    362.31 KB
                         str |        4148 |    297.93 KB
                        dict |          48 |     72.38 KB
                        code |         267 |     33.38 KB
                        type |          23 |     20.30 KB
                         int |         327 |      7.66 KB
            _sre.SRE_Pattern |          13 |      6.00 KB
                         set |           6 |      5.86 KB
                     weakref |          31 |      2.66 KB
                       tuple |          39 |      2.36 KB
           getset_descriptor |          24 |      1.69 KB
         function (__init__) |          13 |      1.52 KB
          wrapper_descriptor |          17 |      1.33 KB
  builtin_function_or_method |          11 |    792     B
                    property |           8 |    704     B
----------


                  types |   # objects |   total size
======================= | =========== | ============
                   list |         221 |     20.75 KB
                    str |         223 |     12.78 KB
                   dict |           2 |    560     B
     <class '__main__.L |           2 |    224     B
                    int |           8 |    192     B
  <class '__main__.Test |           2 |    128     B

主要看print('----------')之后的。

弱引用

弱引用模块是weakref可以用于消除循环引用。

# -*- coding:utf-8 -*-
import time,gc,objgraph

import weakref

class Test(object):
    def __del__(self):
        print("del is called")

def callback(self):
    print("callback")

def leak():
    t1 = Test()
    t2 = Test()
    t1.arrt2 = weakref.proxy(t2)
    t2.arrt1 = weakref.proxy(t1)

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

推荐阅读更多精彩内容

  • python内存管理是通过引用计数来实现的。当对象的引用计数为0时,会被gc回收。 为了探索对象在内存的存储,我们...
    冬季恋歌1218阅读 1,643评论 0 2
  • 1.元类 1.1.1类也是对象 在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。在Python中这...
    TENG书阅读 1,247评论 0 3
  • 一元类 1类也是对象 在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。在Python中这一点仍然成...
    五行缺觉阅读 1,036评论 0 1
  • [TOC] 内存管理 一、托管堆基础 在面向对象中,每个类型代表一种可使用的资源,要使用该资源,必须为代表资源的类...
    _秦同学_阅读 3,772评论 0 3
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    apkcore阅读 1,216评论 2 7