python垃圾回收机制

本文介绍了Python的垃圾回收机制,包括引用计数、标记清除和分代收集策略,讨论了它们的优缺点,并通过C语言底层源码分析了Python如何实现这些机制。此外,还提到了Python的缓存机制,以提高程序效率。
摘要由CSDN通过智能技术生成

Hello,大家好。本期来和大家一起了解一下python垃圾回收机制。

垃圾回收机制

概念

垃圾回收机制是一种内存管理技术,用于自动识别和回收程序中不再使用的内存。这样可以避免内存泄漏,保证应用程序在运行过程中不会耗尽内存资源。

哪些语言支持,哪些语言不支持?

支持垃圾回收机制的语言:

  • Java

  • C#

  • Python

  • Ruby

  • JavaScript

  • Go

不支持垃圾回收机制的语言:

  • C

  • C++

  • Rust

  • Swift

不同的语言有不同的内存管理方法,因此开发者需要根据语言特性和需求选择合适的方式。

垃圾回收机制的优缺点

优点:
  • 采用垃圾回收机制:

  • 1.简化内存管理,减少了代码复杂度和错误的风险。

  • 2.避免内存泄漏,确保程序在运行过程中有足够的内存资源。

  • 3.提高程序效率和稳定性。

  • 不采用垃圾回收机制:

  • 1.更好的控制内存分配,避免因垃圾回收机制带来的性能损失。

  • 2.更容易实现特殊的内存管理需求。

缺点:
  • 采用垃圾回收机制:

  • 1.可能带来一定的性能损失,因为垃圾回收机制需要定期运行以回收内存。

  • 2.可能出现内存碎片问题,因为回收的内存空间可能不连续。

  • 不采用垃圾回收机制:

  • 1.增加了代码复杂度,需要开发者手动管理内存。

  • 2.容易出现内存泄漏问题,如果开发者不正确处理内存,可能导致程序耗尽内存资源。

开发者需要根据具体的需求和应用场景来决定是否采用垃圾回收机制。

python垃圾回收实现的原理

python采用的是引用计数机制为主,标记清除和分代收集(隔代回收)两种机制为辅的策略。

C语言底层源码

首先我们要认识2个结构体(C语言),因为python底层是用C语言实现的。

#define PyObject_HEAD       PyObject ob_base;
#define PyObject_VAR_HEAD      PyVarObject ob_base;

// 宏定义,包含 上一个、下一个,用于构造双向链表用。(放到refchain链表中时,要用到)
#define _PyObject_HEAD_EXTRA            \
    struct _object *_ob_next;           \
    struct _object *_ob_prev;
    
typedef struct _object {
    _PyObject_HEAD_EXTRA // 用于构造双向链表
    Py_ssize_t ob_refcnt;  // 引用计数器
    struct _typeobject *ob_type;    // 数据类型
} PyObject;

typedef struct {
    PyObject ob_base;   // PyObject对象
    Py_ssize_t ob_size; // 元素个数
} PyVarObject;

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

Python的C源码中有一个名为refchain的环状双向链表,Python程序中一旦创建对象都会把这个对象添加到refchain这个链表中。

并且每个类型的对象在创建时都有PyObject中的那4部分数据;

list/set/tuple等由多个元素组成对象创建时都有PyVarObject中的那5部分数据。

因此refchain环状双向链表保存着Python程序创建的所有对象。

引用计数机制

概念:在refchain中的所有对象内部都有一个ob_refcnt用来保存当前对象的引用计数器,顾名思义就是自己被引用的次数,

过程:当值被多次引用时候,不会在内存中重复创建数据,而是引用计数器+1 。当对象被销毁时候同时会让引用计数器-1,如果引用计数器为0,则将对象从refchain链表中摘除,同时在内存中进行销毁(暂不考虑缓存等特殊情况)。

引用计数+1的情况:

  • 对象被创建,例如a=23

  • 对象被引用,例如b=a

  • 对象被作为参数,传入到一个函数中,例如func(a)

  • 对象作为一个元素,存储在容器中,例如list1=[a,a]

引用计数-1的情况:

  • 对象的别名被显式销毁,例如del a

  • 对象的别名被赋予新的对象,例如a=24

  • 一个对象离开它的作用域,例如:func函数执行完毕时,func函数中的局部变量(全局变量不会)

  • 对象所在的容器被销毁,或从容器中删除对象

引用计数优点:

1、简单

2、实时性:一旦没有引用,内存就直接释放了,不用像其他机制得等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。

引用计数缺点:

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

2、循环引用导致内存泄露

循环引用导致内存泄露案例:

v1 = [11, 22, 33]  # refchain中创建一个v1列表对象,[11, 22, 33]列表对象引用计数为1。
v2 = [44, 55, 66]  # refchain中创建一个v2列表对象,[44, 55, 66]列表对象引用计数为1。

v1.append(v2)  # 把v2添加到v1中,则v2对应的[44, 55, 66]列表对象引用计数加1,变成2。
v2.append(v1)  # 把v1添加到v2中,则v1对应的[11, 22, 33]列表对象引用计数加1,变成2。

del v1  # 删除v1,则v1对应的[11, 22, 33]列表对象引用计数减1,变成1.
del v2  # 删除v2,则v2对应的[44, 55, 66]列表对象引用计数减1,变成1.

'''
因为循环引用,即使del v1,但其对应的[11, 22, 33]列表对象引用计数最终1,而不是0。
所以v1对象不会被销毁,垃圾回收器不会回收v1,这就会导致内存泄露。
'''

标记清除

目的:为了解决引用计数器循环引用的不足。

该算法在进行垃圾回收时分为两步:

  • 标记阶段:创建特殊链表专门用于保存 列表、元组、字典、集合、自定义类等对象并标记。

  • 清除阶段:在某种情况下扫描检查这个链表中的对象是否存在循环引用,如果存在则让双方的引用计数器均-1,如果引用计数器是0则垃圾回收。

存在的问题:

1.什么时候扫描?

2.可能存在循环引用的链表扫描代价大,每次扫描耗时久。

所以针对这个问题,Python采用了分代回收的机制

分代回收

目的:为了解决标记清除存在的部分问题

过程:对标记清除中的链表进行优化,将可能存在循环引用的对象拆分到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代扫描检查。

例如:0代对象个数达到800个时扫描1次,如果有循环引用各减1,引用计数为0的则回收,不是0的则升级到1代,标记扫描次数1次。

1代则是0代扫描10次,则1代自身扫描1次。

2代则是1代扫描10次,则2代自身扫描1次。从而解决扫描代价太大的问题,分成了3代提升了效率。

小结

python的垃圾回收机制是怎么运行的?

答:

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

但是,在python中对于那些可以有多个元素组成的对象可能会存在循环引用的问题,为了解决这个问题python又引入了标记清除和分代回收,在其内部维护了4个链表,refchain、0代、1代、2代。在源码内部当达到各自的阈值时,就会触发扫描链表进行标记清除的动作(有循环引用则各自-1)。

情景模拟python垃圾回收机制

根据C语言底层并结合图来讲解内存管理和垃圾回收的详细过程。

第一步:当创建对象age=19时,会将对象添加到refchain链表中。

第二步:当创建对象num_list = [11,22]时,会将列表对象添加到 refchain 和 0代 中。

第三步:若再创建一个新对象使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.

对拼接起来的链表在进行扫描时,主要就是剔除循环引用和销毁垃圾,至此,垃圾回收的过程结束。

补充:python缓存机制

从上文大家可以了解到当对象的引用计数为0时,就会被销毁并释放内存。而实际上他不是这么的简单粗暴,因为反复的创建和销毁会使程序的执行效率变低。因此Python中引入了“缓存机制”机制。

例如:引用计数器为0时,不会真正销毁对象,而是将他放到一个名为 free_list 的链表中,之后再创建对象时不会再重新开辟内存,而是在free_list中将之前的对象拿来并重置内部的值来使用。

int类型:

v2 = 9
v3 = 9
print(id(v2), id(v3))
'''
运行结果:
2150737603120 2150737603120
'''

str类型:

str1 = "aaa"
str2 = "aaa"
print(id(str1), id(str2))
'''
运行结果:
2705512624944 2705512624944
'''

等等。。。

关于垃圾回收机制的更多用法,欢迎小伙伴后台留言哦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值