前言

 为什么 已经 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;
include/object.h

 

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的辅助垃圾收集技术处理那些容器对象

 

 

 

参考

posted on 2020-02-20 08:57  Martin8866  阅读(531)  评论(0编辑  收藏  举报