zoukankan      html  css  js  c++  java
  • Python 内存泄露 内存回收机制

    1、白话垃圾回收

    参考:武沛齐博客中的一篇文章 找不到了

    用通俗的语言解释内存管理和垃圾回收的过程,搞懂这一部分就可以去面试、去装逼了…

    (1) 大管家 refchain

    在Python的C源码中有一个名为refchain的环状双向链表,这个链表比较牛逼了,因为Python程序中一旦创建对象都会把这个对象添加到refchain这个链表中。也就是说他保存着所有的对象。例如:

    age = 18
    name = "武沛齐"

     

    (2) 引用计数器

    在refchain中的所有对象内部都有一个ob_refcnt用来保存当前对象的引用计数器,顾名思义就是自己被引用的次数,例如:

    age = 18
    name = "武沛齐"
    nickname = name

    上述代码表示内存中有 18 和 “武沛齐” 两个值,他们的引用计数器分别为:1、2 。

    当值被多次引用时候,不会在内存中重复创建数据,而是 引用计数器+1 。 当对象被销毁时候同时会让 引用计数器-1,如果引用计数器为0,则将对象从refchain链表中摘除,同时在内存中进行销毁(暂不考虑缓存等特殊情况)。

     

    age = 18
    number = age     # 对象18的引用计数器 + 1
    del age          # 对象18的引用计数器 - 1
    def run(arg):
        print(arg)
    run(number)      # 刚开始执行函数时,对象18引用计数器 + 1,当函数执行完毕之后,对象18引用计数器 - 1 。
    num_list = [11,22,number] # 对象18的引用计数器 + 1

    (3) 标记清除&分代回收

    基于引用计数器进行垃圾回收非常方便和简单,但他还是存在 循环引用 的问题,导致无法正常的回收一些数据,例如:

    v1 = [11,22,33] # refchain中创建一个列表对象,由于v1=对象,所以列表引对象用计数器为1.
    v2 = [44,55,66] # refchain中再创建一个列表对象,因v2=对象,所以列表对象引用计数器为1.
    v1.append(v2)   # 把v2追加到v1中,则v2对应的[44,55,66]对象的引用计数器加1,最终为2.
    v2.append(v1)   # 把v1追加到v1中,则v1对应的[11,22,33]对象的引用计数器加1,最终为2.
    del v1          # 引用计数器-1
    del v2          # 引用计数器-1

    对于上述代码会发现,执行 del 操作之后,没有变量再会去使用那两个列表对象,但由于循环引用的问题,他们的引用计数器不为0,所以他们的状态:永远不会被使用、也不会被销毁。项目中如果这种代码太多,就会导致内存一直被消耗,直到内存被耗尽,程序崩溃。

    为了解决循环引用的问题,引入了 标记清除 技术,专门针对那些可能存在循环引用的对象进行特殊处理,可能存在循环应用的类型有:列表、元组、字典、集合、自定义类等那些能进行数据嵌套的类型。

    标记清除:创建特殊链表专门用于保存 列表、元组、字典、集合、自定义类等对象,之后再去检查这个链表中的对象是否存在循环引用,如果存在则让双方的引用计数器均 - 1 。

    分代回收:对标记清除中的链表进行优化,将那些可能存在循引用的对象拆分到3个链表,链表称为:0/1/2三代,每代都可以存储对象和阈值,当达到阈值时,就会对相应的链表中的每个对象做一次扫描,除循环引用各自减1并且销毁引用计数器为0的对象。

    // 分代的C源码
    #define NUM_GENERATIONS 3
    struct gc_generation generations[NUM_GENERATIONS] = {
        /* PyGC_Head, threshold, count */
        {{(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)}, 700, 0}, // 0代
        {{(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)}, 10, 0},  // 1代
        {{(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)}, 10, 0},  // 2代
    };

    特别注意:0代和1、2代的threshold和count表示的意义不同。

    • 0代,count表示0代链表中对象的数量,threshold表示0代链表对象个数阈值,超过则执行一次0代扫描检查。

    • 1代,count表示0代链表扫描的次数,threshold表示0代链表扫描的次数阈值,超过则执行一次1代扫描检查。

    • 2代,count表示1代链表扫描的次数,threshold表示1代链表扫描的次数阈值,超过则执行一2代扫描检查。

     

    (4) 情景模拟

    根据C语言底层并结合图来讲解内存管理和垃圾回收的详细过程。

    第一步:当创建对象 age=19 时,会将对象添加到refchain链表中。

    第二步:当创建对象 num_list = [11,22] 时,会将列表对象添加到 refchain 和 generations 0代中。

    第三步:新创建对象使generations的0代链表上的对象数量大于阈值700时,要对链表上的对象进行扫描检查。

    当0代大于阈值后,底层不是直接扫描0代,而是先判断2、1是否也超过了阈值。

    • 如果2、1代未达到阈值,则扫描0代,并让1代的 count + 1 。

    • 如果2代已达到阈值,则将2、1、0三个链表拼接起来进行全扫描,并将2、1、0代的count重置为0.

    • 如果1代已达到阈值,则讲1、0两个链表拼接起来进行扫描,并将所有1、0代的count重置为0.

    对拼接起来的链表在进行扫描时,主要就是剔除循环引用和销毁垃圾,详细过程为:

    • 扫描链表,把每个对象的引用计数器拷贝一份并保存到 gc_refs中,保护原引用计数器。

    • 再次扫描链表中的每个对象,并检查是否存在循环引用,如果存在则让各自的gc_refs减 1 。

    • 再次扫描链表,将 gc_refs 为 0 的对象移动到 unreachable 链表中;不为0的对象直接升级到下一代链表中。

    • 处理 unreachable 链表中的对象的 析构函数 和 弱引用,不能被销毁的对象升级到下一代链表,能销毁的保留在此链表。

      • 析构函数,指的就是那些定义了del方法的对象,需要执行之后再进行销毁处理。

      • 弱引用,

    • 最后将 unreachable 中的每个对象销毁并在refchain链表中移除(不考虑缓存机制)。

    至此,垃圾回收的过程结束。

     

    2、定位内存泄露

    from collections import defaultdict
    from gc import get_objects
    ​
    before = defaultdict(int)
    after = defaultdict(int)
    ​
    # 程序(函数/类...)开始
    for i in get_objects():
        before[type(i)] += 1
    ​
    # 代码块
    """
    """
    ​
    # 程序(函数/类...)结束
    for i in get_objects():
        after[type(i)] += 1
    print([(k, after[k] - before[k]) for k in after if (after[k] - before[k]) > 0])
    

    示例

    import gc
    import insightface
    import numpy as np
    from utils.tools import image_process
    from flask import Flask, request, jsonify
    from collections import defaultdict
    from gc import get_objects
    ​
    before = defaultdict(int)
    after = defaultdict(int)
    ​
    face_detector_app = insightface.app.FaceAnalysis(allowed_modules=['detection', ])
    face_detector_app.prepare(ctx_id=0, det_thresh=0.4, det_size=(160, 160))
    app = Flask(__name__)
    ​
    ​
    @app.route("/api/face_detector", methods=["post"])
    def detection():
        try:
            request_data = request.get_json()
            area_magnification = request_data.get('area_magnification')
            resize_magnification = request_data.get('resize_magnification')
            img = request_data.get('img')
            # 1. base64 转 opencv
            opencv_img = image_process.base64_to_opencv(img)
            # 2. 人脸检测
            refaces = face_detector_app.get(opencv_img)
            if refaces:
                # 程序(函数/类...)开始
                for i in get_objects():
                    before[type(i)] += 1
                reface = refaces[0]
                bbox = reface['bbox'].astype(np.int16).tolist()
                score = (reface['det_score'] * 100).astype(np.int8).tolist()
                cropped_img = image_process.cropp_face_img(opencv_img, bbox, area_magnification)
                resize_img = image_process.opencv_img_resize(cropped_img, resize_magnification)
                change_img_clahe_img = image_process.change_img_clahe(resize_img)
                base64_img = image_process.opencv_to_base64(change_img_clahe_img)
                # 程序(函数/类...)结束
                for i in get_objects():
                    after[type(i)] += 1
                print([(k, after[k] - before[k]) for k in after if (after[k] - before[k]) > 0])
                return jsonify({
                    'code': 200,
                    'msg': 'success',
                    'data': {
                        'faceId': 1,
                        'bbox': bbox,
                        'score': score,
                        'faceBase64': str(base64_img, encoding='utf-8') if base64_img else None
                    }
                })
            else:
                return jsonify({'code': 200, 'msg': 'success', 'data': None})
        except Exception as e:
            print(f'人脸检测异常: {e}')
            return jsonify({'code': 500, 'msg': 'error', 'data': None})
    ​
    if __name__ == '__main__':
        app.run(host="0.0.0.0", port=11000, debug=False, threaded=False)
    

    3、解决内存泄露

    如果内存持续增长定位到后可以使用 gc 强制回收

    注意:不要胡乱删除变量,没啥用(找出关键变量)一般不会出现这种情况,基本上都是第三方库导致的,也是和业务有关系的

    具体情况具体分析

    import gc
    import insightface
    import numpy as np
    from utils.tools import image_process
    from flask import Flask, request, jsonify
    ​
    face_detector_app = insightface.app.FaceAnalysis(allowed_modules=['detection', ])
    face_detector_app.prepare(ctx_id=0, det_thresh=0.4, det_size=(160, 160))
    app = Flask(__name__)
    ​
    ​
    @app.route("/api/face_detector", methods=["post"])
    def detection():
        try:
            request_data = request.get_json()
            area_magnification = request_data.get('area_magnification')
            resize_magnification = request_data.get('resize_magnification')
            img = request_data.get('img')
            # 1. base64 转 opencv
            opencv_img = image_process.base64_to_opencv(img)
            # 2. 人脸检测
            refaces = face_detector_app.get(opencv_img)
            if refaces:
                reface = refaces[0]
                bbox = reface['bbox'].astype(np.int16).tolist()
                score = (reface['det_score'] * 100).astype(np.int8).tolist()
                cropped_img = image_process.cropp_face_img(opencv_img, bbox, area_magnification)
                resize_img = image_process.opencv_img_resize(cropped_img, resize_magnification)
                change_img_clahe_img = image_process.change_img_clahe(resize_img)
                base64_img = image_process.opencv_to_base64(change_img_clahe_img)
                # 强制回收垃圾
                del refaces
                gc.collect()
                return jsonify({
                    'code': 200,
                    'msg': 'success',
                    'data': {
                        'faceId': 1,
                        'bbox': bbox,
                        'score': score,
                        'faceBase64': str(base64_img, encoding='utf-8') if base64_img else None
                    }
                })
            else:
                return jsonify({'code': 200, 'msg': 'success', 'data': None})
        except Exception as e:
            print(f'人脸检测异常: {e}')
            return jsonify({'code': 500, 'msg': 'error', 'data': None})
    ​
    if __name__ == '__main__':
        app.run(host="0.0.0.0", port=11000, debug=False, threaded=False)
    

     

     

     

  • 相关阅读:
    【转载】如果你看完这篇文章还不懂计算机时间,那就掐死我吧
    记录 C#中 LINQ 和 SQL 语句 的一些操作数据集合
    记录mysql 存储过程中循环临时表
    记录一个有趣的dotnet开源库。
    将Quartz.Net用于ASP.NET Core 3.0应用程序中的,并实现通过依赖注入获取其他服务
    web api中接收 复杂类型数组参数(对象数组参数)
    网页支付宝支付,通过form表单提交,在苹果手机上无法跳转
    阿里云服务器上搭建FTP服务器,连接时出现:读取目录列表失败的解决办法
    css选择器及float(浮动)
    盒子四大元素
  • 原文地址:https://www.cnblogs.com/xingxingnbsp/p/15571678.html
Copyright © 2011-2022 走看看