前言
为什么 已经 del 析构了 name 变量,然后新的变量 xxxxx的 内存地址却跟原来name一样呢?
带着这个疑问看看了Python的内存管理机制。
一、内存管理机制
Python语言是由C实现的,所以想要剖析Python的内存管理机制,就需要下载Python的源码包看看C源代码是怎么写的?
C源码有2个关键目录
Include
Objects
1.两个重要的结构体
#define _PyObject_HEAD_EXTRA \ struct _object *_ob_next; \ struct _object *_ob_prev; #define PyObject_HEAD PyObject ob_base; #define PyObject_VAR_HEAD PyVarObject ob_base; 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; /* Number of items in variable part,即:元素个数 */ } PyVarObject;
Python中所有类型创建对象时,底层都是PyObject和PyVarObject结构体实现,一般情况下由单个元素组成对象内部会使用PyObject结构体(float)、由多个元素组成的对象内部会使用PyVarObject结构体(str/int/list/dict/tuple/set/自定义类),因为由多个元素组成的话是需要为其维护一个 ob_size(内部元素个数)。
Python 执行 v=0.4
C语言 1.开辟内存 2.数据初始化
ob_fval=0.3
ob_type=float
ob_refcnt=1
3.将对象放到双向链表中 ref_chain
Python 执行 name=v
0.不会重新开辟内存
1.ob_refcnt=1
Python执行 del v
ob_refcnt-1
Python执行
def fuc(arg):
print(123)
func(name)
1.arg参数刚进去 ob_refcnt+1
2.fuc执行完 ob_refcnt-1
Python执行 del name
1.ob_refcnt-1
每次ob_refcnt-1都检查是否为0
如果引用计数器为0就按理说 内存就应该销毁。
但是Python为了提升效率, 会对Python 某些数据类型做一些缓存机制;
为了减少开辟内存和销毁内存占用的时间,我们会把引用计数器为0的对象放到双向链表中,方便下次创建float类型时可以继续使用原来的内存地址
内存管理概述
Python是由C语言开发的解释器,任何操作Python都会调用C的代码。
PyObject: 指向上1个值指针、指向下1个值的指针、计数器、类型
PyobjectVarObject: PyObject、容器个数
在Python中每创建个对象,都会由C语言结构体内部都要维护4个值:双向链表、ob_refcnt、ob_type之后对内存中的数据进行初始化。
引用计数=0, 赋值然后将对象放到双向链表中refchain.
以后再有其他变量指向该内存 引用计算器+=1,如果销毁某个变量,则找到指向的内存,讲其计数器引用-1
引用计数器如果引用为0,则进行垃圾回收。
但是某些数据在内部可能存在缓存机制 例如float/list/int,在其引用计数器引用计数器=0时,不会真正销毁,而是放在 1个叫 free_list的链表中。
如果后期再创建同类型数据时,会取出链表中的对象,然后对对象进行初始化操作,重新赋上新值。
二、垃圾回收机制
Python的垃圾回收机制,以引用计数为主,标记清除、分代回收为辅。
1.引用计数
如果在Python里面创建了1个a=10的变量,那么a变量就指向了对象10。
Cpython解释器会记录对象10这个对象被变量使用的数量,当这个对象引用数量为0时 被回收。
2.标记清除
标记清除策略是对引用计数测试的补充,解决的问题就是当容器数据类型(list、tuple、dict、object)类数据 引用计数为0时,但是它们内部的数据却引用着其他数据。
我现在变量a、b已经访问不到 对象A、B了,但是A、B对象依然引用着其他数据。所以我需要把对象A、B都回收掉。
class A(): pass class B(): pass a=A() b=B() a.haha=b b.hehe=a #a---A #b---B a=None b=None print(a)
解决之道
标记清除(Mark—Sweep)算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。
3.分代回收
如果把Python中声明的所有容器类对象,都放在1个双向链表里进行循环检查 计数器是否为0 ?是否存在循环引用,是不是很耗时呢?
我们可以先把新创建的变量放在1个地方,如果没有以上3种垃圾回收机制都没有回收掉它----------》就放到另1个地方-------》如果还没有被回收掉就放在------------》 下一个地方。这就是分代管理。
分配内存 -> 发现超过阈值了 -> 触发垃圾回收 -> 将所有可收集对象链表放到一起 -> 遍历, 计算有效引用计数 -> 分成 有效引用计数=0 和 有效引用计数 > 0 两个集合 -> 大于0的, 放入到更老一代 -> =0的, 执行回收 -> 回收遍历容器内的各个元素, 减掉对应元素引用计数(破掉循环引用) -> 执行-1的逻辑, 若发现对象引用计数=0, 触发内存回收 -> python底层内存管理机制回收内存
解决之道
分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象