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)