Python垃圾回收

基于C语言源码底层,让你真正了解垃圾回收机制的实现

  • 引用计数器
  • 标记清除
  • 分代回收
  • 缓存机制
  • Python的C源码(3.8.2版本)

1.引用计数器

1.1环状双向链表 refchain

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

  1. name = 'jack'
  2. age = 18
  3. hobby = ['篮球', '美女']
  1. 内部会创建一些数据【上一个对象、下一个对象、类型、引用个数】
  2. name = 'jack'
  3. new = name
  4. 内部会创建一些数据【上一个对象、下一个对象、类型、引用个数、val=18
  5. age = 18
  6. 内部会创建一些数据【上一个对象、下一个对象、类型、引用个数、items=元素、元素个数】
  7. hobby = ['篮球', '美女']

在C源码中如何体现每个对象都有的相同的值:PyObject结构体(4个值)

有多个元素组成的对象:Pyobject结构体(4个值)+ ob_size

1.2类型封装结构体

  1. data = 3.14
  2. 内部会创建:
  3. _ob_next = refchain中的上一个对象
  4. _ob_prev = refchain中的下一个对象
  5. ob_refcnt = 1
  6. ob_type = float
  7. ob_fval = 3.14

1.3引用计数器

  1. v1 = 3.14
  2. v2 = 999
  3. v3 = (1, 2, 3)

当python程序运行时,会根据数据结构的不同来找到对应的结构体,根据结构体中的字段来进行创建相关的数据,然后将对象添加到refchain双向链表中。

在C源码中有两个关键的结构体:PyObject、PyVarObject

每个对象中有ob_refcnt就是引用计数器,值默认为1,当有其他变量引用对象时,引用计数器就会发生变化。

  • 引用
  1. a = 9999
  2. b = a
  • 删除引用
  1. a = 9999
  2. b = a
  3. del b # b变量删除,b对应对象引用计数器-1
  4. del a # a变量删除,a对应对象引用计数器-1
  5. # 当一个对象引用的计数器为0时,意味着没有人再使用这个对象了,这个对象就是垃圾,需要被回收
  6. # 回收:1.对象从refchain链表中移除,2.将对象销毁,内存归还

1.4循环引用的问题

基于引用计数器进行垃圾回收非常方便和简单,但他还是存在循环引用的问题,导致无法正常的回收一些数据,例如:

  1. v1 = [11,22,33] # refchain中创建一个列表对象,由于v1=对象,所以列表引对象用计数器为1.
  2. v2 = [44,55,66] # refchain中再创建一个列表对象,因v2=对象,所以列表对象引用计数器为1.
  3. v1.append(v2) # 把v2追加到v1中,则v2对应的[44,55,66]对象的引用计数器加1,最终为2.
  4. v2.append(v1) # 把v1追加到v1中,则v1对应的[11,22,33]对象的引用计数器加1,最终为2.
  5. del v1 # 引用计数器-1
  6. del v2 # 引用计数器-1

2.标记清除

对于上述代码会发现,执行del操作之后,没有变量再会去使用那两个列表对象,但由于循环引用的问题,他们的引用计数器不为0,所以他们的状态:永远不会被使用、也不会被销毁。项目中如果这种代码太多,就会导致内存一直被消耗,直到内存被耗尽,程序崩溃。

 

为了解决循环引用的问题,引入了标记清除技术,专门针对那些可能存在循环引用的对象进行特殊处理,可能存在循环应用的类型有:列表、元组、字典、集合、自定义类等那些能进行数据嵌套的类型。

 

标记清除:创建特殊链表专门用于保存 列表、元组、字典、集合、自定义类等对象,之后再去检查这个链表中的对象是否存在循环引用,如果存在则让双方的引用计数器均 - 1 。如果减完为0,则垃圾回收

3.分代回收

对标记清除中的链表进行优化,将那些可能存在循引用的对象拆分到3个链表,链表称为:0/1/2三代,每代都可以存储对象和阈值,当达到阈值时,就会对相应的链表中的每个对象做一次扫描,除循环引用各自减1并且销毁引用计数器为0的对象。

  1. // 分代的C源码
  2. #define NUM_GENERATIONS 3
  3. struct gc_generation generations[NUM_GENERATIONS] = {
  4. /* PyGC_Head, threshold, count */
  5. {{(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)}, 700, 0}, // 0代
  6. {{(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)}, 10, 0}, // 1代
  7. {{(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)}, 10, 0}, // 2代
  8. };

特别注意:0代和1、2代的threshold和count表示的意义不同。

  • 0代,count表示0代链表中对象的数量,threshold表示0代链表对象个数阈值,超过则执行一次0代扫描检查
  • 1代,count表示0代链表扫描的次数,threshold表示0代链表扫描的次数阈值,超过则执行一次1代扫描检查。
  • 2代,count表示1代链表扫描的次数,threshold表示1代链表扫描的次数阈值,超过则执行一2代扫描检查。

4.小结

在python中维护了一个refchain双向环状链表、这个链表中存储程序创建的所有对象,每种类型的对象中都有一个ob_refcnt引用计数器的值,引用个数+1、-1,最后当引用计数器变为0时会进行垃圾回收(对象销毁、refchain中移除)。

 

但是,python中那些可以有多个元素组成的对象可能会存在出现循环引用的问题,为了解决这个问题,python又引入了标记清除和分代回收,在其内部为4个链表

  • refchain
  • 2代,10次
  • 1代,10次
  • 0代,700次

    在源码内部当达到各自的阈值时,会出发扫描链表进行标记清除的动作(有循环就各自-1),但是源码内部还提供了优化机制

5.Python缓存

从上文大家可以了解到当对象的引用计数器为0时,就会被销毁并释放内存。而实际上他不是这么的简单粗暴,因为反复的创建和销毁会使程序的执行效率变低。Python中引入了“缓存机制”机制。 例如:引用计数器为0时,不会真正销毁对象,而是将他放到一个名为 free_list 的链表中,之后会再创建对象时不会在重新开辟内存,而是在free_list中将之前的对象来并重置内部的值来使用。

  • float类型,维护的free_list链表最多可缓存100个float对象。
  1. v1 = 3.14 # 开辟内存来存储float对象,并将对象添加到refchain链表。
  2. print( id(v1) ) # 内存地址:140599203433232
  3. del v1 # 引用计数器-1,如果为0则在rechain链表中移除,不销毁对象,而是将对象添加到float的free_list.
  4. v2 = 9.999 # 优先去free_list中获取对象,并重置为9.999,如果free_list为空才重新开辟内存。
  5. print( id(v2) ) # 内存地址:140599203433232
  6. # 注意:引用计数器为0时,会先判断free_list中缓存个数是否满了,未满则将对象缓存,已满则直接将对象销毁。
  • int类型,不是基于free_list,而是维护一个small_ints链表保存常见数据(小数据池),小数据池范围:-5 <= value < 257。即:重复使用这个范围的整数时,不会重新开辟内存。
  1. v1 = 38 # 去小数据池small_ints中获取38整数对象,将对象添加到refchain并让引用计数器+1。
  2. print( id(v1)) #内存地址:4401668032
  3. v2 = 38 # 去小数据池small_ints中获取38整数对象,将refchain中的对象的引用计数器+1。
  4. print( id(v2) ) #内存地址:4401668032
  5. # 注意:在解释器启动时候-5~256就已经被加入到small_ints链表中且引用计数器初始化为1,代码中使用的值时直接去small_ints中拿来用并将引用计数器+1即可。另外,small_ints中的数据引用计数器永远不会为0(初始化时就设置为1了),所以也不会被销毁。
  • str类型,维护unicode_latin1[256]链表,内部将所有的ascii字符缓存起来,以后使用时就不再反复创建。
  1. v1 = "A"
  2. print( id(v1) ) # 输出:140599159374000
  3. del v1
  4. v2 = "A"
  5. print( id(v1) ) # 输出:140599159374000
  6. # 除此之外,Python内部还对字符串做了驻留机制,针对那么只含有字母、数字、下划线的字符串(见源码Objects/codeobject.c),如果内存中已存在则不会重新在创建而是使用原来的地址里(不会像free_list那样一直在内存存活,只有内存中有才能被重复利用)。
  7. v1 = "jack"
  8. v2 = "jack"
  9. print(id(v1) == id(v2)) # 输出:True
  • list类型,维护的free_list数组最多可缓存80个list对象。
  1. v1 = [11,22,33]
  2. print( id(v1) ) # 输出:4517628816
  3. del v1
  4. v2 = ["j","ack"]
  5. print( id(v2) ) # 输出:4517628816
  • tuple类型,维护一个free_list数组且数组容量20,数组中元素可以是链表且每个链表最多可以容纳2000个元组对象。元组的free_list数组在存储数据时,是按照元组可以容纳的个数为索引找到free_list数组中对应的链表,并添加到链表中。
  1. v1 = (1,2)
  2. print( id(v1) )
  3. del v1 # 因元组的数量为2,所以会把这个对象缓存到free_list[2]的链表中。
  4. v2 = ("甲壳虫","Alex") # 不会重新开辟内存,而是去free_list[2]对应的链表中拿到一个对象来使用。
  5. print( id(v2) )
  • dict类型,维护的free_list数组最多可缓存80个dict对象。
  1. v1 = {"k1":123}
  2. print( id(v1) ) # 输出:4515998128
  3. del v1
  4. v2 = {"name":"甲壳虫","age":18,"gender":"男"}
  5. print( id(v2) ) # 输出:4515998128

参考资料:https://pythonav.com/wiki/detail/6/88/

python进阶(7)垃圾回收机制的更多相关文章

  1. Python中的垃圾回收机制

    Python的垃圾回收机制 引子: 我们定义变量会申请内存空间来存放变量的值,而内存的容量是有限的,当一个变量值没有用了(简称垃圾)就应该将其占用的内存给回收掉,而变量名是访问到变量值的唯一方式,所以 ...

  2. python进阶之垃圾回收

    内存使用: 程序在运行的过程需要开辟内存空间,比如创建一个对象需要一片内存空间,定义变量需要内存空间.有内存的开辟,势必也要有内存的释放,不然只进不出那不是貔貅了吗?即使有开辟有释放在短期内还是会有垃 ...

  3. python入门之垃圾回收机制

    目录 一 引入 二.什么是垃圾回收机制? 三.为什么要用垃圾回收机制? 四.垃圾回收机制原理分析 4.1.什么是引用计数? 4.2.引用计数扩展阅读 4.2.1 标记-清除 4.2.2 分代回收 一 ...

  4. (编程语言+python+变量名+垃圾回收机制)*知识点

    编程语言 从低级到高级的发展的过程 1.机器语言 计算机是基于电工作的.(基于高.低电平 1010010101011) 如果用机器语言表现一个字符的意思需要多段代码的行.但是计算机读取的快. 所以机器 ...

  5. 编程语言类别;运行Python程序的方式;变量和常量;Python程序的垃圾回收机制;

    目录 编程语言分类 运行Python程序的两种方式 1.交互式 变量与常量 1.变量 2.常量 3.小整数池 垃圾回收机制 编程语言分类 编程语言分为: 1.机器语言:直接用二进制的0和1和计算机(C ...

  6. Python语法之垃圾回收机制

    目录 一 引入 二.什么是垃圾回收机制? 三.为什么要用垃圾回收机制? 四.垃圾回收机制原理分析 4.1.什么是引用计数? 4.2.引用计数扩展阅读 一 引入 解释器在执行到定义变量的语法时,会申请内 ...

  7. python中的垃圾回收机制及原理

    序言: 来一起看看: 不同于C/C++,像Python这样的语言是不需要程序员写代码来管理内存的,它的GC(Garbage Collection)机制 实现了自动内存管理.GC做的事情就是解放程序员的 ...

  8. python高级:垃圾回收机制

    ---恢复内容开始--- 垃圾回收机制 1.计数引用机制 就是一个变量.数据结构.对象当没有人引用时,python的会启用垃圾回收机制,将其从内存中删除. 怎么看引用的次数呢?sys模块提供的sys. ...

  9. 6、Python语法之垃圾回收机制

    一 .引入 解释器在执行到定义变量的语法时,会申请内存空间来存放变量的值,而内存的容量是有限的,这就涉及到变量值所占用内存空间的回收问题,当一个变量值没有用了(简称垃圾)就应该将其占用的内存给回收掉, ...

  10. python里面的垃圾回收机制

    文章链接:https://www.jianshu.com/p/1e375fb40506 Garbage collection(GC) 现在的高级语言如java,c#等,都采用了垃圾收集机制,而不再是c ...

随机推荐

  1. php之PDO连接mysql数据库,增删改查等等操作实例

    我们使用传统的 mysql_connect .mysql_query方法来连接查询数据库时,如果过滤不严就有SQL注入风险,导致网站被攻击. 虽然可以用mysql_real_escape_string ...

  2. 小心 Enum Parse 中的坑

    小心 Enum Parse 中的坑 Intro 最近使用枚举的时候,踩了一个小坑,分享一下,主要是枚举从 int 值转成枚举时可能会遇到 Sample 来看下面的示例: 首先定义一个枚举: publi ...

  3. 树的直径&树的重心

    树的直径 定义 那么树上最远的两个点,他们之间的距离,就被称之为树的直径. 树的直径的性质 1. 直径两端点一定是两个叶子节点. 2. 距离任意点最远的点一定是直径的一个端点,这个基于贪心求直径方法的 ...

  4. P3399 丝绸之路(DP)

    题目背景 张骞于公元前138年曾历尽艰险出使过西域.加强了汉朝与西域各国的友好往来.从那以后,一队队骆驼商队在这漫长的商贸大道上行进,他们越过崇山峻岭,将中国的先进技术带向中亚.西亚和欧洲,将那里的香 ...

  5. A. Vitaly and Strings

    Vitaly is a diligent student who never missed a lesson in his five years of studying in the universi ...

  6. hdu5317 RGCDQ

    Problem Description Mr. Hdu is interested in Greatest Common Divisor (GCD). He wants to find more an ...

  7. Navicat 使用 SSH 通道

    使用 Navicat for MySQL 通过跳板机登录 Mysql 时(使用 SSH 通道) 报错如下: SSH : Expected key exchange group packet from ...

  8. Kubernets二进制安装(9)之部署主控节点控制器controller-manager

    kube-controller-manager运行控制器,它们是处理集群中常规任务的后台线程 Controller Manager就是集群内部的管理控制中心,由负责不同资源的多个Controller构 ...

  9. ArcGIS制作MobileCache

    在使用ArcGIS Mobile进行二次开发时,矢量图层需要制作成MobileCache,才能在手持设备中加载. 下面介绍如何通过ArcMap制作MobileCache: 一.安装ArcGIS Mob ...

  10. mybatis(三)配置mapper.xml 的基本操作

    参考:https://www.cnblogs.com/wuzhenzhao/p/11101555.html XML 映射文件 本文参考mybatis中文官网进行学习总结:http://www.myba ...