Python中的内存管理:深入分析垃圾回收机制

news2024/11/20 11:50:39

python中有一个名为refchian的环状双向链表,python运行时创建的所有对象都会添加到refchain中。在refchain中的对象PyObject里都有一个ob_refcnt用来保存当前对象的引用计数器,就是该对象被引用的次数,当对象有新引用时ob_refcnt就会增加,当引用他的对象被销毁时,ob_refcnt就会减少。当引用计数器为0时,该对象就会被销毁。

// python对象的核心结构体PyObject
// 源码 Include/object.h
#define PyObject_HEAD                   PyObject ob_base;
#define PyObject_VAR_HEAD      			PyVarObject ob_base;
// 构造一个双向链表
#define _PyObject_HEAD_EXTRA            \
struct _object *_ob_next;                     \
struct _object *_ob_prev;
typedef struct _object {
	_PyObject_HEAD_EXTRA    	// 构造双向链表
	Py_ssize_t ob_refcnt;       // 引用计数器
	PyTypeObject *ob_type;     	// 数据类型
} PyObject;

typedef struct {
	PyObject ob_base;
	Py_ssize_t ob_size; 	/* Number of items in variable part 列表、元组等元素的个数 */
} PyVarObject;
// 对象创建时都会有PyObject,列表、元组、字典、集合都会有PyVarObject

一、引用计数器

Python通过引用计数来保存内存变量追踪,记录该对象被其他使用的对象引用的次数,内部有个跟踪变量叫做引用计数器,每个变量有多少个引用,简称引用计数。当某个引用计数为0时,就列入了垃圾回收队列。

import sys
a=1
sys.getrefcount(a) # ==> 2 
# getrefcount返回变量的调用次数,调用时内部会产生临时变量,所以调用次数是2

1、引用计数增加的情况

1、一个对象被分配给一个新的变量 a = 1, b = a
2、对象被放入一个容器中 list.append(a) set.add(a)
3、对象被当作参数传到函数中

2、引用计数减少的情况

1、使用del 显示的删除对象 del a
2、对象所在的容易被删除 list=[a, b] del list
3、引用超出作用域,或者被重新赋值 a = [1,2] a = [3,4]
引用计数器的问题是不能解决两个对象相互引用对象引用自己的情况,del可以减少引用次数,但计数不会归0。

3、GIL存在的关键因素

del 操作时先执行 DELETE_NAME将对象的的引用计数减1,然后再判断对象的引用数是否为0,如果为0会触发垃圾回收,表面del 操作底层是有两步的。

import dis
dis.dis("del a")
  1           0 DELETE_NAME              0 (a)
              2 LOAD_CONST               0 (None)
              4 RETURN_VALUE

现在如果有两个线程A和线程B,同时对data对象进行del data操作时,线程A先执行 del data后执行了DELETE_NAME,引用计数为0,然后发生了CPU调度B线程执行,也对data执行了del data,结果发现data的引用计数已经为0 了,就直接触发垃圾回收,完了后又切换到线程A执行,此时A也会继续判断data的引用数是否为0,然后进行释放,此时data就会变成野指针,这就是二次释放。为了解决这种问题,引入了GIL,保证每一个时刻只有一个线程在解释器中执行,并且会保证线程切换的时候会把当前的指令执行完再进行切换,就不会发生二次释放的问题。
相同的问题,Python的一个字节码可能会对应C中的多个函数调用,GIL也会保证在线程切换时,执行完当前的底层函数调用。

二、标记-清除

1、堆区和栈区

**堆 **Python中的大部分对象(例如列表、字典、类实例,以及小整数池、短字符缓存区、匿名列表对象缓存区、匿名字典对象缓存区)都存储在堆内存中。堆内存用于存储动态分配的对象,其大小通常由Python的内存管理器自动调整。当你创建一个新的对象时,Python会在堆内存中分配内存空间来存储该对象。
内存用于存储函数调用的上下文信息。每当你调用一个函数时,其局部变量、函数参数、返回地址等信息都会被压入栈内存中。当函数执行完毕时,这些信息会被从栈内存中弹出,控制权返回到调用函数。
例子:
data = “hello world.”
info = data

data = “hello world.” 通俗的讲就是等号=右边的值"hello world."存在堆区,而"hello world."所处的内存地址是存在栈里的。data变量就是对"hello world."对象的引用。

a = [1, 2, 3] 
b = [4, 5, 6]
a.append(b)     # a引用计数器为2
b.append(a)     # b引用计数器为2
del a        	# a的引用计数器为1
del b        	# b的引用计数器为1 

a和b存在循环引用,当执行del操作后,他们的计数器不会为0,所以永远不会被消除,如果代码中存在很多这种代码就会导致内存被耗尽,程序崩溃。可能存在循环引用的类型有列表、元组、字典、集合、自定义类

2、标记

垃圾回收器会使用深度优先搜索来遍历,当前程序的所有栈区引用的对象,将遍历到的对象标记为存活,表示可以访问到。标记一个对象后,垃圾回收器会继续遍历该对象引用的其他对象。
扩展:什么是三色标记算法?

3、清除

当所有的对象都标记完时,垃圾回收器会扫描整个堆区,清除没有被标记的对象,这些对象都是没有被栈区引用的,这些对象就是要被清除的对象。

4、什么情况下会触发标记-清除呢

垃圾回收阶段会暂停程序,等标记清除后才会恢复程序运行,为了减少程序的暂停时间,python通过分代回收以空间换时间提高垃圾回收效率。

三、分代回收

python将可能存在循环引用的容器对象(内部可以引用其他对象的对象,PyListObject、PyDictObject、自定义类对象、自定义类对象的实例对象)拆分成3个链表,分别为0代、1代、2代总共三代,每代都有可以存对象和阈值,当达到阈值时就会扫描链表,将循环引用各自减一、销毁计数器为0的对象,当第0代扫描后存活下来的对象会被移到第1代,在第1代存活下来的对象会被移到第2代,可以简单的理解为:对象存在时间越长,越可能不是垃圾,应该越少去收集。

// 源码 Modules/gcmodule.c
struct gc_generation generations[NUM_GENERATIONS] = {
/* PyGC_Head,                                    threshold,    count */
{{(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)},   700,        0},
{{(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)},   10,         0},
{{(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)},   10,         0},
}

// python源码
import gc
gc.get_threshold()  ## 分代回收机制的参数阈值设置
(700, 10, 10)

1、这种对象新创建的时候,就会被加入到0代链表上,当0代链表上的对象数大于700时,就开始扫描0代链表。此时如果2、1代未达到阈值,则扫描0代,并将1代的count值加1,如果2代已经达到阈值,则将2、1、0代三个链表拼接起来进行扫描,并将2、1、0代的count值置为0,如果1代已经达到阈值,则将1、0两个链表拼接起来进行扫描,并将1、0代的count值置为0。
2、当第0代被扫描10次时,则第1代开始扫描。
3、当第1代被扫描10次时,则第2代开始扫描。
对拼接起来的链表在进行扫描时,主要就是剔除循环引用和销毁垃圾,详细过程为:

  • 扫描链表,把每个对象的引用计数器拷贝一份并保存到 gc_refs中,保护原引用计数器。
  • 再次扫描链表中的每个对象,并检查是否存在循环引用,如果存在则让各自的gc_refs减 1 。
  • 再次扫描链表,将 gc_refs 为 0 的对象移动到unreachable链表中;不为0的对象直接升级到下一代链表中。
  • 处理unreachable链表中的对象的 析构函数 和 弱引用,不能被销毁的对象升级到下一代链表,能销毁的保留在此链表。
    • 析构函数,指的就是那些定义了__del__方法的对象,需要执行之后再进行销毁处理。
    • 弱引用,
  • 最后将 unreachable 中的每个对象销毁并在refchain链表中移除(不考虑缓存机制)。

四、弱引用

弱引用与普通引用不同,弱引用不会增加被引用对象的引用计数,因此不会阻止对象被回收。在Python中可以使用weakref模块来创建和操作弱引用,弱引用的主要用途是解决循环引用问题。
1、支持弱引用的对象
对于list、dict、str本身不支持弱引用,但可以通过创建子类的方式对其进行弱引用,对于int、tuple本身及其子类均不支持弱引用,set直接支持弱引用。

import sys
import weakref
a = {1,2,3}
b = a
sys.getrefcount(a) # 3 a被引用的3次

c = weakref.ref(a) # 对a进行弱引用 引用次数不会增加
sys.getrefcount(a) # 3

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1120179.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

最简单修改nacos的修改权重修改上线下线报错

前言 我在docker中部署了一个单体的nacos服务,过了一段时间,由于我导入了另外一个nacos的服务配置导致服务注册没问题,但是服务的修改权重和修改服务的上线下线会报错.于是就有了这篇文章,这篇文章主要是解决上面说的问题 正文 1.报错信息展示(这里我已经修复过了已经展示不了了…

【iOS】简单的网络请求

应iOS小组要求,仿写知乎日报需要实现网络请求并解析JSON格式数据,这篇文章仅对基本的网络请求和iOS中的JSON解析作以记录,还涉及到RunLoop的一点小插曲,具体请求过程和原理以后会详细学习!🙏 基本网络流程简…

42915-2023 铜精矿及主要含铜物料鉴别规范

1 范围 本文件规定了铜精矿及主要含铜物料的鉴别特征、鉴别流程、鉴别实施及鉴别报告编写。 本文件适用于进口铜精矿与主要含铜物料的鉴别,主要含铜物料包括冰铜、铜火法冶炼渣、铜火法 冶炼烟尘、铜阳极泥、铜渣精矿等铜火法冶炼工艺产生的物料。 2 规范性引用…

前端Vue框架系列—— 学习笔记总结Day02

❤ 作者主页:欢迎来到我的技术博客😎 ❀ 个人介绍:大家好,本人热衷于Java后端开发,欢迎来交流学习哦!( ̄▽ ̄)~* 🍊 如果文章对您有帮助,记得关注、点赞、收藏、…

VMwarePlayer安装Ubuntu,切换中文并安装中文输入法

1.下载和安装 虚拟机使用的免费版官网链接:VMwarePlayer Ubuntu镜像下载官网链接:Ubuntu桌面版 自己学习使用,不需要考虑迁移之类的。选择单个磁盘IO性能会更高 安装过程中如果出现如下报错,则用系统管理员身份运行 右击VMwa…

造车先做三蹦子220101--机器学习字符(字母、和数字识别)的“小白鼠”与“果蝇”

“0”数字字符零 的图片(16*16点阵): import torch import torch.nn as nn import torch.optim as optim from PIL import Image, ImageDraw, ImageFont from torchvision import transforms import matplotlib.pyplot as pltTimes20001000# 参数设置 font_path &q…

嵌入式实时操作系统的设计与开发(内存资源池存储管理)

内存资源池存储管理 内存资源池存储管理属于固定大小内存管理系统,内存池中内存块的分配和回收是基于第一级内存管理系统的,因为内存池中内存块是由第一级内存管理的算法所确定的。 内存池存储管理系统主要用于操作系统的一些常用结构的内存管理。例如…

位操作符^以及正负数在计算机中的存储

(数据是怎么在计算机中存储的)​ 正数和负数在内存中都是以补码的形式存储的,但不同的是正数的原码,补码,反码都是相同的,而负数的原码,补码和反码是不同的。 负数的原码,补码,反码之间存在什么…

神仙级价格:腾讯云双十一服务器优惠价格表来了

2023腾讯云双十一服务器优惠价格表多少钱一年?轻量服务器2核2G3M、2核2G4M、2核4G5M、4核8G12M、8核16G18M、16核32G28M和云服务器CVM标准型S5实例优惠价格,腾讯云百科今年双11服务器价格会在当前的价格基础上享受个9折优惠,可领券 https://c…

【Java】一只小菜坤的编程题之旅【4】

文章目录 1丶合并两个有序链表2丶栈的压入、弹出序列3丶设计循环队列4丶最小栈 1丶合并两个有序链表 小菜坤的答案: class Solution {public ListNode mergeTwoLists(ListNode list1, ListNode list2) {ListNode newHeadnew ListNode(0);ListNode tmpnewHead;while…

Git的介绍和命令汇总

目录 一、git介绍 1、git的工作区域 2、git中文件的四种状态 二、常用命令 1、基础命令 2、提交类命令 3、删除类命令 4、分支类相关命令 5、 查看类相关命令 6、撤销类命令 一、git介绍 1、git的工作区域 在Git中,有四个工作区域:工作区域&am…

k8s认证

1. 证书介绍 服务端保留公钥和私钥,客户端使用root CA认证服务端的公钥 一共有多少证书: *Etcd: Etcd对外提供服务,要有一套etcd server证书Etcd各节点之间进行通信,要有一套etcd peer证书Kube-APIserver访问Etcd&a…

算法学习之 背包01问题 , 备战leecode

来看题目 我们分析一下题目&#xff0c;首先我们要排序&#xff0c;这有助于我们得到最大的值&#xff0c;我们要得到一个递推公式 代码如下: class Solution { public:int maxSatisfaction(vector<int>& satisfaction) {int n satisfaction.size();vector<v…

【数据结构复习之路】串 (超详细讲解) 严蔚敏版

专栏&#xff1a;数据结构复习之路 复习完上面一章【线性表】【栈和队列】&#xff0c;我们接着复习串&#xff0c;这篇文章我写的非常详细且通俗易懂&#xff0c;看完保证会带给你不一样的收获。如果对你有帮助&#xff0c;看在我这么辛苦整理的份上&#xff0c;三连一下啦 目…

42917-2023 消光制品用聚氯乙烯树脂

1 范围 本文件规定了消光制品用聚氯乙烯树脂的分类、技术要求、取样、试验方法、检验规则及标志、随行 文件、包装、运输和贮存。 本文件适用于氯乙烯与交联剂悬浮共聚所制得的用于生产消光制品的聚氯乙烯树脂。 2 规范性引用文件 下列文件中的内容通过文中的规范性引用而…

通过高通量测序评估金针菇(双孢蘑菇)生产过程中的微生物演替

1.1 Title:Microbial succession during button mushroom (Agaricus bisporus) production evaluated via high-throughput sequencing 1.2 作者&#xff1a;Ban Ga-Hee 1.3 机构&#xff1a;Ewha Womans University 1.4 期刊&#xff1a;Food Microbiology 1.5 分区/影响因…

连接器信号完整性仿真教程 八

连接器信号完整性仿真主要是解算S参数及与之相关的参数&#xff0c;查看仿真结果相对于HFSS&#xff0c;其操作要简单得多。不需要复杂的操作&#xff0c;基本上左边导航树中&#xff0c;就可直接打开需要查看的仿真结果。下面以B to B Connector仿真结果实例演示&#xff0c;详…

何为心理承受能力?如何提高心理承受能力?

心理承受能力&#xff0c;也可以理解为人的抗压能力&#xff0c;指的是承受压力&#xff0c;承受逆境的能力。人的一生其实就是在不断的解决问题&#xff0c;见招拆招&#xff0c;遇到问题解决问题&#xff0c;在我们不断学习和锻炼的过程中&#xff0c;提高了我们解决问题的效…

Python之解析式和生成器表达式

Python之解析式和生成器表达式 列表解析式 列表解析式List Comprehension&#xff0c;也叫列表推导式。 语法 [返回值 for 元素 in 可迭代对象 if 条件]使用中括号[]&#xff0c;内部是for循环&#xff0c;if条件语句可选返回一个新的列表 列表解析式是一种语法糖 编译器…

Vue中如何处理表单输入验证?

在Vue中,能使用多种方式处理表单输入验证。以下是几种常见的方法: 1:使用Vue的指令和表达式: Vue提供了一些内置的指令和表达式,可以直接在模板中进行表单验证。例如,你可以使用v-model指令结合条件表达式来验证输入内容。 <template><div><input v-mod…