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