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

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.rhkb.cn/news/165742.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

西门子博途软件加密保护方法

一、程序块的专有技术保护 程序块的专有技术保护主要是对项目中的程序块(OB、FB、FC、DB)进行访问保护,如果没有专有技术保护密码则无法看到程序块中的具体内容,对于专有技术保护的 DB 块,如果没有密码只能读不能写。…

eNSP-OSPF协议其他区域不与骨干区域相连解决方法3

virtual-link技术 AR1 [ar1]int g0/0/0 [ar1-GigabitEthernet0/0/0]ip add 192.168.1.1 24 [ar1-GigabitEthernet0/0/0]quit [ar1]ospf [ar1-ospf-1]area 0 [ar1-ospf-1-area-0.0.0.0]net 192.168.1.0 0.0.0.255 [ar1-ospf-1-area-0.0.0.0]quit AR2 [ar2]int g0/0/0 [ar2-Gig…

skiaSharp linux 生成验码字体显示不出来

一、拷贝windows下的字体如:C:\Windows\Fonts 设置字体的地方: var fontPath Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Fonts", "TAHOMA.TTF");最终效果:

d3dx9_43.dll丢失怎么解决,四个解决方法帮你解决d3dx9_43.dll丢失

随着科技的不断发展,我们越来越依赖各种软件和硬件设备来提高生活和工作效率。然而,有时候我们可能会遇到一些技术问题,如“d3dx9_43.dll丢失”的问题。这个问题可能导致某些程序无法正常运行,给我们的生活带来诸多不便。因此&…

C++初阶--类与对象(1)

文章目录 类的引入类的定义访问限定符类成员的注意事项变量名的冲突 类的实例化类成员的声明与定义类的大小this指针特性 总结 类的引入 在c语言中,我们会这样写一个栈: struct Stack {int* a;int top;int capacity; };void StackInit(struct Stack* p…

线性代数2:梯队矩阵形式

图片来自 Europeana on Unsplash 一、前言 欢迎阅读的系列文章的第二篇文章,内容是线性代数的基础知识,线性代数是机器学习背后的基础数学。在我之前的文章中,我介绍了线性方程和系统、矩阵符号和行缩减运算。本文将介绍梯队矩阵形式&#xf…

Android 如何在Service中使用ViewModel

需求:最近有反馈说,需要在service中使用网络请求,而我网络请求就是封装的ViewModel。然后我就发现,原来service不支持,懵了呀!哈哈 还是去看看ViewModel的源码了解下吧。下面有几个介绍的。就不多做赘述了。…

微信小程序开发之后台数据交互及wxs应用

目录 一、后端准备 1. 应用配置 2. 数据源配置 二、数据库 1. 创建 2. 数据表 3. 数据测试 三、前端 1. 请求方法整合 2. 数据请求 3. WXS的使用 4. 样式美化 5. 页面 一、后端准备 通过SpringMVC及mybatis的技术学习,还有前后端分离的技术应用&…

Mycat2 分布式数据库中间件

一.安装部署 Mycat2目前还不支持直接获取Docker镜像,需要自己通过Dockerfile打包镜像,其实这也是为了开发者考虑,比如一些个性化功能,如自定义分片等 Dockerfile FROM docker.io/adoptopenjdk/openjdk8:latestENV AUTO_RUN_DIR…

RemObjects Elements 12.0 Crack

Elements 是一个现代多功能软件开发工具链。 它支持六种流行的编程语言:Oxygene (Object Pascal)、C#、Java、Mercury (Visual Basic.NET™)、Go 和 Swift,适用于所有现代平台。 使用 Elements,您可以为您喜欢的任何平台进行编程- 无论是单…

想要精通算法和SQL的成长之路 - 找到最终的安全状态

想要精通算法和SQL的成长之路 - 找到最终的安全状态 前言一. 找到最终的安全状态1.1 初始化邻接图1.2 构建反向邻接图1.3 BFS遍历1.4 完整代码 前言 想要精通算法和SQL的成长之路 - 系列导航 一. 找到最终的安全状态 原题链接 我们从题目中可以看出来: 出度为0的…

面试官:如何理解CDN?说说实现原理?

一、是什么 CDN (全称 Content Delivery Network),即内容分发网络 构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降…

大模型技术实践(五)|支持千亿参数模型训练的分布式并行框架

在上一期的大模型技术实践中,我们介绍了增加式方法、选择式方法和重新参数化式方法三种主流的参数高效微调技术(PEFT)。微调模型可以让模型更适合于我们当前的下游任务,但当模型过大或数据集规模很大时,单个加速器&…

OpenCV中world模块介绍

OpenCV中有很多模块,模块间保持最小的依赖关系,用户可以根据自己的实际需要链接相关的库,而不需链接所有的库,这样在最终交付应用程序时可以减少总库的大小。但如果需要依赖OpenCV的库太多,有时会带来不方便,此时可以使…

忆联分布式数据库存储解决方案,助力MySQL实现高性能、低时延

据艾瑞咨询研究院《2022 年中国数据库研究报告》显示,截止2021年,中国分布式数据库占比达到 20%左右,主要以 MySQL 和 PostgreSQL 为代表的开源数据库为主。MySQL 作为备受欢迎的开源数据库,当前已广泛应用于互联网、金融、交通、…

【C++初阶】类和对象(上)

个人主页点击直达:小白不是程序媛 我的代码仓库:Gitee C系列专栏:C头疼记 目录 前言 面向过程和面向对象的初步认识 类的引入 类的定义 类的两种定义方式: 类的访问限定符及封装 封装 类的作用域 类的实例化 类对象模型…

LVS负载均衡集群 (NAT模式)

LVS集群 集群的概念: 为解决某个特定的问题,将多个计算机组合起来形成一个单个系统 集群的水平扩展: 增加设备,并行运行多个服务,通过网路连接和算法来调度服务分配的问题 集群的类型: 负载均衡集群&#…

【Javascript】基础数据类型

目录 基础数据类型 1.number 字面量声明 数字对象方式声明 整数判断 指定返回小数位数 NaN-表示非数字值 浮点精度 解决误差 String 字面量声明 数字对象声明 连接运算符 获取长度 大小写转换 转换成大写 转换成小写 ​编辑 移除空白 获取单字符 ​编辑 截…

html中公用css、js提取、使用

前言 开发中,页面会有引用相同的css、js的情况,如需更改则每个页面都需要调整,重复性工作较多,另外在更改内容之后上传至服务器中会有缓存问题,特针对该情况对公用css、js进行了提取并对引用时增加了版本号 一、提取…

分布式Trace:横跨几十个分布式组件的慢请求要如何排查?

目录 前言 一、问题的出现? 二、一体化架构中的慢请求排查如何做 三、分布式 Trace原理 四、如何来做分布式 Trace 前言 在分布式服务架构下,一个 Web 请求从网关流入,有可能会调用多个服务对请求进行处理,拿到最终结果。这个…