• 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. 这只是皮毛。
  • 相关阅读:
    Django框架介绍
    CSS基础
    BOM与DOM操作
    JavaScript
    PHP Socket编程 之 fsockopen链接https时OpenSSL错误
    PHP Socket编程 之 深入解析fsockopen与pfsockopen的区别
    PHP Socket编程 之 使用fsockopen伪造IP
    PHP Socket编程 之 利用 fsockopen() 函数开放端口扫描器的实例
    PHP Socket编程 之 fsockopen指定ip获取远程数据 及 301自动跳转
    PHP Socket编程 之 抓取数据遇到Transfer-Encoding chunked
  • 原文地址:https://www.cnblogs.com/wuyongqiang/p/11164716.html
Copyright © 2020-2023  润新知