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
     

主要看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)

你可能感兴趣的:(python 内存管理,内存泄漏)