前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >python 内存泄漏

python 内存泄漏

作者头像
为为为什么
发布2022-08-09 17:17:50
2.6K0
发布2022-08-09 17:17:50
举报
文章被收录于专栏:又见苍岚

python 自带内存回收机制,但时不时也会发生内存泄漏的问题,本文记录 Python 内存泄漏相关内容。

内存泄漏

程序运行时都需要在内存中申请资源用于存放变量,python 在处理内存中的变量时会调用垃圾回收机制,会留心那些永远不会被引用的变量并及时回收变量,删除并释放相关资源。

  • Python 会为变量维护引用记数器 ,这是 Python 垃圾回收机制的基础,如果一个对象的引用数量不为 0 那么是不会被垃圾回收的;
  • 因此如果在程序中恰好有方法造成了循环引用或通过某种方式使得引用数量无法降至0,则变量无法被回收, 在批量处理大量任务时内存占用便会不断提升
  • 内存泄漏最直接的现象就是 Python 占用的内存量不断增加,直至内存溢出

问题复现

  • 以全局变量阻止垃圾回收为例:
代码语言:javascript
复制
from time import sleep
import numpy as np
import tqdm


if __name__ == '__main__':
    mem_list = []
    for _ in tqdm.tqdm(range(10)):
        huge_mem = np.random.random([1000, 1000, 100])
        mem_list.append(huge_mem)
        sleep(3)
    pass

  • 类似的泄漏现象即是如此,逐渐吃光内存

调试手段

引用数量
  • 可以通过 sys.getrefcount 函数得到对象引用数量
代码语言:javascript
复制
import sys
import numpy as np


if __name__ == '__main__':

    # 建立对象
    test = {}
    # 默认对象引用数量为 2
    print(sys.getrefcount(test)) 		# 2 
    # 为该对象建立引用
    quo = test
    # 添加引用后,二者引用数量为 3
    print(sys.getrefcount(quo)) 		# 3 
    print(sys.getrefcount(test)) 		# 3 
    # 删除引用
    del quo
    # 删除引用后,引用数变回 2
    print(sys.getrefcount(test))		# 2
    # 重新添加相同的引用
    quo = test
    # 和之前一样,引用数变为 3
    print(sys.getrefcount(quo))			# 3
    print(sys.getrefcount(test))		# 3
    # 创建新对象(空列表) 覆盖原始 test 对象(空字典)
    test = []
    # quo 保持对空字典的引用,新对象仅有自己的引用,因此二者都为 2
    print(sys.getrefcount(quo))			# 2
    print(sys.getrefcount(test))		# 2
    print(test, quo)					# [] {}
    pass

  • 应用数量随着引用情况的变化而变化,可以查看变量的引用数是否清空来调试内存泄漏的情况
objgraph
  • objgraph 是一个用于诊断内存问题的工具,可以通过该工具打印对象数量,以此观察内存变化与对象数量的关系。
  • 安装工具
代码语言:javascript
复制
pip install objgraph 

  • 调试示例
代码语言:javascript
复制
import objgraph

if __name__ == '__main__':
    test_list = []
    for _ in range(10):
        test_list.append([])
        objgraph.show_most_common_types(limit=7)
        print()

-->
function           11180
dict               6615
tuple              4385
wrapper_descriptor 2924
list               2643
weakref            2377
member_descriptor  1950

function           11180
dict               6615
tuple              4398
wrapper_descriptor 2924
list               2644
weakref            2377
member_descriptor  1950

function           11180
dict               6615
tuple              4398
wrapper_descriptor 2924
list               2645
weakref            2377
member_descriptor  1950

function           11180
dict               6615
tuple              4398
wrapper_descriptor 2924
list               2646
weakref            2377
member_descriptor  1950

function           11180
dict               6615
tuple              4398
wrapper_descriptor 2924
list               2647
weakref            2377
member_descriptor  1950

...

示例中不断增加 list 对象,在计数器中可以看到仅 list 对象不断递增

弱引用

  • 影响 Python 垃圾回收的核心问题在于对象引用,而Python 内置了一种特殊的引用,该引用不会增加引用数,可以作为垃圾回收良好的技术
  • 详细介绍移步 Python 弱引用 查看

循环引用

大多数内存爆炸增长都是由于将变量存在python 内置可变容器中导致的,比较容易排查,一种更加隐蔽的情况为循环引用

问题复现
代码语言:javascript
复制
import sys
import numpy as np


class MemLeak:
    def __init__(self, name):
        self.name = name
        self.huge_memory = np.random.random([1000, 1000, 100])
        self.child = None
        self.parent = None
    
    def __del__(self):
        print(f"对象 {self.name} 已经被删除。")
    
    def ref_count(self):
        print(f"对象 {self.name} 当前引用数为 {sys.getrefcount(self)}")


if __name__ == '__main__':
    fir = MemLeak('first')
    fir.ref_count()
    fir = []

    fir = MemLeak('first')
    sec = MemLeak('second')
    fir.ref_count()
    sec.ref_count()

    # 循环引用
    fir.child = sec
    sec.parent = fir
    fir.ref_count()
    sec.ref_count()

    del sec
    fir.ref_count()
    del fir

    pass

  • 可以看到,正常情况下,创建的对象被覆盖后,如果引用数归零(line 22),则 python 会自动调用回收机制,并同时清空内存
  • 当出现循环引用时,对象的引用数增加了,即使手动 del 对象该对象在内存中也不会被删除,仅会在 python 程序退出时释放内存,也就是循环引用导致了内存泄漏
解决方案
  • 我们需要打破循环引用导致的引用数增加,在不改变代码逻辑的情况下,可以将部分 引用转换为弱引用,在保证功能不变的前提下打破计数的引用环,使得对象删除时内存得以正确释放
  • 修正代码
代码语言:javascript
复制
import sys
import numpy as np
import weakref


class MemLeak:
    def __init__(self, name):
        self.name = name
        self.huge_memory = np.random.random([1000, 1000, 100])
        self.child = None
        self.parent = None
    
    def __del__(self):
        print(f"对象 {self.name} 已经被删除。")
    
    def ref_count(self):
        print(f"对象 {self.name} 当前引用数为 {sys.getrefcount(self)}")


if __name__ == '__main__':
    fir = MemLeak('first')
    fir.ref_count()
    fir = []

    fir = MemLeak('first')
    sec = MemLeak('second')
    fir.ref_count()
    sec.ref_count()

    # 循环引用
    fir.child = weakref.ref(sec)
    sec.parent = fir
    fir.ref_count()
    sec.ref_count()

    del sec
    fir.ref_count()
    del fir

    pass

  • 通过调试过程可以看到,在使用弱引用打破计数引用环后,删除对象可以正常释放内存,避免了之前的内存泄漏
  • 使用弱引用时需要注意,弱引用不计入引用数量,因此如果需要某个变量存在,必须给他一个正经的引用名称,如果直接用弱引用指向创建的对象,该对象会由于引用数为0而在创建后直接被删除
代码语言:javascript
复制
import sys
import numpy as np
import weakref


class MemLeak:
    def __init__(self, name):
        self.name = name
        self.huge_memory = np.random.random([1000, 1000, 100])
        self.child = None
        self.parent = None
    
    def __del__(self):
        print(f"对象 {self.name} 已经被删除。")
    
    def ref_count(self):
        print(f"对象 {self.name} 当前引用数为 {sys.getrefcount(self)}")


if __name__ == '__main__':
    weakr = weakref.ref(MemLeak('test'))

    
-->
对象 test 已经被删除。
  • 然而在实际应用中我们不是很喜欢手动删除所有对象,毕竟不写 C++ 好多年了,是否有方案即解决循环引用难以回收的问题,又可以方便地通过直接覆盖变量的方式方便 python 资源自动回收呢,我在这里做了一个尝试供后人参考
代码语言:javascript
复制
import sys
import numpy as np
import weakref


class MemLeak:
    def __init__(self, name):
        self.name = name
        self.huge_memory = np.random.random([1000, 1000, 100])
        self.child = None
        self.parent = None
    
    def __del__(self):
        print(f"对象 {self.name} 已经被删除。")
    
    def ref_count(self):
        print(f"对象 {self.name} 当前引用数为 {sys.getrefcount(self)}")


if __name__ == '__main__':
    fir = MemLeak('first')
    fir.ref_count()

    fir.child = MemLeak('second')
    fir.child.parent = weakref.ref(fir)
    fir.ref_count()
    fir = []

    pass

-->
对象 first 当前引用数为 4
对象 first 当前引用数为 4
对象 first 已经被删除。
对象 second 已经被删除。

  • 思路就是根节点中的变量维护其余节点的唯一引用,同时其余节点反向引用时使用弱引用,这样根节点和其他节点都仅有一个有效引用,并且其他节点的引用会随着根节点的消失而清空,这样仅通过覆盖根节点即完成了循环引用中所有变量的销毁回收

字典缓存

问题复现
  • 字典经常用来保存已经生成的变量,避免使用同一个结果的函数多次生成
  • 然而临时结果在无人引用时由于字典的引用会导致保存的对象不会自动释放
代码语言:javascript
复制
import mtutils
import numpy as np
import weakref

dict_leak = {}


if __name__ == '__main__':

    for index in mtutils.tqdm(range(6)):
        key = mtutils.create_uuid()
        value = np.random.random([1000, 1000, 100])
        dict_leak[key] = value

  • 尽管原始创建的变量已经被覆盖销毁,由于在字典中仍保留了他们的引用,因此内存不会被释放
解决方案
  • 解决的思路还是从引用数上入手,我们的需求是令那些不再有人能引用到的 value 被清理回收
  • 实际上,用字典缓存数据对象的做法很常用,为此 weakref 模块还提供了两种只保存弱引用的字典对象
    • weakref.WeakKeyDictionary ,键只保存弱引用的映射类(一旦键不再有强引用,键值对条目将自动消失);
    • weakref.WeakValueDictionary ,值只保存弱引用的映射类(一旦值不再有强引用,键值对条目将自动消失);
  • 因此,我们的数据缓存字典可以采用 weakref.WeakValueDictionary 来实现,它的接口跟普通字典完全一样。这样我们不用再自行维护弱引用对象,代码逻辑更加简洁明了
代码语言:javascript
复制
import mtutils
import numpy as np
import weakref

dict_leak = weakref.WeakValueDictionary()


if __name__ == '__main__':

    for index in mtutils.tqdm(range(6)):
        key = mtutils.create_uuid()
        value = np.random.random([1000, 1000, 100])
        dict_leak[key] = value

  • 仅更换了字典定义,python 可以正常执行垃圾回收工作

终极方案

  • 如果无论如何都难以解决内存泄漏的问题,尝试在代码中加入强制垃圾回收的命令

gc 模块是Python的垃圾收集器模块,gc 使用标记清除算法回收垃圾

代码语言:javascript
复制
import gc
# 强制进行垃圾回收 
gc.collect() 

  • 弊端就是这个函数比较慢

参考资料

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年5月12日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 内存泄漏
  • 问题复现
  • 调试手段
    • 引用数量
      • objgraph
      • 弱引用
      • 循环引用
        • 问题复现
          • 解决方案
          • 字典缓存
            • 问题复现
              • 解决方案
              • 终极方案
              • 参考资料
              相关产品与服务
              容器服务
              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档