python垃圾回收机制为什么标记能解决循环引用问题_Python 源码阅读:垃圾回收机制...

原标题:Python 源码阅读:垃圾回收机制

概述

无论何种垃圾收集机制, 一般都是两阶段: 垃圾检测和垃圾回收.

在Python中, 大多数对象的生命周期都是通过对象的引用计数来管理的.

问题: 但是存在循环引用的问题: a 引用 b, b 引用 a, 导致每一个对象的引用计数都不为0, 所占用的内存永远不会被回收

要解决循环引用: 必需引入其他垃圾收集技术来打破循环引用. Python中使用了标记-清除以及分代收集

即, Python 中垃圾回收机制: 引用计数(主要), 标记清除, 分代收集(辅助)

引用计数

引用计数, 意味着必须在每次分配和释放内存的时候, 加入管理引用计数的动作

引用计数的优点: 最直观最简单, 实时性, 任何内存, 一旦没有指向它的引用, 就会立即被回收

计数存储

回顾 Python 的对象

e.g. 引用计数增加以及减少

>>>from sys importgetrefcount

>>>

>>>a=[1,2,3]

>>>getrefcount(a)

2

>>>b=a

>>>getrefcount(a)

3

>>>delb

>>>getrefcount(a)

2

计数增加

增加对象引用计数, refcnt incr

#define Py_INCREF(op) (

_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA

((PyObject*)(op))->ob_refcnt++)

计数减少

减少对象引用计数, refcnt desc

#define _Py_DEC_REFTOTAL _Py_RefTotal--

#define _Py_REF_DEBUG_COMMA ,

#define Py_DECREF(op)

do{

if(_Py_DEC_REFTOTAL_Py_REF_DEBUG_COMMA

--((PyObject*)(op))->ob_refcnt!=0)

_Py_CHECK_REFCNT(op)

else

_Py_Dealloc((PyObject *)(op));

}while(0)

即, 发现refcnt变成0的时候, 会调用_Py_Dealloc

PyAPI_FUNC(void)_Py_Dealloc(PyObject *);

#define _Py_REF_DEBUG_COMMA ,

#define _Py_Dealloc(op) (

_Py_INC_TPFREES(op)_Py_COUNT_ALLOCS_COMMA

(*Py_TYPE(op)->tp_dealloc)((PyObject *)(op)))

#endif /* !Py_TRACE_REFS */

会调用各自类型的tp_dealloc

例如dict

PyTypeObjectPyDict_Type={

PyVarObject_HEAD_INIT(&PyType_Type,0)

"dict",

sizeof(PyDictObject),

0,

(destructor)dict_dealloc,/* tp_dealloc */

....

}

staticvoid

dict_dealloc(register PyDictObject *mp)

{

.....

// 如果满足条件, 放入到缓冲池freelist中

if(numfree tp_free((PyObject *)mp);

Py_TRASHCAN_SAFE_END(mp)

}

Python基本类型的tp_dealloc, 通常都会与各自的缓冲池机制相关, 释放会优先放入缓冲池中(对应的分配会优先从缓冲池取). 这个内存分配与回收同缓冲池机制相关

当无法放入缓冲池时, 会调用各自类型的tp_free

int, 比较特殊

// int, 通用整数对象缓冲池机制

(freefunc)int_free,/* tp_free */

string

// string

PyObject_Del,/* tp_free */

dict/tuple/list

PyObject_GC_Del, /* tp_free */

然后, 我们再回头看, 自定义对象的tp_free

PyTypeObjectPyType_Type={

PyVarObject_HEAD_INIT(&PyType_Type,0)

"type",/* tp_name */

...

PyObject_GC_Del,/* tp_free */

};

即, 最终, 当计数变为0, 触发内存回收动作. 涉及函数PyObject_Del和PyObject_GC_Del, 并且, 自定义类以及容器类型(dict/list/tuple/set等)使用的都是后者PyObject_GC_Del.

内存回收 PyObject_Del / PyObject_GC_Del

如果引用计数=0:

1.放入缓冲池

2.真正销毁,PyObject_Del/PyObject_GC_Del内存操作

这两个操作都是进行内存级别的操作

PyObject_Del

PyObject_Del(op) releases the memory allocated for an object. It does not

run a destructor — it only frees the memory. PyObject_Free is identical.

这块删除, PyObject_Free 涉及到了Python底层内存的分配和管理机制, 具体见前面的博文

PyObject_GC_Del

void

PyObject_GC_Del(void*op)

{

PyGC_Head *g=AS_GC(op);

// Returns true if a given object is tracked

if(IS_TRACKED(op))

// 从跟踪链表中移除

gc_list_remove(g);

if(generations[0].count>0){

generations[0].count--;

}

PyObject_FREE(g);

}

IS_TRACKED 涉及到标记-清除的机制

generations 涉及到了分代回收

PyObject_FREE, 则和Python底层内存池机制相关

标记-清除

问题: 什么对象可能产生循环引用?

PyIntObject/PyStringObject等不可能

Python中的循环引用总是发生在container对象之间, 所谓containser对象即是内部可持有对其他对象的引用: list/dict/class/instance等等

垃圾收集带来的开销依赖于container对象的数量, 必需跟踪所创建的每一个container对象, 并将这些对象组织到一个集合中.

可收集对象链表

可收集对象链表: 将需要被收集和跟踪的container, 放到可收集的链表中

任何一个python对象都分为两部分: PyObject_HEAD + 对象本身数据

/* PyObject_HEAD defines the initial segment of every PyObject. */

#define PyObject_HEAD

_PyObject_HEAD_EXTRA

Py_ssize_tob_refcnt;

struct_typeobject *ob_type;

//----------------------------------------------------

#define _PyObject_HEAD_EXTRA

struct_object *_ob_next;

struct_object *_ob_prev;

// 双向链表结构, 垃圾回收

可收集对象链表

Modules/gcmodule.c

/* GC information is stored BEFORE the object structure. */

typedefunion_gc_head{

struct{

// 建立链表需要的前后指针

union _gc_head *gc_next;

union _gc_head *gc_prev;

// 在初始化时会被初始化为 GC_UNTRACED

Py_ssize_tgc_refs;

}gc;

longdoubledummy;/* force worst-case alignment */

}PyGC_Head;

创建container的过程: container对象 = pyGC_Head | PyObject_HEAD | Container Object

PyObject *

_PyObject_GC_New(PyTypeObject *tp)

{

PyObject *op=_PyObject_GC_Malloc(_PyObject_SIZE(tp));

if(op!=NULL)

op=PyObject_INIT(op,tp);

returnop;

}

=>_PyObject_GC_Malloc

#define _PyGC_REFS_UNTRACKED (-2)

#define GC_UNTRACKED _PyGC_REFS_UNTRACKED

PyObject *

_PyObject_GC_Malloc(size_tbasicsize)

{

PyObject *op;

PyGC_Head *g;

if(basicsize>PY_SSIZE_T_MAX-sizeof(PyGC_Head))

returnPyErr_NoMemory();

// 为 对象本身+PyGC_Head申请内存, 注意分配的size

g=(PyGC_Head *)PyObject_MALLOC(

sizeof(PyGC_Head)+basicsize);

if(g==NULL)

returnPyErr_NoMemory();

// 初始化 GC_UNTRACED

g->gc.gc_refs=GC_UNTRACKED;

generations[0].count++;/* number of allocated GC objects */

// 如果大于阈值, 执行分代回收

if(generations[0].count>generations[0].threshold&

enabled&&

generations[0].threshold&&

!collecting&&

!PyErr_Occurred()){

collecting=1;

collect_generations();

collecting=0;

}

op=FROM_GC(g);

returnop;

}

PyObject_HEAD and PyGC_HEAD

注意, FROM_GC和AS_GC用于 PyObject_HEAD PyGC_HEAD地址相互转换

Modules/gcmodule.c

/* Get an object's GC head */

#define AS_GC(o) ((PyGC_Head *)(o)-1)

/* Get the object given the GC head */

#define FROM_GC(g) ((PyObject *)(((PyGC_Head *)g)+1))

objimpl.h

#define _Py_AS_GC(o) ((PyGC_Head *)(o)-1)

问题: 什么时候将container放到这个对象链表中

e.g list

listobject.c

PyObject *

PyList_New(Py_ssize_tsize)

{

PyListObject *op;

op=PyObject_GC_New(PyListObject,&PyList_Type);

_PyObject_GC_TRACK(op);

return(PyObject *)op;

}

// => _PyObject_GC_TRACK

// objimpl.h

// 加入到可收集对象链表中

#define _PyObject_GC_TRACK(o) do {

PyGC_Head *g=_Py_AS_GC(o);

if(g->gc.gc_refs!=_PyGC_REFS_UNTRACKED)

Py_FatalError("GC object already tracked");

g->gc.gc_refs=_PyGC_REFS_REACHABLE;

g->gc.gc_next=_PyGC_generation0;

g->gc.gc_prev=_PyGC_generation0->gc.gc_prev;

g->gc.gc_prev->gc.gc_next=g;

_PyGC_generation0->gc.gc_prev=g;

}while(0);

问题: 什么时候将container从这个对象链表中摘除

// Objects/listobject.c

staticvoid

list_dealloc(PyListObject *op)

{

Py_ssize_ti;

PyObject_GC_UnTrack(op);

.....

}

// => PyObject_GC_UnTrack => _PyObject_GC_UNTRACK

// 对象销毁的时候

#define _PyObject_GC_UNTRACK(o) do {

PyGC_Head *g=_Py_AS_GC(o);

assert(g->gc.gc_refs!=_PyGC_REFS_UNTRACKED);

g->gc.gc_refs=_PyGC_REFS_UNTRACKED;

g->gc.gc_prev->gc.gc_next=g->gc.gc_next;

g->gc.gc_next->gc.gc_prev=g->gc.gc_prev;

g->gc.gc_next=NULL;

}while(0);

问题: 如何进行标记-清除

现在, 我们得到了一个链表

Python将自己的垃圾收集限制在这个链表上, 循环引用一定发生在这个链表的一群独享之间.

0. 概览

_PyObject_GC_Malloc 分配内存时, 发现超过阈值, 此时, 会触发gc, collect_generations然后调用collect, collect包含标记-清除逻辑

=>gc_list_merge

// 执行拷贝而已

/* append list `from` onto list `to`; `from` becomes an empty list */

staticvoid

gc_list_merge(PyGC_Head *from,PyGC_Head *to)

{

PyGC_Head *tail;

assert(from!=to);

if(!gc_list_is_empty(from)){

tail=to->gc.gc_prev;

tail->gc.gc_next=from->gc.gc_next;

tail->gc.gc_next->gc.gc_prev=tail;

to->gc.gc_prev=from->gc.gc_prev;

to->gc.gc_prev->gc.gc_next=to;

}

// 清空

gc_list_init(from);

}

=>

staticvoid

gc_list_init(PyGC_Head *list)

{

list->gc.gc_prev=list;

list->gc.gc_next=list;

}

即, 此刻, 所有待进行处理的对象都集中在同一个链表中

处理,

其逻辑是, 要去除循环引用, 得到有效引用计数

有效引用计数: 将循环引用的计数去除, 最终得到的 => 将环从引用中摘除, 各自引用计数数值-1

实际操作, 并不要直接修改对象的 ob_refcnt, 而是修改其副本, PyGC_Head中的gc.gc_ref

2. 第二步: update_refs

遍历对象链表, 将每个对象的gc.gc_ref值设置为ob_refcnt

3. 第三步: 计算有效引用计数

/* A traversal callback for subtract_refs. */

staticint

visit_decref(PyObject *op,void*data)

{

assert(op!=NULL);

// 判断op指向的对象是否是被垃圾收集监控的, 对象的type对象中有Py_TPFLAGS_HAVE_GC符号

if(PyObject_IS_GC(op)){

PyGC_Head *gc=AS_GC(op);

/* We're only interested in gc_refs for objects in the

* generation being collected, which can be recognized

* because only they have positive gc_refs.

*/

assert(gc->gc.gc_refs!=0);/* else refcount was too small */

if(gc->gc.gc_refs>0)

gc->gc.gc_refs--;// -1

}

return0;

}

/* Subtract internal references from gc_refs. After this, gc_refs is >= 0

* for all objects in containers, and is GC_REACHABLE for all tracked gc

* objects not in containers. The ones with gc_refs > 0 are directly

* reachable from outside containers, and so can't be collected.

*/

staticvoid

subtract_refs(PyGC_Head *containers)

{

traverseproctraverse;

PyGC_Head *gc=containers->gc.gc_next;

// 遍历链表

for(;gc!=containers;gc=gc->gc.gc_next){

// 与特定的类型相关, 得到类型对应的traverse函数

traverse=Py_TYPE(FROM_GC(gc))->tp_traverse;

// 调用

(void)traverse(FROM_GC(gc),

(visitproc)visit_decref,// 回调形式传入

NULL);

}

}

我们可以看看dictobject的traverse函数

staticint

dict_traverse(PyObject *op,visitprocvisit,void*arg)

{

Py_ssize_ti=0;

PyObject *pk;

PyObject *pv;

// 遍历所有键和值

while(PyDict_Next(op,&i,&pk,&pv)){

Py_VISIT(pk);

Py_VISIT(pv);

}

return0;

}

逻辑大概是: 遍历容器对象里面的所有对象, 通过visit_decref将这些对象的引用计数都-1,

最终, 遍历完链表之后, 整个可收集对象链表中所有container对象之间的循环引用都被去掉了

4. 第四步: 垃圾标记

move_unreachable, 将可收集对象链表中, 根据有效引用计数 不等于0(root对象) 和 等于0(非root对象, 垃圾, 可回收), 一分为二

5. 第五步: 将存活对象放入下一代

/* Move reachable objects to next generation. */

if(young!=old){

if(generation==NUM_GENERATIONS-2){

long_lived_pending+=gc_list_size(young);

}

gc_list_merge(young,old);

}

else{

/* We only untrack dicts in full collections, to avoid quadratic

dict build-up. See issue #14775. */

untrack_dicts(young);

long_lived_pending=0;

long_lived_total=gc_list_size(young);

}

6. 第六步: 执行回收

7. gc逻辑

分配内存

->发现超过阈值了

->触发垃圾回收

->将所有可收集对象链表放到一起

->遍历,计算有效引用计数

->分成有效引用计数=0和有效引用计数>0两个集合

->大于0的,放入到更老一代

-> =0的,执行回收

->回收遍历容器内的各个元素,减掉对应元素引用计数(破掉循环引用)

->执行-1的逻辑,若发现对象引用计数=0,触发内存回收

->python底层内存管理机制回收内存

分代回收

分代收集: 以空间换时间

思想: 将系统中的所有内存块根据其存货的时间划分为不同的集合, 每个集合就成为一个”代”, 垃圾收集的频率随着”代”的存活时间的增大而减小(活得越长的对象, 就越不可能是垃圾, 就应该减少去收集的频率)

Python中, 引入了分代收集, 总共三个”代”. Python 中, 一个代就是一个链表, 所有属于同一”代”的内存块都链接在同一个链表中

表头数据结构

gcmodule.c

structgc_generation{

PyGC_Headhead;

intthreshold;/* collection threshold */// 阈值

intcount;/* count of allocations or collections of younger

generations */// 实时个数

};

三个代的定义

#define NUM_GENERATIONS 3

#define GEN_HEAD(n) (&generations[n].head)

// 三代都放到这个数组中

/* linked lists of container objects */

staticstructgc_generationgenerations[NUM_GENERATIONS]={

/* PyGC_Head, threshold, count */

{{{GEN_HEAD(0),GEN_HEAD(0),0}},700,0},//700个container, 超过立即触发垃圾回收机制

{{{GEN_HEAD(1),GEN_HEAD(1),0}},10,0},// 10个

{{{GEN_HEAD(2),GEN_HEAD(2),0}},10,0},// 10个

};

PyGC_Head *_PyGC_generation0=GEN_HEAD(0);

超过阈值, 触发垃圾回收

Python 中的gc模块

gc模块, 提供了观察和手动使用gc的接口

import gc

gc.set_debug(gc.DEBUG_STATS|gc.DEBUG_LEAK)

gc.collect()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值