zoukankan      html  css  js  c++  java
  • python内存管理--垃圾回收

    python 程序在运行的时候,需要在内存中开辟出一块空间,用于存放运行时产生的临时变量;计算完成后,再将结果输出到永久性存储器中。如果数量过大,
    内存空间管理不善,就会出现 OOM(out of memory), 俗称爆内存,程序可能被操作系统终止。

    引用计数

    Python 中一切皆对象。因此,一切变量,本质上都是对象的一个指针。

    import os
    import psutil
    
    # 显示当前 python 程序占用的内存大小
    def show_memory_info(hint):
        pid = os.getpid()  # 进程ID
        p = psutil.Process(pid)  # 返回进程对象,不传 pid 默认会获取当前的pid
        info = p.memory_full_info()  # pfullmem 对象
        memory = info.uss / 1024. / 1024
        print(f'{hint} memory used: {memory} MB')
    
    def func():
        show_memory_info('initial')
        a = [i for i in range(10000000)]
        show_memory_info('after a created')
    
    func()
    show_memory_info('finished')
    """
    运行结果:
    initial memory used: 8.32421875 MB
    after a created memory used: 396.21484375 MB
    finished memory used: 9.0078125 MB
    """

    示例中调用函数 func(), 在列表 a 被创建之后,内存占用迅速增加到了 396MB, 而在函数调用之后,内存返回正常。

    这是因为函数内部声明的列表a是局部变量,在函数返回后,局部变量的引用会注销掉;此时,列表a所指代对象的引用计数为0,python便会执行垃圾
    回收,因此之前占用的大量内存被释放了。

    import os
    import psutil
    
    def show_memory_info(hint):
        pid = os.getpid()
        p = psutil.Process()
        info = p.memory_full_info()
        memory = info.uss / 1024 / 1024
        print(f'{hint} memory used: {memory} MB')
    
    def func():
        show_memory_info('initial')
        global a
        a = [i for i in range(10000000)]
        show_memory_info('after a created')
    
    func()
    show_memory_info('finished')
    """
    运行结果:
    initial memory used: 8.4765625 MB
    after a created memory used: 395.86328125 MB
    finished memory used: 395.86328125 MB
    """

    global a 将 a 声明为全局变量。那么,即使函数返回后,列表的引用依然存在,于是对象就不会被垃圾回收掉,依然占用大量内存。

    同样,如果我们把生成的列表返回,然后在主程序中接收,那么引用依然存在,垃圾回收就不会触发,大量内存仍然被占用着:

    def func():
        show_memory_info('initial')
        a = [i for i in derange(10000000)]
        show_memory_info('after a created')
        return a
    
    a = func()
    show_memory_info('finished')

    这是最常见的几种情况。

    python内部的引用计数机制

    import sys
    
    a = []
    # 两次引用,一次来自 a,一次来自 getrefcount
    print(sys.getrefcount(a))
    
    def func(a):
        # 四次引用,a,python 的函数调用栈,函数参数,和 getrefcount
        print(sys.getrefcount(a))
    
    func(a)
    # 两次引用,一次来自 a,一次来自 getrefcount,函数 func 调用已经不存在
    print(sys.getrefcount(a))

    sys.getrefcount() 这个函数,可以查看一个变量的引用计数。它本身也会引入一次计数。

    在函数调用时,会产生额外的两次引用,一个来自函数栈,一个来自函数参数。

    import sys
    
    a = []
    
    print(sys.getrefcount(a))  # 两次
    
    b = a
    
    print(sys.getrefcount(a))  # 三次
    
    c = b
    d = b
    e = c
    f = e
    g = d
    
    print(sys.getrefcount(a))  # 八次

    理解了引用这个概念后,引用释放是一种非常自然和清晰的思想。相比C语言里,你需要使用free去手动释放内存,python 自带垃圾回收。
    如果想手动回收可以先 del a 来删除一个对象;然后强制调用 gc.collect(),即可手动启动垃圾回收。

    import gc
    import os
    import psutil
    
    def show_memory_info(hint):
        pid = os.getpid()
        p = psutil.Process(pid)
        info = p.memory_full_info()
        memory = info.uss / 1024 / 1024
        print(f'{hint} memory used: {memory} MB')
    
    show_memory_info('initial')
    
    a = [i for i in range(10000000)]
    
    show_memory_info('after a created')
    
    del a
    gc.collect()
    
    show_memory_info('finish')

    引用计数为0是垃圾回收的充要条件么?

    循环引用

    如果有两个对象,它们互相引用,并且不再被别的对象引用,那么它们应该被垃圾回收么?(python自带的不会,手动却可以)

    import os
    import psutil
    
    def show_memory_info(hint):
        pid = os.getpid()
        p = psutil.Process(pid)
        info = p.memory_full_info()
        memory = info.uss / 1024 / 1024
        print(f'{hint} memory used: {memory} MB')
    
    def func():
        show_memory_info('initial')
        a = [i for i in range(10000000)]
        b = [i for i in range(10000000)]
        show_memory_info('after a, b created')
        a.append(b)
        b.append(a)
    
    func()
    show_memory_info('finished')
    """
    运行结果:
    initial memory used: 8.48046875 MB
    after a, b created memory used: 783.83203125 MB
    finished memory used: 783.83203125 MB
    """

    这里 a 和 b 互相引用,并且,作为局部变量,在函数 func 调用结束后,a 和 b 这两个指针从程序意义上已经不存在了。但是,很明显,依然有内存
    占用。因为互相引用,导致它们的引用数都不为0。

    如果这段代码运行在生产环境中,哪怕 a 和 b 一开始占用的空间不是很大,但经过长时间的运行后,所占内存会越来越大,最终会撑爆服务器。

    互相引用还是很容易发现的,更隐蔽的情况是出现一个引用环,在工程代码比较复杂的情况下,引用环很难被发现。

    解决这类问题,我们可以通过手动垃圾回收,即显式的调用 gc.collect(), 来启动垃圾回收。

    import gc
    import os
    import psutil
    
    def show_memory_info(hint):
        pid = os.getpid()
        p = psutil.Process(pid)
        info = p.memory_full_info()
        memory = info.uss / 1024 / 1024
        print(f'{hint} memory used: {memory} MB')
    
    def func():
        show_memory_info('initial')
        a = [i for i in range(10000000)]
        b = [i for i in range(10000000)]
        show_memory_info('after a, b created')
        a.append(b)
        b.append(a)
    
    func()
    gc.collect()  # 手动垃圾回收
    show_memory_info('finished')
    """
    运行结果:
    initial memory used: 8.453125 MB
    after a, b created memory used: 783.90625 MB
    finished memory used: 9.37109375 MB
    """

    虽然 a,b 的引用计数不为0,但是我们也可以通过 gc.collect() 进行垃圾回收

    python 使用标记清除(mark-sweep)算法和分代收集(generational), 来启用针对循环引用的自动垃圾回收。

    先来看看标记清除算法。我们先用图论来理解不可达的概念。对于一个有向图,如果从一个节点出发进行遍历,并标记其经过的所有节点;
    那么,在遍历结束后,所有没有被标记的节点,我们称之为不可达节点。显然,这些节点的存在没有任何意义,我们就需要对它们进行垃圾回收。

    当然,每次都遍历全图,对于python而言是一种巨大的性能浪费。所以在python垃圾回收实现中,mark-sweep 使用双向链表维护了一个数据结构,
    并且只考虑容器类的对象(只有容器类对象才有可能产生循环引用)。

    而分代收集算法,则是另一个优化手段。

    python将所有对象分为三代。刚刚创立的对象是第0代;经过一次垃圾回收后,依然存在的对象,便会依次从上一代挪到下一代。而每一代启动自动垃圾
    回收的阈值,则是可以单独指定的。当垃圾回收器中新增对象(新建的对象)减去删除对象(手动调用del删除的对象、函数运行结束释放的对象等)达到相应的阈值时,就会对这一代对象启动垃圾回收。

    事实上,分代收集基于的思想是,新生的对象更有可能被垃圾回收,而存活更久的对象也有更高的概率继续存活。因此,通过这种做法,可以节约不少
    计算量,从而提高python的性能。

    引用计数是其中最简单的实现,引用计数并非充要条件。还有其他的可能性,比如循环引用就是其中之一。

    调试内存泄漏

    虽然有了自动回收机制,还是会出现内存泄露的情况。

    可以通过 objgraph(一个可视化引用关系的包)。在这个包中,主要关注两个函数,第一个是 show_refs(), 它可以生成清晰的引用关系图。
    需要手动下载安装graphviz,然后将其 bin 目录放入到环境变量中,才能出来图片。在 jupyter notebook 中可以直接显示图片。但是在pycharm中
    会显示图片地址,需要自己去手动打开。

    通过下面这段代码和生成的引用调用图,你能非常直观的发现,有两个list互相引用,说明这里极有可能引起内存泄漏。这样一来,再去代码层排查就容易多了。

    import objgraph
    import os
    os.environ["PATH"] += os.pathsep + r'D:GoogleDownloadgraphviz-2.38
    eleasein'
    a = [1, 2, 3]
    b = [4, 5, 6]
    a.append(b)
    b.append(a)
    objgraph.show_refs([a])

    而另一个非常有用的函数是 show_backrefs()

    import objgraph
    import os
    os.environ["PATH"] += os.pathsep + r'D:GoogleDownloadgraphviz-2.38
    eleasein'
    a = [1, 2, 3]
    b = [4, 5, 6]
    a.append(b)
    b.append(a)
    objgraph.show_backrefs([a])

    这个代码显示的图片比之前的复杂的多。show_backrefs() 有很多有用的参数,比如层数限制(max_depth)、宽度限制(too_many)、输出格式控制(filename output)、
    节点过滤(filter, extra_ignore)等。

    总结

    1. 垃圾回收是 python 自带的机制,用于自动释放不会再用到的内存空间;
    2. 引用计数是其中最简单的实现,这只是充分非必要条件,因为循环引用需要通过不可达判定,来确定是否可以回收;
    3. Python 的自动回收算法包括标记清除和分代收集,主要针对的是循环引用的垃圾收集;
    4. 调试内存泄漏的工具:objgraph;
    5. 这只是皮毛。
     
     
     
  • 相关阅读:
    Linux文件属性
    [Oracle] Listener的动态注册
    jQuery easyUI Pagination控件自定义div分页(不用datagrid)
    桂林电子科技大学出校流量控制器Android版1.0.0
    php使用check box
    Python windows ping
    Python selenium chrome 环境配置
    Linux wget auto login and backup database
    PyQt4 ShowHMDB show sqlite3 with QTableWidget summary
    PyQt4 py2exe 打包 HardwareManager
  • 原文地址:https://www.cnblogs.com/sea520/p/11168522.html
Copyright © 2011-2022 走看看