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)

 

 

 

相关