Python入门【内存管理机制、Python缓存机制、垃圾回收机制、分代回收机制】(三十二)

news2024/9/24 13:22:46

👏作者简介:大家好,我是爱敲代码的小王,CSDN博客博主,Python小白
📕系列专栏:python入门到实战、Python爬虫开发、Python办公自动化、Python数据分析、Python前后端开发
📧如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀
🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人

🔥🔥🔥 python入门到实战专栏:从入门到实战 

🔥🔥🔥 Python爬虫开发专栏:从入门到实战

🔥🔥🔥 Python办公自动化专栏:从入门到实战

🔥🔥🔥 Python数据分析专栏:从入门到实战

🔥🔥🔥 Python前后端开发专栏:从入门到实战

目录

内存管理机制

Python缓存机制

垃圾回收机制

分代回收机制


内存管理机制

Python是由C语言开发的,底层操作都是基于C语言实现,Python中创建每个对象,内部都会与C语言结构体维护一些值。 

源码下载,https://www.python.org/

将压缩文件减压,可以看到有很多文件,主要关心两个(Include、 Objects) 在Include目录下object.h中可以查看创建对象的结构体。 

#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;

在创建对象时,每个对象至少内部4个值,PyObject结构体(上一个 对象、下一个对象、类型、引用个数)。

有多个元素组成的对象使用PyVarObject,里面由:PyObject结构体(上一个对象、下一个对象、类型、引用个数)+Ob_size(items=元素,元素个数)。

环状双向链表refchain

在python程序中创建的任何对象都会被放在refchain链表中。

类型封装结构体

f = 3.14
'''
内部会创建:
1.开辟内存
2.初始化
 ob_fval = 3.14 值
 ob_type = float 类型
 ob_refcnt = 1 引用数量
3.将对象加入到双向链表refchain中
   _ob_next = refchain中的上一个对象
   _ob_prev = refchain中的下一个对象
'''

Python缓存机制

小整数对象池 

一些整数在程序中的使用非常广泛,Python为了优化速度,使用了小整数对象池, 避免为整数频繁申请和销毁内存空间。

Python 对小整数的定义是 [-5, 257) 这些整数对象是提前建立好的,不会被垃圾回收。在一个 Python 的程序中,所有位于这个范围内的整数使用的都是同一个对象。在一个 Python 的程序中,无论这个整数处于LEGB(局部变量,闭包,全局,内建模块)中的哪个位置, 所有位于这个范围内的整数使用的都是同一个对象。

【示例】验证小整数池

In [8]: a = 100

In [9]: id(a)
Out[9]: 140705185112832

#删除变量a
In [10]: del a

In [11]: b = 100

In [12]: id(b)
Out[12]: 140705185112832

大整数对象池

每一个大整数,均创建一个新的对象。

【示例】验证大整数池

In [13]: a = 257

In [14]: b = 257

In [15]: id(a)
Out[15]: 1747036560496

In [16]: id(b)
Out[16]: 1747036558352

In [17]: a is b
Out[17]: False

intern机制

每个单词(字符串),不夹杂空格或者其他符号,默认开启intern机制,共享内存,靠引用计数决定是否销毁。

【示例】intern机制

>>> a = 'helloworld'
>>> b = 'helloworld'
>>> a is b
>>> a = 'hello world'
>>> b = 'hello world'
>>> a is b

free_list机制

当一个对象的引用计数器为0时,按理说应该回收,但内存不会直接回收,而是将对象添加到free_list链表中缓存。以后再去创建对象 时,不再重新开辟内存,而是直接使用free_list。

以上的free_list的代表:float,list,tuple,dict。

>>> f1 = 3.14
>>> id(f1)
2078136427568
>>> del f1
>>> f2 = 9.998
>>> id(f2)
2078136427568

f1 = 3.14,会创建float类型,并且加入refchain中。del f1 则减1, refchain移除,按理会进行销毁,但是实际中不会真正销毁,而是 会添加到free_list。以后创建f2=9.998等,只要是float类型,则不会重新开辟内存,会去free_list获取对象,对象内部进行初始化, 替换值3.14换成9.99,再放到refchain中。不是所有的都放入 free_list。例如存储100个缓存,则前面新创建的80个会放入 free_list。如果满了81之后则会销毁。

1、 float类型,维护的free_list链表最多可缓存100个float对象。

#ifndef PyFloat_MAXFREELIST//定义free_list的最大长度
#define PyFloat_MAXFREELIST 100
#endif

2、 list类型,维护的free_list数组最多可缓存80个list对象。

#ifndef PyList_MAXFREELIST#define
PyList_MAXFREELIST 80
#endif

【示例】

>>> v1 = [11,22,33]
>>> id(v1)
2078137539904
>>> del v1
>>> v2 = ['a','b']
>>> id(v2)
2078137539904

3、 tuple类型,维护一个free_list数组且数组含量20,数组中元素 可以是链表且每个链表最多可以含钠2000个元组对象。元组的 free_list数据在存储数据时,是根据元组可容纳的个数为索引找 到free_list数组中对应的链表,并添加到链表中。

>>> t1 = (1,2,3)
>>> id(t1)
2078137564992
>>> del t1 #元组的数量是3,所以把这个对象缓存到
free_list[3]的链表中
>>> t2 = ('a','b','c')
#元组的数量也是3,不会重新开辟内存,而是去
free_list[3]对应的链表中拿到一个对象来使用
>>> id(t2)
2078137564992
>>> del t2
>>> t3 = (11,22,33,44)
>>> id(t3)
2078138219472

 4、 dict类型,维护的free_list数组最多可缓存80个dict对象。

#ifndef PyDict_MAXFREELIST#define
PyDict_MAXFREELIST 80
#endif

【示例】

>>> d1 = {'name':'zs'}
>>> id(d1)
2078137472640
>>> del d1
>>> d2 = {'age':30}
>>> id(d2)
2078137472640

垃圾回收机制

Python 内部采用 引用计数法 ,为每个对象维护引用次数,并据此回收不再需要的垃圾对象。由于引用计数法存在重大缺陷,循环引 用时有内存泄露风险,因此 Python 还采用 标记清除法 来回收存在循环引用的垃圾对象。此外,为了提高垃圾回收( GC )效率,Python 还引入了 分代回收机制 。 

 引用计数法

Python采用了类似Windows内核对象一样的方式来对内存进行管理。每一个对象,都维护这一个对指向该对对象的引用的计数。

引用计数 是计算机编程语言中的一种 内存管理技术 ,它将资源被引用的次数保存起来,当引用次数变为 0 时就将资源释放。它管理 的资源并不局限于内存,还可以是对象、磁盘空间等等。

Python 也使用引用计数这种方式来管理内存,每个 Python 对象都包含一个公共头部,头部中的 ob_refcnt 字段便用于维护对象被引用 次数。回忆对象模型部分内容,我们知道一个典型的 Python 对象结构如下:

当创建一个对象实例时,先在堆上为对象申请内存,对象的引用计数被初始化为 1 。以 Python 为例,我们创建一个 float 对象保存 6.66,并把它赋值到变量 f : 

>>> f = 6.66
>>> f
6.66

由于此时只有变量 f 引用 float 对象,因此它的引用计数为 1 :

当我们把 pi 赋值给 f 后,float 对象的引用计数就变成了 2 ,因为现在有两个变量引用它: 

>>> ff = f
>>> ff
6.66

我们新建一个 list 对象,并把 float 对象保存在里面。这样一来, float 对象有多了一个来自 list 对象的引用,因此它的引用计数又加 一,变成 3 了: 

>>> l = [f]
>>> l
[6.66]

标准库 sys 模块中有一个函数 getrefcount 可以获取对象引用计数:

>>> import sys
>>> sys.getrefcount(pi)
4

 咦!引用计数不应该是 3 吗?为什么会是 4 呢?由于 float 对象被作为参数传给 getrefcount 函数,它在函数执行过程中作为函数的局部变量存在,创建了一个临时的引用,因此又多了一个引用:

随着 getrefcount 函数执行完毕并返回,它的栈帧对象将从调用链中解开并销毁,这时 float 对象的引用计数也跟着下降。因此,当一个 对象作为参数传个函数后,它的引用计数将加一;当函数返回,局部名字空间销毁后,对象引用计数又减一。

引用计数就这样随着引用关系的变动,不断变化着。当所有引用都消除后,引用计数就降为零,这时 Python 就可以安全地销毁对象, 回收内存了: 

>>> del l
>>> del ff
>>> del f

引用计数增加

1、 对象被创建

2 、如果有新的对象使用该对象

3 、作为容器对象的一个元素

4、 被作为参数传递给函数

引用计数减少

1、 对象的引用被显示的销毁

2、 新对象不再使用该对象

3 、对象从列表中被移除,或者列表对象本身被销毁

4 、函数调用结束 

引用计数机制的优点 

1、简单

2、实时性:一旦没有引用,内存就直接释放了。

 引用计数机制的缺点

1、维护引用计数消耗资源

2、循环引用的问题无法解决

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

 标记-清除

引用计数法能够解决大多数垃圾回收的问题,但是遇到两个对象相互引用的情况,del语句可以减少引用次数,但是引用计数不会归 0,对象也就不会被销毁,从而造成了内存泄漏问题。针对该情况, Python引入了标记-清除机制

循环引用 

引用计数这种管理内存的方式虽然很简单,但是有一个比较大的瑕疵,即它不能很好的解决循环引用问题。如上图所示:对象 A 和对 象 B,相互引用了对方作为自己的成员变量,只有当自己销毁时, 才会将成员变量的引用计数减 1。因为对象 A 的销毁依赖于对象 B 销毁,而对象 B 的销毁与依赖于对象 A 的销毁,这样就造成了我们 称之为循环引用(Reference Cycle)的问题,这两个对象即使在外界已经没有任何指针能够访问到它们了,它们也无法被释放。 

class People:
    pass
class Cat:
    pass
#创建People的实例对象
p = People()
#创建Dog的实例对象
c = Cat()
#People的宠物属性指向Cat
p.pet = c
#Cat的主人属性指向People
c.master = p
#删除p和c对象
del p
del c

上述实例中,对象p中的属性引用c,而对象c中属性同时来引用p, 从而造成仅仅删除p和c对象,也无法释放其内存空间,因为他们依然在被引用。深入解释就是,循环引用后,p和c被引用个数为2, 删除p和c对象后,两者被引用个数变为1,并不是0,而python只有 在检查到一个对象的被引用个数为0时,才会自动释放其内存,所以 这里无法释放p和c的内存空间。

主动思路一般分为两步:垃圾识别 垃圾回收 。垃圾对象被识别出来后,回收就只是自然而然的工作了,因此垃圾识别是解决问题的关键。那么,有什么办法可以将垃圾对象识别出来呢?我们来考察一个一般化例子:

这是一个对象引用关系图,其中灰色部分是需要回收但由于循环引用而无法回收的垃圾对象,绿色部分是被程序引用而不能回收的活跃对象。如果我们能够将活跃对象逐个遍历并标记,那么最后没有被标记的对象就是垃圾对象。

遍历活跃对象,第一步需要找出 根对象 ( root object )集合。所谓根对象,就是指被全局引用或者在栈中引用的对象,这部对象是不能被删除的。因此,我们将这部分对象标记为绿色,作为活跃对象遍历的起点。 

根对象本身是 可达的 ( reachable ),不能删除;被根对象引用的对象也是可达的,同样不能删除;以此类推。我们从一个根对象出发,沿着引用关系遍历,遍历到的所有对象都是可达的,不能删除。 

 这样一来,当我们遍历完所有根对象,活跃对象也就全部找出来了:

而没有被标色的对象就是 不可达 ( unreachable )的垃圾对象,可以被安全回收。循环引用的致命缺陷完美解决了! 

这就是垃圾回收中常用的 标记清除法 。 

【示例】标记清除法

 上图中小黑点(变量)表示根节点,从根节点出发,每个对象都有引用和被引用的情况,如果该对象找不到根节点,那么就会被清除,如图1,2,3都有被小黑点(变量)引用,4,5没有变量引用,所以 4,5就会被清除。

分代回收机制

Python 程序启动后,内部可能会创建大量对象。如果每次执行标记清除法时,都需要遍历所有对象,多半会影响程序性能。为此, Python引入分代回收机制——将对象分为若干“”( generation ), 每次只处理某个代中的对象,因此 GC 卡顿时间更短。

考察对象的生命周期,可以发现一个显著特征:一个对象存活的时间越长,它下一刻被释放的概率就越低。我们应该也有这样的亲身体会:经常在程序中创建一些临时对象,用完即刻释放;而定义为 全局变量的对象则极少释放。

因此,根据对象存活时间,对它们进行划分就是一个不错的选择。 对象存活时间越长,它们被释放的概率越低,可以适当降低回收频率;相反,对象存活时间越短,它们被释放的概率越高,可以适当提高回收频率。

对象存活时间释放概率回收频率

Python 内部根据对象存活时间,将对象分为 3 代(见 Include/internal/mem.h ):

#define NUM_GENERATIONS 3

 随着时间的推进,程序冗余对象逐渐增多,达到一定阈值,系统进行回收。

这 3 个代分别称为:初生代、中生代 以及 老生代。当这 3 个代初始化完毕后,对应的 gc_generation 数组大概是这样的:

import gc
#python 中内置模块gc触发
print(gc.get_threshold()) #查看gc默认值
#输出(700, 10, 10)

第一代链表:

当第一代达到700,就开始检测哪些对象引用计数变成0了,把不是0的放到第二代链表里,此时第一代链表就是空了,当再次达到700 时,就再检测一遍。

第二代链表:

当第二代链表达到10,就检测一次。

第三代链表:

第三代链表检测10之后,第三代链表检测一次。

import gc

#返回一个元组,分别获取这三代当前计数
gc.get_count()  

#返回一个元组,分别获取这三代当前的收集阈值
gc.get_threshold()

#设置阈值
gc.set_threshold()

#关闭gc垃圾回收机制
gc.disable()

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

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

相关文章

Java IO流(二)IO模型(BIO|NIO|AIO)

概述 Java IO模型同步阻塞IO(BIO)、同步非阻塞IO(NIO)、异步非阻塞IO(AIO/NIO2),Java中的BIO、NIO和AIO理解为是Java语言对操作系统的各种IO模型的封装 IO模型 BIO(Blocking I/O) 概述 BIO是一种同步并阻…

『SpringBoot 源码分析』run() 方法执行流程:(1)初始化 SpringApplication 、上下文环境、应用上下文

『SpringBoot 源码分析』run() 方法执行流程:(1)初始化 SpringApplication 、上下文环境、应用上下文 基于 2.2.9.RELEASE问题:当方法进行了注释标记之后,springboot 又是怎么注入到容器中并创建类呢? 首…

Java入门必备|有你想知道的代码技巧

前言 本文主要分享记录学习Java时的敲代码大法,一步步与idea这个软件磨合,让它为我们敲代码这条路提供更便捷的帮助(雀食好用哈) 一.psvm 很多刚上手IJ软件,就被main()方法给折服了,这段代码量十分大 当…

常见指令以及权限理解

常见指令以及权限理解 命令格式: command [-options] parameter1 parameter1 命令 选项 参数1 参数2 1.command为命令名称,例如变化目录的cd等 2.中括号[ ]实际在命令中是不存在的,这个中括号代表可选,通常选项前面会添加一个符号…

Linux命令200例:clock的具体应用,设置系统的时钟时间、硬件时钟和定时器等相关信息

🏆作者简介,黑夜开发者,全栈领域新星创作者✌。CSDN专家博主,阿里云社区专家博主,2023年6月csdn上海赛道top4。 🏆数年电商行业从业经验,历任核心研发工程师,项目技术负责人。 &…

缺少或找不到vcruntime140_1.dll的解决方法

某天,当我准备打开电脑上的一个应用程序时,突然收到一个错误提示,显示缺少了vcruntime140_1.dll文件。这个文件是一个重要的系统组件,它的丢失导致了我无法正常运行该应用程序。于是,我开始了一场寻找和修复旅程。然而…

“深度学习”学习日记:Tensorflow实现VGG每一个卷积层的可视化

2023.8.19 深度学习的卷积对于初学者是非常抽象,当时在入门学习的时候直接劝退一大班人,还好我坚持了下来。可视化时用到的图片(我们学校的一角!!!)以下展示了一个卷积和一次Relu的变化 作者使…

leetcode 387.字符串中第一个唯一字符

⭐️ 题目描述 🌟 leetcode链接:https://leetcode.cn/problems/first-unique-character-in-a-string/description/ 思路: 比较优的方式使用相对映射记录的方式。在 ASCII 表中小写字母 -97 就是 0 - 25。在依次从前遍历查找即可。需要注意的…

论文笔记:Continuous Trajectory Generation Based on Two-Stage GAN

2023 AAAI 1 intro 1.1 背景 建模人类个体移动模式并生成接近真实的轨迹在许多应用中至关重要 1)生成轨迹方法能够为城市规划、流行病传播分析和交通管控等城市假设分析场景提供仿仿真数据支撑2)生成轨迹方法也是目前促进轨迹数据开源共享与解决轨迹数…

十六、Spring Cloud Sleuth 分布式请求链路追踪

目录 一、概述1、为什么出出现这个技术?需要解决哪些问题2、是什么?3、解决 二、搭建链路监控步骤1、下载运行zipkin2、服务提供者3、服务调用者4、测试 一、概述 1、为什么出出现这个技术?需要解决哪些问题 2、是什么? 官网&am…

RingBuffer 环形缓冲区----镜像指示位

文字和图片参考和来自这些文章: 大疆嵌入式软件编程题找鞍点_已知循环缓冲区是一个可以无限循环读写的缓冲区,当缓冲区满了还继续写的话就会覆_一禅的师兄的博客-CSDN博客 ring buffer,一篇文章讲透它? - 知乎 (zhihu.com) 1 概述 1.1 什…

Python可视化在量化交易中的应用(15)_Seaborn箱线图小提琴图

Seaborn中箱线图和小提琴图的绘制方法 箱线图和小提琴图常被用来观测数据的中位数、上下四分位数分布范围以及异常值的分布情况。 seaborn中绘制箱线图使用的是sns.boxplot()函数。 sns.boxplot(x,y,hue,data,order,hue_order,orient,color,palette,saturation0.75,width0.8,do…

C语言 功能型API --------------------strcat()

NAME strcat, strncat - concatenate two strings 头文件 SYNOPSIS #include <string.h> 函数原型&#xff1a; char *strcat(char *dest, const char *src); 功能&#xff1a; 在字符串dest的末尾将字符串src拼接上去 #include <stdio.h> #inc…

227、仿真-基于51单片机锅炉热电偶PT100铂电阻温度控制Proteus仿真设计(程序+Proteus仿真+原理图+流程图+元器件清单+配套资料等)

毕设帮助、开题指导、技术解答(有偿)见文未 目录 一、设计功能 二、Proteus仿真图 三、原理图 四、程序源码 资料包括&#xff1a; 需要完整的资料可以点击下面的名片加下我&#xff0c;找我要资源压缩包的百度网盘下载地址及提取码。 方案选择 单片机的选择 方案一&…

LlamaGPT -基于Llama 2的自托管类chatgpt聊天机器人

LlamaGPT一个自托管、离线、类似 ChatGPT 的聊天机器人&#xff0c;由 Llama 2 提供支持。100% 私密&#xff0c;不会有任何数据离开你的设备。 推荐&#xff1a;用 NSDT编辑器 快速搭建可编程3D场景 1、如何安装LlamaGPT LlamaGPT可以安装在任何x86或arm64系统上。 首先确保…

网络编程(TCP和UDP的基础模型)

一、TCP基础模型&#xff1a; tcp Ser&#xff1a; #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h> #include <head.h>#define PORT 88…

探索无限创造力的星辰大道,画出想象的浩瀚宇宙!-turtle

介绍 视频教程地址在此&#xff1a;https://www.bilibili.com/video/BV1Pm4y1H7Tb/ 大家好&#xff0c;欢迎来到本视频&#xff01;今天&#xff0c;我们将一同探索Python编程世界中的一个有趣而创意的库——Turtle库。无需专业绘画技能&#xff0c;你就可以轻松地用代码绘制…

docker的安装与基础使用

一.docker简介 1&#xff09;什么是docker Docker是一种用于构建、打包和运行应用程序的开源平台。它基于操作系统级虚拟化技术&#xff0c;可以将应用程序和其依赖的库、环境等资源打包到一个可移植的容器中&#xff0c;形成一个轻量级、独立的可执行单元。 开发者在本地编…

QT TLS initialization failed问题(已解决) QT基础入门【网络编程】openssl

问题: qt.network.ssl: QSslSocket::connectToHostEncrypted: TLS initialization failed 这个问题的出现主要是使用了https请求:HTTPS ≈ HTTP + SSL,即有了加密层的HTTP 所以Qt 组件库需要OpenSSL dll 文件支持HTTPS 解决: 1.加入以下两行代码获取QT是否支持opensll以…

【学会动态规划】单词拆分(24)

目录 动态规划怎么学&#xff1f; 1. 题目解析 2. 算法原理 1. 状态表示 2. 状态转移方程 3. 初始化 4. 填表顺序 5. 返回值 3. 代码编写 写在最后&#xff1a; 动态规划怎么学&#xff1f; 学习一个算法没有捷径&#xff0c;更何况是学习动态规划&#xff0c; 跟我…